Repository: openvinotoolkit/datumaro
Pull request #920 (2023-04-07 03:56:53)

[Doc] add tutorials for level 3 and 4
<!-- Contributing guide: https://github.com/openvinotoolkit/datumaro/blob/develop/CONTRIBUTING.md -->

### Summary

Please take a look at http://10.225.20.174:7041/build/html/docs/level-up/basic_skills/03_dataset_import_export.html
and http://10.225.20.174:7041/build/html/docs/level-up/basic_skills/04_detect_data_format.html#level-4-detect-data-format-from-an-unknown-dataset for references.

<!--
Resolves #111 and #222.
Depends on #1000 (for series of dependent commits).

This PR introduces this capability to make the project better in this and that.

- Added this feature
- Removed that feature
- Fixed the problem #1234
-->

### How to test
<!-- Describe the testing procedure for reviewers, if changes are
not fully covered by unit tests or manual testing can be complicated. -->

### Checklist
<!-- Put an 'x' in all the boxes that apply -->
- [ ] I have added unit tests to cover my changes.​
- [ ] I have added integration tests to cover my changes.​
- [ ] I have added the description of my changes into [CHANGELOG](https://github.com/openvinotoolkit/datumaro/blob/develop/CHANGELOG.md).​
- [ ] I have updated the [documentation](https://github.com/openvinotoolkit/datumaro/tree/develop/docs) accordingly

### License

- [ ] I submit _my code changes_ under the same [MIT License](https://github.com/openvinotoolkit/datumaro/blob/develop/LICENSE) that covers the project.
  Feel free to contact the maintainers if that's a concern.
- [ ] I have updated the license header for each file (see an example below).

```python
# Copyright (C) 2023 Intel Corporation
#
# SPDX-License-Identifier: MIT
```



Patch:
diff --git a/.github/workflows/code_scan.yml b/.github/workflows/code_scan.yml
index 6c95d4001..b2cb1eb04 100644
--- a/.github/workflows/code_scan.yml
+++ b/.github/workflows/code_scan.yml
@@ -16,11 +16,10 @@ jobs:
         uses: actions/setup-python@v4
         with:
           python-version: 3.8
-      - name: Run checks
-        run: |
-          pip install --user -r tests/requirements-linters.txt
-          echo "Bandit version: "`bandit --version | head -1`
-          bandit -r -c ipas_default.config ./ -f txt -o bandit-report.txt
+      - name: Install dependencies
+        run: python -m pip install tox
+      - name: Bandit Scanning
+        run: tox -e bandit-scan
       - name: Upload Bandit artifact
         uses: actions/upload-artifact@v3
         with:
@@ -28,9 +27,10 @@ jobs:
           path: .tox/bandit-report.txt
         # Use always() to always run this step to publish scan results when there are test failures
         if: ${{ always() }}
-      - name: Microsoft Teams Notification
-        uses: skitionek/notify-microsoft-teams@master
-        if: failure()
-        with:
-          webhook_url: ${{ secrets.MSTEAMS_WEBHOOK }}
-          overwrite: "{title: `Code Scanning failure`, text: Failure in `${workflow_link}`}"
+  call-notify-to-teams:
+    needs: [Bandit]
+    if: |
+      always() &&
+      contains(needs.*.result, 'failure')
+    uses: ./.github/workflows/notify_teams.yml
+    secrets: inherit
diff --git a/.github/workflows/health_check.yml b/.github/workflows/health_check.yml
index d6f5fe8f3..d83aeb4dd 100644
--- a/.github/workflows/health_check.yml
+++ b/.github/workflows/health_check.yml
@@ -26,7 +26,7 @@ jobs:
       - name: Installing dependencies
         run: |
           python -m pip install -r tests/requirements.txt
-          python -m pip install -e '.[default,tf]'
+          python -m pip install -e '.[default,tf,tfds-dev]'
       - name: Code instrumentation
         run: |
           python -m pytest -v --cov --cov-report xml:coverage.xml
diff --git a/.github/workflows/issue_assignment.yml b/.github/workflows/issue_assignment.yml
index 0c6cb3830..e6665b4a8 100644
--- a/.github/workflows/issue_assignment.yml
+++ b/.github/workflows/issue_assignment.yml
@@ -1,16 +1,18 @@
 name: Issue assignment
 
 on:
+  workflow_dispatch: # For testing
   issues:
     types: [opened]
 
-    jobs:
-      auto-assign:
-        runs-on: ubuntu-latest
-        steps:
-          - name: Auto-assign Issue
-          uses: pozil/auto-assign-issue@v1.11.0
-          with:
-            assignees: vinnamkim,JihwanEom,sooahleex,bonhunko
-            numOfAssignee: 1
-            allowSelfAssign: false
+jobs:
+  auto-assign:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Auto-assign Issue
+        uses: pozil/auto-assign-issue@v1.11.0
+        with:
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          assignees: vinnamkim,JihwanEom,sooahleex,bonhunko
+          numOfAssignee: 1
+          allowSelfAssign: false
diff --git a/.github/workflows/nightly_check.yml b/.github/workflows/nightly_check.yml
index 576ea907e..26b0c4d18 100644
--- a/.github/workflows/nightly_check.yml
+++ b/.github/workflows/nightly_check.yml
@@ -33,9 +33,10 @@ jobs:
         with:
           name: test-results
           path: nightly_regression_test_report.html
-      - name: Microsoft Teams Notification
-        uses: skitionek/notify-microsoft-teams@master
-        if: failure()
-        with:
-          webhook_url: ${{ secrets.MSTEAMS_WEBHOOK }}
-          overwrite: "{title: `Nightly Regression Test failure`, text: Failure in `${workflow_link}`}"
+  call-notify-to-teams:
+    needs: [nightly_regression_test]
+    if: |
+      always() &&
+      contains(needs.*.result, 'failure')
+    uses: ./.github/workflows/notify_teams.yml
+    secrets: inherit
diff --git a/.github/workflows/notify_teams.yml b/.github/workflows/notify_teams.yml
new file mode 100644
index 000000000..e72b8ec43
--- /dev/null
+++ b/.github/workflows/notify_teams.yml
@@ -0,0 +1,43 @@
+name: Notify to Microsoft Teams
+
+on:
+  workflow_call:
+    secrets:
+      MSTEAMS_WEBHOOK:
+        required: true
+
+jobs:
+  notify-to-teams:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Microsoft Teams Notification
+        uses: skitionek/notify-microsoft-teams@master
+        if: always()
+        with:
+          webhook_url: ${{ secrets.MSTEAMS_WEBHOOK }}
+          raw: >-
+            {
+              "@type": "MessageCard",
+              "@context": "http://schema.org/extensions",
+              "title": "Failure on ${{ github.workflow }}",
+              "summary": "Failure on ${{ github.workflow }}",
+              "sections": [
+                {
+                  "activityTitle": "Failure on ${{ github.workflow }}",
+                  "activitySubtitle": "",
+                  "activityImage": "https://raw.githubusercontent.com/Skitionek/notify-microsoft-teams/master/icons/failure.png"
+                }
+              ],
+              "potentialAction": [
+                {
+                  "@type": "OpenUri",
+                  "name": "Go to failure",
+                  "targets": [
+                    {
+                      "os": "default",
+                      "uri": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+                    }
+                  ]
+                }
+              ]
+            }
diff --git a/.github/workflows/pypi_publish.yml b/.github/workflows/pypi_publish.yml
deleted file mode 100644
index 9745b5d71..000000000
--- a/.github/workflows/pypi_publish.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Publish to PyPI
-
-on:
-  release:
-    types: [published]
-
-jobs:
-  pypi-publish:
-    environment: pypi
-    name: Upload release to PyPI
-    runs-on: ubuntu-latest
-    steps:
-    - name: Test
-      env:
-        TEST_SECRET: ${{ secrets.PYPI_API_TOKEN }}
-      run: |
-        echo ${TEST_SECRET}
-    - name: Checkout
-      uses: actions/checkout@v3
-    - name: Set up Python 3.9
-      uses: actions/setup-python@v3
-      with:
-        python-version: "3.9"
-    - name: Install pypa/build
-      run: >-
-        python -m
-        pip install
-        build
-        --user
-    - name: Build a binary wheel and a source tarball
-      run: >-
-        python -m
-        build
-        --sdist
-        --wheel
-        --outdir dist/
-        .
-    - name: Publish package distributions to PyPI
-      uses: pypa/gh-action-pypi-publish@v1.7.1
-      with:
-        password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/weekly_check.yml b/.github/workflows/weekly_check.yml
index 407f76021..ae24a71d1 100644
--- a/.github/workflows/weekly_check.yml
+++ b/.github/workflows/weekly_check.yml
@@ -36,9 +36,10 @@ jobs:
         with:
           name: test-results
           path: report/stability_test_report.html
-      - name: Microsoft Teams Notification
-        uses: skitionek/notify-microsoft-teams@master
-        if: failure()
-        with:
-          webhook_url: ${{ secrets.MSTEAMS_WEBHOOK }}
-          overwrite: "{title: `Weekly Stability Test failure`, text: Failure in `${workflow_link}`}"
+  call-notify-to-teams:
+    needs: [weekly_stability_test]
+    if: |
+      always() &&
+      contains(needs.*.result, 'failure')
+    uses: ./.github/workflows/notify_teams.yml
+    secrets: inherit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93ed10c8f..7943065d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   (<https://github.com/openvinotoolkit/datumaro/pull/909>)
 - Refactor CLI commands w/ and w/o project
   (<https://github.com/openvinotoolkit/datumaro/pull/910>)
+- Refactor hl_ops.py
+  (<https://github.com/openvinotoolkit/datumaro/pull/912>)
 - Add tfds:uc_merced and tfds:eurosat download
   (<https://github.com/openvinotoolkit/datumaro/pull/914>)
 
@@ -25,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   (<https://github.com/openvinotoolkit/datumaro/pull/907>)
 - Fix log issue when importing celeba and align celeba dataset
   (<https://github.com/openvinotoolkit/datumaro/pull/919>)
+- Save extra images of PointCloud when exporting to datumaro format
+  (<https://github.com/openvinotoolkit/datumaro/pull/918>)
 
 ## 28/03/2023 - Release 1.1.1
 ### Bug fixes
diff --git a/datumaro/__init__.py b/datumaro/__init__.py
index acbced3fd..81b98b7de 100644
--- a/datumaro/__init__.py
+++ b/datumaro/__init__.py
@@ -42,14 +42,7 @@ from .components.dataset_base import (
 )
 from .components.environment import Environment, PluginRegistry
 from .components.exporter import Exporter, ExportErrorPolicy, FailingExportErrorPolicy
-from .components.hl_ops import (  # pylint: disable=redefined-builtin
-    export,
-    filter,
-    merge,
-    run_model,
-    transform,
-    validate,
-)
+from .components.hl_ops import HLOps
 from .components.importer import FailingImportErrorPolicy, Importer, ImportErrorPolicy
 from .components.launcher import Launcher, ModelTransform
 from .components.media import ByteImage, Image, MediaElement, Video, VideoFrame
diff --git a/datumaro/components/filter.py b/datumaro/components/filter.py
index 36068a27f..f85b6263a 100644
--- a/datumaro/components/filter.py
+++ b/datumaro/components/filter.py
@@ -5,7 +5,7 @@
 import logging as log
 
 # Disable B410: import_lxml - the library is used for writing
-from lxml import etree as ET  # nosec, lxml has proper XPath implementation
+from lxml import etree as ET  # nosec
 
 from datumaro.components.annotation import (
     Annotation,
diff --git a/datumaro/components/hl_ops.py b/datumaro/components/hl_ops.py
deleted file mode 100644
index acf478310..000000000
--- a/datumaro/components/hl_ops.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# Copyright (C) 2022 Intel Corporation
-#
-# SPDX-License-Identifier: MIT
-
-import inspect
-import os
-import os.path as osp
-import shutil
-from typing import Dict, Optional, Type, Union
-
-from datumaro.components.dataset import Dataset, DatasetItemStorageDatasetView, IDataset
-from datumaro.components.environment import Environment
-from datumaro.components.exporter import Exporter
-from datumaro.components.filter import XPathAnnotationsFilter, XPathDatasetFilter
-from datumaro.components.launcher import Launcher, ModelTransform
-from datumaro.components.merge.exact_merge import ExactMerge
-from datumaro.components.transformer import Transform
-from datumaro.components.validator import TaskType, Validator
-from datumaro.util import parse_str_enum_value
-from datumaro.util.scope import on_error_do, scoped
-
-
-def transform(
-    dataset: IDataset,
-    method: Union[str, Type[Transform]],
-    *,
-    env: Optional[Environment] = None,
-    **kwargs,
-) -> IDataset:
-    """
-    Applies some function to dataset items.
-
-    Results are computed lazily, if the transform supports this.
-
-    Args:
-        dataset: The dataset to be transformed
-        method: The transformation to be applied to the dataset.
-            If a string is passed, it is treated as a plugin name,
-            which is searched for in the environment
-            set by the 'env' argument
-        env: A plugin collection. If not set, the built-in plugins are used
-        **kwargs: Parameters for the transformation
-
-    Returns: a wrapper around the input dataset
-    """
-
-    if isinstance(method, str):
-        if env is None:
-            env = Environment()
-        method = env.transforms[method]
-
-    if not (inspect.isclass(method) and issubclass(method, Transform)):
-        raise TypeError("Unexpected 'method' argument type: %s" % type(method))
-
-    produced = method(dataset, **kwargs)
-
-    return Dataset(source=produced, env=env)
-
-
-def filter(
-    dataset: IDataset,
-    expr: str,
-    *,  # pylint: disable=redefined-builtin
-    filter_annotations: bool = False,
-    remove_empty: bool = False,
-) -> IDataset:
-    """
-    Filters out some dataset items or annotations, using a custom filter
-    expression.
-
-    Args:
-        dataset: The dataset to be filtered
-        expr: XPath-formatted filter expression
-            (e.g. `/item[subset = 'train']`, `/item/annotation[label = 'cat']`)
-        filter_annotations: Indicates if the filter should be
-            applied to items or annotations
-        remove_empty: When filtering annotations, allows to
-            exclude empty items from the resulting dataset
-
-    Returns: a wrapper around the input dataset, which is computed lazily
-        during iteration
-    """
-
-    if filter_annotations:
-        return transform(dataset, XPathAnnotationsFilter, xpath=expr, remove_empty=remove_empty)
-    else:
-        if not expr:
-            return dataset
-        return transform(dataset, XPathDatasetFilter, xpath=expr)
-
-
-def merge(*datasets: IDataset) -> IDataset:
-    """
-    Merges several datasets using the "simple" (exact matching) algorithm:
-
-        - items are matched by (id, subset) pairs
-        - matching items share the fields available
-
-            - nothing + nothing = nothing,
-            - nothing + something = something
-            - something A + something B = conflict
-        - annotations are matched by value and shared
-        - in case of conflicts, throws an error
-
-    Returns: a wrapper around the input datasets
-    """
-
-    merger = ExactMerge()
-    infos = merger.merge_infos(d.infos() for d in datasets)
-    categories = merger.merge_categories(d.categories() for d in datasets)
-    media_type = merger.merge_media_types(datasets)
-    return DatasetItemStorageDatasetView(
-        merger.merge(*datasets), infos=infos, categories=categories, media_type=media_type
-    )
-
-
-def run_model(
-    dataset: IDataset,
-    model: Union[Launcher, Type[ModelTransform]],
-    *,
-    batch_size: int = 1,
-    append_annotation: bool = False,
-    **kwargs,
-) -> IDataset:
-    """
-    Applies a model to dataset items' media and produces a dataset with
-    media and annotations.
-
-    Args:
-        dataset: The dataset to be transformed
-        model: The model to be applied to the dataset
-        batch_size: The number of dataset items processed
-            simultaneously by the model
-        append_annotation: Whether append new annotation to existed annotations
-        **kwargs: Parameters for the model
-
-    Returns: a wrapper around the input dataset, which is computed lazily
-        during iteration
-    """
-
-    if isinstance(model, Launcher):
-        return transform(
-            dataset,
-            ModelTransform,
-            launcher=model,
-            batch_size=batch_size,
-            append_annotation=append_annotation,
-            **kwargs,
-        )
-    elif inspect.isclass(model) and issubclass(model, ModelTransform):
-        return transform(
-            dataset, model, batch_size=batch_size, append_annotation=append_annotation, **kwargs
-        )
-    else:
-        raise TypeError("Unexpected model argument type: %s" % type(model))
-
-
-@scoped
-def export(
-    dataset: IDataset,
-    path: str,
-    format: Union[str, Type[Exporter]],
-    *,
-    env: Optional[Environment] = None,
-    **kwargs,
-) -> None:
-    """
-    Saves the input dataset in some format.
-
-    Args:
-        dataset: The dataset to be saved
-        path: The output directory
-        format: The desired output format for the dataset.
-            If a string is passed, it is treated as a plugin name,
-            which is searched for in the environment set by the 'env' argument
-        env: A plugin collection. If not set, the built-in plugins are used
-        **kwargs: Parameters for the export format
-    """
-
-    if isinstance(format, str):
-        if env is None:
-            env = Environment()
-        exporter = env.exporters[format]
-    else:
-        exporter = format
-
-    if not (inspect.isclass(exporter) and issubclass(exporter, Exporter)):
-        raise TypeError("Unexpected 'format' argument type: %s" % type(exporter))
-
-    path = osp.abspath(path)
-    if not osp.exists(path):
-        on_error_do(shutil.rmtree, path, ignore_errors=True)
-    os.makedirs(path, exist_ok=True)
-
-    exporter.convert(dataset, save_dir=path, **kwargs)
-
-
-def validate(
-    dataset: IDataset, task: Union[str, TaskType], *, env: Optional[Environment] = None, **kwargs
-) -> Dict:
-    """
-    Checks dataset annotations for correctness relatively to a task type.
-
-    Args:
-        dataset: The dataset to check
-        task: Target task type - classification, detection etc.
-        env: A plugin collection. If not set, the built-in plugins are used
-        **kwargs: Parameters for the validator
-
-    Returns: a dictionary with validation results
-    """
-
-    task = parse_str_enum_value(task, TaskType).name
-
-    if env is None:
-        env = Environment()
-
-    validator: Validator = env.validators[task](**kwargs)
-    return validator.validate(dataset)
diff --git a/datumaro/components/hl_ops/__init__.py b/datumaro/components/hl_ops/__init__.py
new file mode 100644
index 000000000..eecf2598b
--- /dev/null
+++ b/datumaro/components/hl_ops/__init__.py
@@ -0,0 +1,246 @@
+# Copyright (C) 2023 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import inspect
+import os
+import os.path as osp
+import shutil
+from typing import Dict, Iterable, Optional, Type, Union
+
+from datumaro.components.dataset import Dataset, DatasetItemStorageDatasetView, IDataset
+from datumaro.components.environment import Environment
+from datumaro.components.errors import DatasetError
+from datumaro.components.exporter import Exporter
+from datumaro.components.filter import XPathAnnotationsFilter, XPathDatasetFilter
+from datumaro.components.launcher import Launcher, ModelTransform
+from datumaro.components.merge.exact_merge import ExactMerge
+from datumaro.components.transformer import Transform
+from datumaro.components.validator import TaskType, Validator
+from datumaro.util import parse_str_enum_value
+from datumaro.util.scope import on_error_do, scoped
+
+__all__ = ["HLOps"]
+
+
+class HLOps:
+    """High-level dataset operations for Python API."""
+
+    @staticmethod
+    def transform(
+        dataset: IDataset,
+        method: Union[str, Type[Transform]],
+        *,
+        env: Optional[Environment] = None,
+        **kwargs,
+    ) -> IDataset:
+        """
+        Applies some function to dataset items.
+
+        Results are computed lazily, if the transform supports this.
+
+        Args:
+            dataset: The dataset to be transformed
+            method: The transformation to be applied to the dataset.
+                If a string is passed, it is treated as a plugin name,
+                which is searched for in the environment
+                set by the 'env' argument
+            env: A plugin collection. If not set, the built-in plugins are used
+            **kwargs: Parameters for the transformation
+
+        Returns: a wrapper around the input dataset
+        """
+
+        if isinstance(method, str):
+            if env is None:
+                env = Environment()
+            method = env.transforms[method]
+
+        if not (inspect.isclass(method) and issubclass(method, Transform)):
+            raise TypeError(f"Unexpected 'method' argument type: {type(method)}")
+
+        produced = method(dataset, **kwargs)
+
+        return Dataset(source=produced, env=env)
+
+    @staticmethod
+    def filter(
+        dataset: IDataset,
+        expr: str,
+        *,  # pylint: disable=redefined-builtin
+        filter_annotations: bool = False,
+        remove_empty: bool = False,
+    ) -> IDataset:
+        """
+        Filters out some dataset items or annotations, using a custom filter
+        expression.
+
+        Args:
+            dataset: The dataset to be filtered
+            expr: XPath-formatted filter expression
+                (e.g. `/item[subset = 'train']`, `/item/annotation[label = 'cat']`)
+            filter_annotations: Indicates if the filter should be
+                applied to items or annotations
+            remove_empty: When filtering annotations, allows to
+                exclude empty items from the resulting dataset
+
+        Returns: a wrapper around the input dataset, which is computed lazily
+            during iteration
+        """
+
+        if filter_annotations:
+            return HLOps.transform(
+                dataset, XPathAnnotationsFilter, xpath=expr, remove_empty=remove_empty
+            )
+        else:
+            if not expr:
+                return dataset
+            return HLOps.transform(dataset, XPathDatasetFilter, xpath=expr)
+
+    @staticmethod
+    def merge(*datasets: IDataset) -> IDataset:
+        """
+        Merges several datasets using the "simple" (exact matching) algorithm:
+
+            - items are matched by (id, subset) pairs
+            - matching items share the fields available
+
+                - nothing + nothing = nothing,
+                - nothing + something = something
+                - something A + something B = conflict
+            - annotations are matched by value and shared
+            - in case of conflicts, throws an error
+
+        Returns: a wrapper around the input datasets
+        """
+
+        merger = ExactMerge()
+        infos = merger.merge_infos(d.infos() for d in datasets)
+        categories = merger.merge_categories(d.categories() for d in datasets)
+        media_type = merger.merge_media_types(datasets)
+        return DatasetItemStorageDatasetView(
+            merger.merge(*datasets), infos=infos, categories=categories, media_type=media_type
+        )
+
+    @staticmethod
+    def run_model(
+        dataset: IDataset,
+        model: Union[Launcher, Type[ModelTransform]],
+        *,
+        batch_size: int = 1,
+        append_annotation: bool = False,
+        **kwargs,
+    ) -> IDataset:
+        """
+        Run the model on the dataset item media entities, such as images,
+        to obtain pseudo labels and add them as dataset annotations.
+
+        Args:
+            dataset: The dataset to be transformed
+            model: The model to be applied to the dataset
+            batch_size: The number of dataset items processed
+                simultaneously by the model
+            append_annotation: Whether append new annotation to existed annotations
+            **kwargs: Parameters for the model
+
+        Returns: a wrapper around the input dataset, which is computed lazily
+            during iteration
+        """
+
+        if isinstance(model, Launcher):
+            return HLOps.transform(
+                dataset,
+                ModelTransform,
+                launcher=model,
+                batch_size=batch_size,
+                append_annotation=append_annotation,
+                **kwargs,
+            )
+        elif inspect.isclass(model) and issubclass(model, ModelTransform):
+            return HLOps.transform(
+                dataset, model, batch_size=batch_size, append_annotation=append_annotation, **kwargs
+            )
+        else:
+            raise TypeError(f"Unexpected model argument type: {type(model)}")
+
+    @staticmethod
+    @scoped
+    def export(
+        dataset: IDataset,
+        path: str,
+        format: Union[str, Type[Exporter]],
+        *,
+        env: Optional[Environment] = None,
+        **kwargs,
+    ) -> None:
+        """
+        Saves the input dataset in some format.
+
+        Args:
+            dataset: The dataset to be saved
+            path: The output directory
+            format: The desired output format for the dataset.
+                If a string is passed, it is treated as a plugin name,
+                which is searched for in the environment set by the 'env' argument
+            env: A plugin collection. If not set, the built-in plugins are used
+            **kwargs: Parameters for the export format
+        """
+
+        if isinstance(format, str):
+            if env is None:
+                env = Environment()
+            exporter = env.exporters[format]
+        else:
+            exporter = format
+
+        if not (inspect.isclass(exporter) and issubclass(exporter, Exporter)):
+            raise TypeError(f"Unexpected 'format' argument type: {type(exporter)}")
+
+        path = osp.abspath(path)
+        if not osp.exists(path):
+            on_error_do(shutil.rmtree, path, ignore_errors=True)
+        os.makedirs(path, exist_ok=True)
+
+        exporter.convert(dataset, save_dir=path, **kwargs)
+
+    @staticmethod
+    def validate(
+        dataset: IDataset,
+        task: Union[str, TaskType],
+        *,
+        env: Optional[Environment] = None,
+        **kwargs,
+    ) -> Dict:
+        """
+        Checks dataset annotations for correctness relatively to a task type.
+
+        Args:
+            dataset: The dataset to check
+            task: Target task type - classification, detection etc.
+            env: A plugin collection. If not set, the built-in plugins are used
+            **kwargs: Parameters for the validator
+
+        Returns: a dictionary with validation results
+        """
+
+        task = parse_str_enum_value(task, TaskType).name
+
+        if env is None:
+            env = Environment()
+
+        validator: Validator = env.validators[task](**kwargs)
+        return validator.validate(dataset)
+
+    @staticmethod
+    def aggregate(dataset: Dataset, from_subsets: Iterable[str], to_subset: str) -> Dataset:
+        subset_names = set(dataset.subsets().keys())
+
+        for subset in from_subsets:
+            if subset not in subset_names:
+                raise DatasetError(
+                    f"{subset} is not found in the subset names ({subset_names}) in the dataset."
+                )
+
+        return HLOps.transform(
+            dataset, "map_subsets", mapping={subset: to_subset for subset in from_subsets}
+        )
diff --git a/datumaro/components/project.py b/datumaro/components/project.py
index 98b32205c..c917bc848 100644
--- a/datumaro/components/project.py
+++ b/datumaro/components/project.py
@@ -1912,7 +1912,7 @@ class Project:
 
         try:
             obj_type, obj_hash = self._git.rev_parse(ref)
-        except Exception:  # nosec - B110:try_except_pass
+        except Exception:  # nosec try_except_pass
             pass  # Ignore git errors
         else:
             if obj_type != "commit":
diff --git a/datumaro/plugins/data_formats/cifar.py b/datumaro/plugins/data_formats/cifar.py
index e76b6cb2c..c51224a22 100644
--- a/datumaro/plugins/data_formats/cifar.py
+++ b/datumaro/plugins/data_formats/cifar.py
@@ -4,7 +4,7 @@
 
 import os
 import os.path as osp
-import pickle  # nosec - disable B403:import_pickle check - fixed
+import pickle  # nosec import_pickle
 from collections import OrderedDict
 from typing import Any, Dict, List, Optional
 
diff --git a/datumaro/plugins/data_formats/datumaro/exporter.py b/datumaro/plugins/data_formats/datumaro/exporter.py
index 07e239534..22c77cf22 100644
--- a/datumaro/plugins/data_formats/datumaro/exporter.py
+++ b/datumaro/plugins/data_formats/datumaro/exporter.py
@@ -105,7 +105,7 @@ class _SubsetWriter:
                 # Temporarily update pcd path and save it.
                 fname = context.make_pcd_filename(item)
                 context.save_point_cloud(item, fname=fname, subdir=item.subset)
-                item.media = PointCloud.from_file(path=fname, extra_images=pcd._extra_images)
+                saved_pcd = PointCloud.from_file(path=fname, extra_images=pcd._extra_images)
 
                 # Temporarily update pcd related images paths and save them.
                 for i, img in enumerate(sorted(pcd.extra_images, key=lambda v: v.path)):
@@ -114,6 +114,7 @@ class _SubsetWriter:
                     img._path = fname
 
                     if img.has_data:
+                        item.media = img
                         context.save_image(
                             item,
                             basedir=context.related_images_dir,
@@ -121,6 +122,7 @@ class _SubsetWriter:
                             fname=fname,
                         )
 
+                item.media = saved_pcd
             yield
             item.media = pcd
             if context.save_media:
diff --git a/datumaro/util/pickle_util.py b/datumaro/util/pickle_util.py
index ccf11f288..616afddea 100644
--- a/datumaro/util/pickle_util.py
+++ b/datumaro/util/pickle_util.py
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: MIT
 
-import pickle  # nosec - disable B403:import_pickle check - fixed
+import pickle  # nosec import_pickle
 
 import numpy.core.multiarray
 
diff --git a/docs/source/docs/level-up/basic_skills/03_dataset_import_export.md b/docs/source/docs/level-up/basic_skills/03_dataset_import_export.md
deleted file mode 100644
index 5ce260933..000000000
--- a/docs/source/docs/level-up/basic_skills/03_dataset_import_export.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Data Import and Export
-
-Datumaro aims to refine data
-
-``` bash
-datum create -o <project/dir>
-datum import -p <project/dir> -f image_dir <directory/path/>
-```
-
-or, if you work with Datumaro API:
-
-- for using with a project:
-
-  ```python
-  from datumaro.project import Project
-
-  project = Project.init()
-  project.import_source('source1', format='image_dir', url='directory/path/')
-  dataset = project.working_tree.make_dataset()
-  ```
-
-- for using as a dataset:
-
-  ```python
-  from datumaro import Dataset
-
-  dataset = Dataset.import_from('directory/path/', 'image_dir')
-  ```
diff --git a/docs/source/docs/level-up/basic_skills/03_dataset_import_export.rst b/docs/source/docs/level-up/basic_skills/03_dataset_import_export.rst
new file mode 100644
index 000000000..c4e67b15d
--- /dev/null
+++ b/docs/source/docs/level-up/basic_skills/03_dataset_import_export.rst
@@ -0,0 +1,90 @@
+=============
+Level 3: Data Import and Export
+=============
+
+Datumaro is a tool that supports public data formats across a wide range of tasks such as
+classification, detection, segmentation, pose estimation, or visual tracking.
+To facilitate this, Datumaro provides assistance with data import and export via both Python API and CLI.
+This makes it easier for users to work with various data formats using Datumaro.
+
+Prepare dataset
+============
+
+For the segmentation task, we here introduce the Cityscapes, which collects road scenes from 50
+different cities and contains 5K fine-grained pixel-level annotations and 20K coarse annotations.
+More detailed description is given by :ref:`here <Cityscapes>`.
+The Cityscapes dataset is available for free `download <https://www.cityscapes-dataset.com/downloads/>`_.
+
+Convert data format
+============
+
+Users sometimes needs to compare, merge, or manage various kinds of public datasets in a unified
+system. To achieve this, Datumaro not only has `import` and `export` funcionalities, but also
+provides `convert`, which shortens the import and export into a single command line.
+We now convert the Cityscapes data into the MS-COCO format, which is described in :ref:`here <COCO>`.
+
+
+.. tabbed:: CLI
+
+  Without creation of a project, we can achieve this with a single line command `convert` in Datumaro
+
+  .. code-block:: bash
+
+    datum convert -if cityscapes -i <path/to/cityscapes> -f coco_panoptic -o <path/to/output>
+
+.. tabbed:: Python
+
+  With Pthon API, we can import the data through `Dataset` as below.
+
+  .. code-block:: python
+
+      from datumaro.components.dataset import Dataset
+
+      data_path = '/path/to/cityscapes'
+      data_format = 'cityscapes'
+
+      dataset = Dataset.import_from(data_path, data_format)
+
+  We then export the import dataset as
+
+  .. code-block:: python
+
+      output_path = '/path/to/output'
+
+      dataset.export(output_path, format='coco_panoptic')
+
+.. tabbed:: ProjectCLI
+
+  With the project-based CLI, we first require to create a project by
+
+  .. code-block:: bash
+
+    datum create -o <path/to/project>
+
+  We now import Cityscapes data into the project through
+
+  .. code-block:: bash
+
+    datum import --format cityscapes -p <path/to/project> <path/to/cityscapes>
+
+  (Optional) When we import a data, the change is automatically commited in the project.
+  This can be shown through `log` as
+
+  .. code-block:: bash
+
+    datum log -p <path/to/project>
+
+  (Optional) We can check the imported dataset information such as subsets, number of data, or
+  categories through `info`.
+
+  .. code-block:: bash
+
+    datum info -p <path/to/project>
+
+  Finally, we export the data within the project with MS-COCO format as
+
+  .. code-block:: bash
+
+    datum export --format coco -p <path/to/project> -o <path/to/save> -- --save-media
+
+For a data with an unknown format, we can detect the format in the :ref:`next level <Level 4: Detect Data Format from an Unknown Dataset>`!
diff --git a/docs/source/docs/level-up/basic_skills/04_detect_data_format.md b/docs/source/docs/level-up/basic_skills/04_detect_data_format.md
deleted file mode 100644
index b6bfb43b1..000000000
--- a/docs/source/docs/level-up/basic_skills/04_detect_data_format.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Detect Data Format from an Unknown Dataset
-
-Datumaro aims to refine data
-
-``` bash
-datum create -o <project/dir>
-datum import -p <project/dir> -f image_dir <directory/path/>
-```
-
-or, if you work with Datumaro API:
-
-- for using with a project:
-
-  ```python
-  from datumaro.project import Project
-
-  project = Project.init()
-  project.import_source('source1', format='image_dir', url='directory/path/')
-  dataset = project.working_tree.make_dataset()
-  ```
-
-- for using as a dataset:
-
-  ```python
-  from datumaro import Dataset
-
-  dataset = Dataset.import_from('directory/path/', 'image_dir')
-  ```
diff --git a/docs/source/docs/level-up/basic_skills/04_detect_data_format.rst b/docs/source/docs/level-up/basic_skills/04_detect_data_format.rst
new file mode 100644
index 000000000..5a30683f0
--- /dev/null
+++ b/docs/source/docs/level-up/basic_skills/04_detect_data_format.rst
@@ -0,0 +1,41 @@
+=============
+Level 4: Detect Data Format from an Unknown Dataset
+=============
+
+Datumaro provides a function to detect the format of a dataset before importing data. This can be
+useful in cases where information about the original format of the data has been lost or is unclear.
+With this function, users can easily identify the format and proceed with appropriate data
+handling processes.
+
+Detect data format
+============
+
+.. tabbed:: CLI
+
+  .. code-block:: bash
+
+    datum detect-format <path/to/data>
+
+  The printed format can be utilized as `format` argument when importing a dataset as following the
+  :ref:`previous level <Level 3: Data Import and Export>`.
+
+.. tabbed:: Python
+
+  .. code-block:: python
+
+      from datumaro.components.environment import Environment
+
+      data_path = '/path/to/data'
+
+      env = Environment()
+
+      detected_formats = env.detect_dataset(data_path)
+
+
+  (Optional) With the detected format, we can import the dataset as below.
+
+  .. code-block:: python
+
+      from datumaro.components.dataset import Dataset
+
+      dataset = Dataset.import_from(data_path, detected_formats[0])
diff --git a/docs/source/docs/level-up/basic_skills/index.rst b/docs/source/docs/level-up/basic_skills/index.rst
index 794a8ea55..44a45c6a1 100644
--- a/docs/source/docs/level-up/basic_skills/index.rst
+++ b/docs/source/docs/level-up/basic_skills/index.rst
@@ -26,6 +26,7 @@ Basic Skills
       :text: Level 3: Dataset Import & Export
       :classes: btn-outline-primary btn-block
 
+   :badge:`ProjectCLI,badge-primary`
    :badge:`CLI,badge-info`
    :badge:`Python,badge-warning`
 
diff --git a/ipas_default.config b/ipas_default.config
index d7f185d9e..ebd61dfff 100644
--- a/ipas_default.config
+++ b/ipas_default.config
@@ -90,6 +90,13 @@ tests:
 skips:
   [ 'B101', 'B102', 'B103', 'B104', 'B105', 'B106', 'B107', 'B108', 'B110', 'B112', 'B201', 'B501', 'B502', 'B503', 'B504', 'B505', 'B506', 'B507', 'B601', 'B602', 'B603', 'B604', 'B605', 'B606', 'B607', 'B608', 'B609', 'B610', 'B611', 'B701', 'B702', 'B703']
 
+# Added to exclude some path which are not actual source code for this project
+exclude_dirs: [
+  '.tox/',
+  '.vscode/',
+  '.git/',
+]
+
 ### (optional) plugin settings - some test plugins require configuration data
 ### that may be given here, per-plugin. All bandit test plugins have a built in
 ### set of sensible defaults and these will be used if no configuration is
diff --git a/tox.ini b/tox.ini
index 4cba16d43..b107ad857 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,17 @@
 [tox]
 isolated_build = true
 skip_missing_interpreters = true
-envlist =
-    pre-commit
+
 
 [testenv:pre-commit]
 basepython = python3
 deps =
     pre-commit
 commands = pre-commit run --show-diff-on-failure --color=always --all-files
+
+
+[testenv:bandit-scan]
+deps =
+    bandit
+commands =
+    - bandit -r -c {toxinidir}/ipas_default.config {toxinidir}/ -f txt -o {toxworkdir}/bandit-report.txt


Test patch:
diff --git a/tests/unit/test_cifar_format.py b/tests/unit/test_cifar_format.py
index 1bb7ce75c..0d4354a6c 100644
--- a/tests/unit/test_cifar_format.py
+++ b/tests/unit/test_cifar_format.py
@@ -4,7 +4,7 @@
 
 import os
 import os.path as osp
-import pickle  # nosec - disable B403:import_pickle check
+import pickle  # nosec import_pickle
 import shutil
 from unittest import TestCase
 
diff --git a/tests/unit/test_coco_format.py b/tests/unit/test_coco_format.py
index 3f5b22cdc..02d95444a 100644
--- a/tests/unit/test_coco_format.py
+++ b/tests/unit/test_coco_format.py
@@ -1,6 +1,6 @@
 import os
 import os.path as osp
-import pickle  # nosec - disable B403:import_pickle check
+import pickle  # nosec import_pickle
 import shutil
 from copy import deepcopy
 from functools import partial
diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py
index 507afab84..762f56ca5 100644
--- a/tests/unit/test_dataset.py
+++ b/tests/unit/test_dataset.py
@@ -1,17 +1,15 @@
 import os
 import os.path as osp
-import pickle  # nosec - disable B403:import_pickle check
+import pickle  # nosec import_pickle
 from unittest import TestCase, mock
 
 import numpy as np
 import pytest
 
-import datumaro.components.hl_ops as hl_ops
 from datumaro.components.annotation import (
     AnnotationType,
     Bbox,
     Caption,
-    Ellipse,
     Label,
     LabelCategories,
     Mask,
@@ -2147,133 +2145,6 @@ class DatasetFilterTest(TestCase):
         compare_datasets(self, expected, filtered)
 
 
-class TestHLOps(TestCase):
-    def test_can_transform(self):
-        expected = Dataset.from_iterable(
-            [DatasetItem(0, subset="train")], categories=["cat", "dog"]
-        )
-
-        dataset = Dataset.from_iterable(
-            [DatasetItem(10, subset="train")], categories=["cat", "dog"]
-        )
-
-        actual = hl_ops.transform(dataset, "reindex", start=0)
-
-        compare_datasets(self, expected, actual)
-
-    def test_can_filter_items(self):
-        expected = Dataset.from_iterable(
-            [DatasetItem(0, subset="train")], categories=["cat", "dog"]
-        )
-
-        dataset = Dataset.from_iterable(
-            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
-            categories=["cat", "dog"],
-        )
-
-        actual = hl_ops.filter(dataset, "/item[id=0]")
-
-        compare_datasets(self, expected, actual)
-
-    def test_can_filter_annotations(self):
-        expected = Dataset.from_iterable(
-            [DatasetItem(0, subset="train", annotations=[Label(0, id=1)])],
-            categories=["cat", "dog"],
-        )
-
-        dataset = Dataset.from_iterable(
-            [
-                DatasetItem(
-                    0,
-                    subset="train",
-                    annotations=[
-                        Label(0, id=0),
-                        Label(0, id=1),
-                    ],
-                ),
-                DatasetItem(1, subset="train"),
-            ],
-            categories=["cat", "dog"],
-        )
-
-        actual = hl_ops.filter(
-            dataset, "/item/annotation[id=1]", filter_annotations=True, remove_empty=True
-        )
-
-        compare_datasets(self, expected, actual)
-
-    @mark_requirement(Requirements.DATUM_GENERAL_REQ)
-    def test_can_filter_by_annotation_types(self):
-        annotations = [
-            Label(0, id=0),
-            Bbox(0, 0, 1, 1, id=1),
-            Polygon([0, 0, 0, 1, 1, 1], id=2),
-            Ellipse(0, 0, 1, 1, id=3),
-        ]
-
-        dataset = Dataset.from_iterable(
-            [
-                DatasetItem(
-                    0,
-                    subset="train",
-                    annotations=annotations,
-                )
-            ],
-        )
-
-        types = {ann.type.name for ann in annotations}
-
-        for t in types:
-            allowed_types = types - {t}
-            cmd = " or ".join([f"type='{allowed_type}'" for allowed_type in allowed_types])
-            actual = hl_ops.filter(
-                dataset,
-                f"/item/annotation[{cmd}]",
-                filter_annotations=True,
-                remove_empty=True,
-            )
-            actual_anns = [item for item in actual][0].annotations
-            self.assertEqual(len(actual_anns), len(allowed_types))
-
-    def test_can_merge(self):
-        expected = Dataset.from_iterable(
-            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
-            categories=["cat", "dog"],
-        )
-
-        dataset_a = Dataset.from_iterable(
-            [
-                DatasetItem(0, subset="train"),
-            ],
-            categories=["cat", "dog"],
-        )
-
-        dataset_b = Dataset.from_iterable(
-            [DatasetItem(1, subset="train")], categories=["cat", "dog"]
-        )
-
-        actual = hl_ops.merge(dataset_a, dataset_b)
-
-        compare_datasets(self, expected, actual)
-
-    def test_can_export(self):
-        expected = Dataset.from_iterable(
-            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
-            categories=["cat", "dog"],
-        )
-
-        dataset = Dataset.from_iterable(
-            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
-            categories=["cat", "dog"],
-        )
-
-        with TestDir() as test_dir:
-            hl_ops.export(dataset, test_dir, "datumaro")
-            actual = Dataset.load(test_dir)
-
-            compare_datasets(self, expected, actual)
-
-
 @pytest.fixture
 def fxt_test_case():
     return TestCase()
diff --git a/tests/unit/test_hl_ops.py b/tests/unit/test_hl_ops.py
new file mode 100644
index 000000000..f13b802b6
--- /dev/null
+++ b/tests/unit/test_hl_ops.py
@@ -0,0 +1,168 @@
+# Copyright (C) 2023 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import pytest
+
+from datumaro import Dataset, DatasetItem, HLOps
+from datumaro.components.annotation import Bbox, Ellipse, Label, Polygon
+
+from tests.requirements import Requirements, mark_requirement
+from tests.utils.test_utils import TestCaseHelper, TestDir
+from tests.utils.test_utils import compare_datasets as _compare_datasets
+
+
+def compare_datasets(_, expected, actual):
+    _compare_datasets(TestCaseHelper(), expected, actual)
+
+
+class HLOpsTest:
+    def test_can_transform(self):
+        expected = Dataset.from_iterable(
+            [DatasetItem(0, subset="train")], categories=["cat", "dog"]
+        )
+
+        dataset = Dataset.from_iterable(
+            [DatasetItem(10, subset="train")], categories=["cat", "dog"]
+        )
+
+        actual = HLOps.transform(dataset, "reindex", start=0)
+
+        compare_datasets(self, expected, actual)
+
+    def test_can_filter_items(self):
+        expected = Dataset.from_iterable(
+            [DatasetItem(0, subset="train")], categories=["cat", "dog"]
+        )
+
+        dataset = Dataset.from_iterable(
+            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
+            categories=["cat", "dog"],
+        )
+
+        actual = HLOps.filter(dataset, "/item[id=0]")
+
+        compare_datasets(self, expected, actual)
+
+    def test_can_filter_annotations(self):
+        expected = Dataset.from_iterable(
+            [DatasetItem(0, subset="train", annotations=[Label(0, id=1)])],
+            categories=["cat", "dog"],
+        )
+
+        dataset = Dataset.from_iterable(
+            [
+                DatasetItem(
+                    0,
+                    subset="train",
+                    annotations=[
+                        Label(0, id=0),
+                        Label(0, id=1),
+                    ],
+                ),
+                DatasetItem(1, subset="train"),
+            ],
+            categories=["cat", "dog"],
+        )
+
+        actual = HLOps.filter(
+            dataset, "/item/annotation[id=1]", filter_annotations=True, remove_empty=True
+        )
+
+        compare_datasets(self, expected, actual)
+
+    @mark_requirement(Requirements.DATUM_GENERAL_REQ)
+    def test_can_filter_by_annotation_types(self):
+        annotations = [
+            Label(0, id=0),
+            Bbox(0, 0, 1, 1, id=1),
+            Polygon([0, 0, 0, 1, 1, 1], id=2),
+            Ellipse(0, 0, 1, 1, id=3),
+        ]
+
+        dataset = Dataset.from_iterable(
+            [
+                DatasetItem(
+                    0,
+                    subset="train",
+                    annotations=annotations,
+                )
+            ],
+        )
+
+        types = {ann.type.name for ann in annotations}
+
+        for t in types:
+            allowed_types = types - {t}
+            cmd = " or ".join([f"type='{allowed_type}'" for allowed_type in allowed_types])
+            actual = HLOps.filter(
+                dataset,
+                f"/item/annotation[{cmd}]",
+                filter_annotations=True,
+                remove_empty=True,
+            )
+            actual_anns = [item for item in actual][0].annotations
+            assert len(actual_anns) == len(allowed_types)
+
+    def test_can_merge(self):
+        expected = Dataset.from_iterable(
+            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
+            categories=["cat", "dog"],
+        )
+
+        dataset_a = Dataset.from_iterable(
+            [
+                DatasetItem(0, subset="train"),
+            ],
+            categories=["cat", "dog"],
+        )
+
+        dataset_b = Dataset.from_iterable(
+            [DatasetItem(1, subset="train")], categories=["cat", "dog"]
+        )
+
+        actual = HLOps.merge(dataset_a, dataset_b)
+
+        compare_datasets(self, expected, actual)
+
+    def test_can_export(self):
+        expected = Dataset.from_iterable(
+            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
+            categories=["cat", "dog"],
+        )
+
+        dataset = Dataset.from_iterable(
+            [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")],
+            categories=["cat", "dog"],
+        )
+
+        with TestDir() as test_dir:
+            HLOps.export(dataset, test_dir, "datumaro")
+            actual = Dataset.load(test_dir)
+
+            compare_datasets(self, expected, actual)
+
+    def test_aggregate(self):
+        expected = Dataset.from_iterable(
+            [
+                DatasetItem(0, subset="default"),
+                DatasetItem(1, subset="default"),
+                DatasetItem(2, subset="default"),
+            ],
+            categories=["cat", "dog"],
+        )
+
+        dataset = Dataset.from_iterable(
+            [
+                DatasetItem(0, subset="train"),
+                DatasetItem(1, subset="val"),
+                DatasetItem(2, subset="test"),
+            ],
+            categories=["cat", "dog"],
+        )
+
+        actual = HLOps.aggregate(
+            dataset, from_subsets=["train", "val", "test"], to_subset="default"
+        )
+
+        compare_datasets(self, expected, actual)
diff --git a/tests/unit/test_imagenet_format.py b/tests/unit/test_imagenet_format.py
index 91c6394d2..5e88fa75d 100644
--- a/tests/unit/test_imagenet_format.py
+++ b/tests/unit/test_imagenet_format.py
@@ -1,4 +1,4 @@
-import pickle  # nosec - disable B403:import_pickle check
+import pickle  # nosec import_pickle
 from unittest import TestCase
 
 import numpy as np
diff --git a/tests/unit/test_voc_format.py b/tests/unit/test_voc_format.py
index 3ba2caa90..78c2829dc 100644
--- a/tests/unit/test_voc_format.py
+++ b/tests/unit/test_voc_format.py
@@ -1,6 +1,6 @@
 import os
 import os.path as osp
-import pickle  # nosec - disable B403:import_pickle check
+import pickle  # nosec import_pickle
 from collections import OrderedDict
 from functools import partial
 from unittest import TestCase
diff --git a/tests/unit/test_yolo_format.py b/tests/unit/test_yolo_format.py
index 9b9bfbc9a..a73cf3e92 100644
--- a/tests/unit/test_yolo_format.py
+++ b/tests/unit/test_yolo_format.py
@@ -1,6 +1,6 @@
 import os
 import os.path as osp
-import pickle  # nosec - disable B403:import_pickle check
+import pickle  # nosec import_pickle
 from unittest import TestCase
 
 import numpy as np