diff --git a/.github/workflows/code_scan.yml b/.github/workflows/code_scan.yml index 2bc01036cd..fde7def50a 100644 --- a/.github/workflows/code_scan.yml +++ b/.github/workflows/code_scan.yml @@ -31,14 +31,14 @@ jobs: mkdir -p .ci/base/docs pip-compile -o .ci/base/docs/requirements.txt docs/requirements.txt - name: Run Trivy Scan (full, csv) - uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0 + uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # 0.29.0 with: trivy-config: ".ci/trivy-csv.yaml" scan-type: 'fs' scan-ref: ".ci/" scanners: vuln,secret - name: Run Trivy Scan (prod, spdx.json) - uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0 + uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # 0.29.0 with: trivy-config: ".ci/trivy-json.yaml" scan-type: 'fs' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 93ef850ddc..c6a290e0da 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -52,7 +52,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -73,7 +73,7 @@ jobs: python -m build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: category: "/language:${{matrix.language}}" - name: Generate Security Report diff --git a/.github/workflows/issue_assignment.yml b/.github/workflows/issue_assignment.yml index 8cba4d3f45..16caac999b 100644 --- a/.github/workflows/issue_assignment.yml +++ b/.github/workflows/issue_assignment.yml @@ -14,9 +14,9 @@ jobs: steps: - name: Auto-assign Issue - uses: pozil/auto-assign-issue@v2.0.0 + uses: pozil/auto-assign-issue@v2.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - assignees: vinnamkim,jihyeonyi,sooahleex,itrushkin + assignees: jihyeonyi,sooahleex,itrushkin numOfAssignee: 1 allowSelfAssign: false diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 3870725d6e..1b35dc3af6 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -62,6 +62,6 @@ jobs: run: | tox -vvv -e tests-py${{ matrix.tox-env-py }}-${{ matrix.tox-env-os }} -- tests/integration - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: ${{ matrix.os }}_Python-${{ matrix.python-version }} diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml index 061b8f9f25..b4653f07ac 100644 --- a/.github/workflows/publish_to_pypi.yml +++ b/.github/workflows/publish_to_pypi.yml @@ -80,12 +80,12 @@ jobs: file_glob: true - name: Publish package distributions to PyPI if: ${{ steps.check-tag.outputs.match != '' }} - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.12.2 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Publish package distributions to TestPyPI if: ${{ steps.check-tag.outputs.match == '' }} - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.12.2 with: password: ${{ secrets.TESTPYPI_API_TOKEN }} repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8f9d09195f..693bff2bbc 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: sarif_file: results.sarif diff --git a/3rd-party.txt b/3rd-party.txt index 0401a59ee0..85d2f2edf2 100644 --- a/3rd-party.txt +++ b/3rd-party.txt @@ -7518,5 +7518,22 @@ Apache-2.0 See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------- +portalocker + +BSD-3-Clause + +Copyright 2022 Rick van Hattem + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------- * Other names and brands may be claimed as the property of others. diff --git a/CHANGELOG.md b/CHANGELOG.md index c246c0f18c..22af027194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,79 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## \[unreleased\] +## \[Unreleased\] + +### New features +- Convert Cuboid2D annotation to/from 3D data + () +- Add label groups for hierarchical classification in ImageNet + () + +### Enhancements +- Enhance 'id_from_image_name' transform to ensure each identifier is unique + () +- Optimize path assignment to handle point cloud in JSON without images + () +- Add documentation for framework conversion + () + +### Bug fixes +- Fix assertion to compare hashkeys against expected value + () + +## Q4 2024 Release 1.10.0 + +### New features +- Support KITTI 3D format + (, ) +- Add PseudoLabeling transform for unlabeled dataset + () + +### Enhancements +- Raise an appropriate error when exporting a datumaro dataset if its subset name contains path separators. + () +- Update docs for transform plugins + () +- Update ov ir model for explorer openvino launcher with CLIP ViT-L/14@336px model + () +- Optimize path assignment to handle point cloud in JSON without images + () +- Set TabularTransform to process clean transform in parallel + () + +### Bug fixes +- Fix datumaro format to load visibility information from Points annotations + () + +## Q4 2024 Release 1.9.1 +### Enhancements +- Support multiple labels for kaggle format + () +- Use DataFrame.map instead of DataFrame.applymap + () + +### Bug fixes +- Fix StreamDataset merging when importing in eager mode + () + +## Q3 2024 Release 1.9.0 ### New features - Add a new CLI command: datum format () +- Add a new Cuboid2D annotation type + () +- Support language dataset for DmTorchDataset + () ### Enhancements - Change _Shape to Shape and add comments for subclasses of Shape () +- Fix `kitti_raw` importer and exporter for dimensions (height, width, length) in meters + () ### Bug fixes +- Fix KITTI-3D importer and exporter + () ## Q3 2024 Release 1.8.0 ### New features diff --git a/README.md b/README.md index fd63a4f743..067d007604 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build status](https://github.com/openvinotoolkit/datumaro/actions/workflows/health_check.yml/badge.svg)](https://github.com/openvinotoolkit/datumaro/actions/workflows/health_check.yml) [![codecov](https://codecov.io/gh/openvinotoolkit/datumaro/branch/develop/graph/badge.svg?token=FG25VU096Q)](https://codecov.io/gh/openvinotoolkit/datumaro) +[![Downloads](https://static.pepy.tech/badge/datumaro)](https://pepy.tech/project/datumaro) A framework and CLI tool to build, transform, and analyze datasets. diff --git a/docker/segment-anything/requirements.txt b/docker/segment-anything/requirements.txt index 8d1056bd21..99e2828731 100644 --- a/docker/segment-anything/requirements.txt +++ b/docker/segment-anything/requirements.txt @@ -371,38 +371,33 @@ numpy==1.26.4 \ # onnxruntime # opencv-python # pycocotools -onnx==1.16.0 \ - --hash=sha256:034ae21a2aaa2e9c14119a840d2926d213c27aad29e5e3edaa30145a745048e1 \ - --hash=sha256:03a627488b1a9975d95d6a55582af3e14c7f3bb87444725b999935ddd271d352 \ - --hash=sha256:0e60ca76ac24b65c25860d0f2d2cdd96d6320d062a01dd8ce87c5743603789b8 \ - --hash=sha256:0efeb46985de08f0efe758cb54ad3457e821a05c2eaf5ba2ccb8cd1602c08084 \ - --hash=sha256:209fe84995a28038e29ae8369edd35f33e0ef1ebc3bddbf6584629823469deb1 \ - --hash=sha256:237c6987c6c59d9f44b6136f5819af79574f8d96a760a1fa843bede11f3822f7 \ - --hash=sha256:257858cbcb2055284f09fa2ae2b1cfd64f5850367da388d6e7e7b05920a40c90 \ - --hash=sha256:298f28a2b5ac09145fa958513d3d1e6b349ccf86a877dbdcccad57713fe360b3 \ - --hash=sha256:30f02beaf081c7d9fa3a8c566a912fc4408e28fc33b1452d58f890851691d364 \ - --hash=sha256:3e0860fea94efde777e81a6f68f65761ed5e5f3adea2e050d7fbe373a9ae05b3 \ - --hash=sha256:5202559070afec5144332db216c20f2fff8323cf7f6512b0ca11b215eacc5bf3 \ - --hash=sha256:62a2e27ae8ba5fc9b4a2620301446a517b5ffaaf8566611de7a7c2160f5bcf4c \ - --hash=sha256:66300197b52beca08bc6262d43c103289c5d45fde43fb51922ed1eb83658cf0c \ - --hash=sha256:70a90649318f3470985439ea078277c9fb2a2e6e2fd7c8f3f2b279402ad6c7e6 \ - --hash=sha256:71839546b7f93be4fa807995b182ab4b4414c9dbf049fee11eaaced16fcf8df2 \ - --hash=sha256:7449241e70b847b9c3eb8dae622df8c1b456d11032a9d7e26e0ee8a698d5bf86 \ - --hash=sha256:7532343dc5b8b5e7c3e3efa441a3100552f7600155c4db9120acd7574f64ffbf \ - --hash=sha256:7665217c45a61eb44718c8e9349d2ad004efa0cb9fbc4be5c6d5e18b9fe12b52 \ - --hash=sha256:7755cbd5f4e47952e37276ea5978a46fc8346684392315902b5ed4a719d87d06 \ - --hash=sha256:77579e7c15b4df39d29465b216639a5f9b74026bdd9e4b6306cd19a32dcfe67c \ - --hash=sha256:7fb29a9a692b522deef1f6b8f2145da62c0c43ea1ed5b4c0f66f827fdc28847d \ - --hash=sha256:81b4ee01bc554e8a2b11ac6439882508a5377a1c6b452acd69a1eebb83571117 \ - --hash=sha256:8cf3e518b1b1b960be542e7c62bed4e5219e04c85d540817b7027029537dec92 \ - --hash=sha256:9eadbdce25b19d6216f426d6d99b8bc877a65ed92cbef9707751c6669190ba4f \ - --hash=sha256:ae0029f5e47bf70a1a62e7f88c80bca4ef39b844a89910039184221775df5e43 \ - --hash=sha256:c392faeabd9283ee344ccb4b067d1fea9dfc614fa1f0de7c47589efd79e15e78 \ - --hash=sha256:d7886c05aa6d583ec42f6287678923c1e343afc4350e49d5b36a0023772ffa22 \ - --hash=sha256:ddf14a3d32234f23e44abb73a755cb96a423fac7f004e8f046f36b10214151ee \ - --hash=sha256:e5752bbbd5717304a7643643dba383a2fb31e8eb0682f4e7b7d141206328a73b \ - --hash=sha256:ec22a43d74eb1f2303373e2fbe7fbcaa45fb225f4eb146edfed1356ada7a9aea \ - --hash=sha256:f51179d4af3372b4f3800c558d204b592c61e4b4a18b8f61e0eea7f46211221a +onnx==1.17.0 \ + --hash=sha256:0141c2ce806c474b667b7e4499164227ef594584da432fd5613ec17c1855e311 \ + --hash=sha256:081ec43a8b950171767d99075b6b92553901fa429d4bc5eb3ad66b36ef5dbe3a \ + --hash=sha256:0e906e6a83437de05f8139ea7eaf366bf287f44ae5cc44b2850a30e296421f2f \ + --hash=sha256:23b8d56a9df492cdba0eb07b60beea027d32ff5e4e5fe271804eda635bed384f \ + --hash=sha256:317870fca3349d19325a4b7d1b5628f6de3811e9710b1e3665c68b073d0e68d7 \ + --hash=sha256:3193a3672fc60f1a18c0f4c93ac81b761bc72fd8a6c2035fa79ff5969f07713e \ + --hash=sha256:38b5df0eb22012198cdcee527cc5f917f09cce1f88a69248aaca22bd78a7f023 \ + --hash=sha256:3d955ba2939878a520a97614bcf2e79c1df71b29203e8ced478fa78c9a9c63c2 \ + --hash=sha256:3e19fd064b297f7773b4c1150f9ce6213e6d7d041d7a9201c0d348041009cdcd \ + --hash=sha256:48ca1a91ff73c1d5e3ea2eef20ae5d0e709bb8a2355ed798ffc2169753013fd3 \ + --hash=sha256:4a183c6178be001bf398260e5ac2c927dc43e7746e8638d6c05c20e321f8c949 \ + --hash=sha256:4f3fb5cc4e2898ac5312a7dc03a65133dd2abf9a5e520e69afb880a7251ec97a \ + --hash=sha256:5ca7a0894a86d028d509cdcf99ed1864e19bfe5727b44322c11691d834a1c546 \ + --hash=sha256:659b8232d627a5460d74fd3c96947ae83db6d03f035ac633e20cd69cfa029227 \ + --hash=sha256:67e1c59034d89fff43b5301b6178222e54156eadd6ab4cd78ddc34b2f6274a66 \ + --hash=sha256:76884fe3e0258c911c749d7d09667fb173365fd27ee66fcedaf9fa039210fd13 \ + --hash=sha256:8167295f576055158a966161f8ef327cb491c06ede96cc23392be6022071b6ed \ + --hash=sha256:95c03e38671785036bb704c30cd2e150825f6ab4763df3a4f1d249da48525957 \ + --hash=sha256:d545335cb49d4d8c47cc803d3a805deb7ad5d9094dc67657d66e568610a36d7d \ + --hash=sha256:d6fc3a03fc0129b8b6ac03f03bc894431ffd77c7d79ec023d0afd667b4d35869 \ + --hash=sha256:dfd777d95c158437fda6b34758f0877d15b89cbe9ff45affbedc519b35345cf9 \ + --hash=sha256:e4673276b558b5b572b960b7f9ef9214dce9305673683eb289bb97a7df379a4b \ + --hash=sha256:ea5023a8dcdadbb23fd0ed0179ce64c1f6b05f5b5c34f2909b4e927589ebd0e4 \ + --hash=sha256:ecf2b617fd9a39b831abea2df795e17bac705992a35a98e1f0363f005c4a5247 \ + --hash=sha256:f01a4b63d4e1d8ec3e2f069e7b798b2955810aa434f7361f01bc8ca08d69cce4 \ + --hash=sha256:f0e437f8f2f0c36f629e9743d28cf266312baa90be6a899f405f78f2d4cb2e1d # via segment_anything (./segment-anything/setup.py) onnxruntime==1.17.1 \ --hash=sha256:2dff1a24354220ac30e4a4ce2fb1df38cb1ea59f7dac2c116238d63fe7f4c5ff \ diff --git a/docs/requirements.txt b/docs/requirements.txt index 8607e58ff3..7f346f353b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,9 +5,9 @@ opencv-python-headless==4.10.0.84 # docs -markupsafe==2.1.5 +markupsafe==3.0.2 nbconvert>=7.2.3 -ipython==8.26.0 +ipython==8.29.0 sphinx==7.2.6 pydata-sphinx-theme==0.15.2 sphinx-copybutton diff --git a/docs/source/docs/command-reference/context_free/prune.md b/docs/source/docs/command-reference/context_free/prune.md index 0142c0f597..18c30b21e5 100644 --- a/docs/source/docs/command-reference/context_free/prune.md +++ b/docs/source/docs/command-reference/context_free/prune.md @@ -13,7 +13,7 @@ Prune supports various methodology. By default, datasets are updated in-place. The `-o/--output-dir` option can be used to specify another output directory. When updating in-place, use the `--overwirte` parameter (in-place updates fail by default to prevent data loss), unless a project target is modified. -The current project (`-p/--project`) is also used as a context for plugins, so it can be useful for datasest paths having custom formats. When not specified, the current project's working tree is used. +The current project (`-p/--project`) is also used as a context for plugins, so it can be useful for dataset paths having custom formats. When not specified, the current project's working tree is used. The command can be applied to a dataset or a project build target, a stage or the combined `project` target, in which case all the project targets will be affected. diff --git a/docs/source/docs/command-reference/context_free/transform.md b/docs/source/docs/command-reference/context_free/transform.md index c33bdc8359..c79cc94a99 100644 --- a/docs/source/docs/command-reference/context_free/transform.md +++ b/docs/source/docs/command-reference/context_free/transform.md @@ -101,7 +101,10 @@ Basic dataset item manipulations: - [`remove_images`](#remove_images) - Removes specific images - [`remove_annotations`](#remove_annotations) - Removes annotations - [`remove_attributes`](#remove_attributes) - Removes attributes -- [`astype_annotations`](#astype_annotations) - Convert annotation type +- [`astype_annotations`](#astype_annotations) - Transforms annotation types +- [`pseudo_labeling`](#pseudo_labeling) - Generates pseudo labels for unlabeled data +- [`correct`](#correct) - Corrects annotation types +- [`clean`](#clean) - Removes noisy data for tabular dataset Subset manipulations: - [`random_split`](#random_split) - Splits dataset into subsets @@ -173,15 +176,36 @@ Examples: #### `id_from_image_name` -Renames items in the dataset using image file name (without extension). +Renames items in the dataset based on the image file name, excluding the extension. +When 'ensure_unique' is enabled, a random suffix is appended to ensure each identifier is unique +in cases where the image name is not distinct. By default, the random suffix is three characters long, +but this can be adjusted with the 'suffix_length' parameter. Usage: ```console -id_from_image_name [-h] +id_from_image_name [-h] [-u] [-l SUFFIX_LENGTH] ``` Optional arguments: -- `-h`, `--help` (flag) - Show this help message and exit +- `-h`, `--help` (flag) - show this help message and exit +- `-u`, `--ensure_unique` (flag) - Appends a random suffix to ensure each identifier is unique if the image name is duplicated +- `-l`, `--suffix_length` (int) - Alters the length of the random suffix if the `ensure_unique` is enabled(default: 3) + +Examples: +- Renames items without duplication check + ```console + datum transform -t id_from_image_name + ``` + +- Renames items with duplication check + ```console + datum transform -t id_from_image_name -- --ensure_unique + ``` + +- Renames items with duplication check and alters the suffix length(default: 3) + ```console + datum transform -t id_from_image_name -- --ensure_unique --suffix_length 2 + ``` #### `reindex` @@ -826,6 +850,35 @@ bbox_values_decrement [-h] Optional arguments: - `-h`, `--help` (flag) - Show this help message and exit +#### `pseudo_labeling` + +Assigns pseudo-labels to items in a dataset based on their similarity to predefined labels. This class is useful for semi-supervised learning when dealing with missing or uncertain labels. + +The process includes: + +- Similarity Computation: Uses hashing techniques to compute the similarity between items and predefined labels. +- Pseudo-Label Assignment: Assigns the most similar label as a pseudo-label to each item. + +Attributes: + +- `extractor` (IDataset) - Provides access to dataset items and their annotations. +- `labels` (Optional[List[str]]) - List of predefined labels for pseudo-labeling. Defaults to all available labels if not provided. +- `explorer` (Optional[Explorer]) - Computes hash keys for items and labels. If not provided, a new Explorer is created. + +Usage: +```console +pseudo_labeling [-h] [--labels LABELS] + +Optional arguments: +- `-h`, `--help` (flag) - Show this help message and exit +- `--labels` (str) - Comma-separated list of label names for pseudo-labeling + +Examples: +- Assign pseudo-labels based on predefined labels + ```console + datum transform -t pseudo_labeling -- --labels 'label1,label2' + ``` + #### `correct` Correct the dataset from a validation report @@ -838,3 +891,27 @@ correct [-h] [-r REPORT_PATH] Optional arguments: - `-h`, `--help` (flag) - Show this help message and exit - `-r`, `--reports` (str) - A validation report from a 'validate' CLI (default=validation_reports.json) + +#### `clean` + +Refines and preprocesses media items in a dataset, focusing on string, numeric, and categorical data. This transform is designed to clean and improve the quality of the data, making it more suitable for analysis and modeling. + +The cleaning process includes: + +- String Data: Removes unnecessary characters using NLP techniques. +- Numeric Data: Identifies and handles outliers and missing values. +- Categorical Data: Cleans and refines categorical information. + +Usage: +```console +clean [-h] +``` + +Optional arguments: +- `-h`, `--help` (flag) - Show this help message and exit + +Examples: +- Clean and preprocess dataset items + ```console + datum transform -t clean + ``` diff --git a/docs/source/docs/command-reference/context_free/validate.md b/docs/source/docs/command-reference/context_free/validate.md index 52284876d1..cdf52856f5 100644 --- a/docs/source/docs/command-reference/context_free/validate.md +++ b/docs/source/docs/command-reference/context_free/validate.md @@ -58,7 +58,7 @@ Examples: datum validate -p -t classification -- -ir 40 ``` -### List of validation items (annomaly types) +### List of validation items (anomaly types) | Anomaly Type | Description | Task Type | | ------------ | ----------- | --------- | diff --git a/docs/source/docs/command-reference/helper/format.md b/docs/source/docs/command-reference/helper/format.md index 20f6f95f71..f9617f1c57 100644 --- a/docs/source/docs/command-reference/helper/format.md +++ b/docs/source/docs/command-reference/helper/format.md @@ -13,7 +13,7 @@ usage: datum format [-h] [-li | -le] [-d DELIMITER] Parameters: - `-h, --help` - Print the help message and exit. -- `-d DELIMITER, --delimiter DELIMITER` - Seperator used to list data format names (default: `\n`). For example, `datum format -d ','` command displays +- `-d DELIMITER, --delimiter DELIMITER` - Separator used to list data format names (default: `\n`). For example, `datum format -d ','` command displays ```console Supported import formats: ade20k2017,ade20k2020,align_celeba,... diff --git a/docs/source/docs/data-formats/formats/ava_action.md b/docs/source/docs/data-formats/formats/ava_action.md index 792cee9af4..3d8e2ac93b 100644 --- a/docs/source/docs/data-formats/formats/ava_action.md +++ b/docs/source/docs/data-formats/formats/ava_action.md @@ -7,7 +7,7 @@ The AVA action format specification is available The dataset has annotations for recognizing an action per instance from video frames like visual tracking task. Specifically, the AVA action dataset contains frame indices, -bounding box cooridnates, actions, and tracking ids in the annotation file. The action +bounding box coordinates, actions, and tracking ids in the annotation file. The action categories are described in `ava_action_list_v2.2.pbtxt`. For the ease use for object detection, the AVA action dataset provides the bounding box proposals from `Faster R-CNN`. diff --git a/docs/source/docs/data-formats/formats/cityscapes.md b/docs/source/docs/data-formats/formats/cityscapes.md index 0f5ac2b58b..9c11a3aa0e 100644 --- a/docs/source/docs/data-formats/formats/cityscapes.md +++ b/docs/source/docs/data-formats/formats/cityscapes.md @@ -137,7 +137,7 @@ Extra options for exporting to Cityscapes format: #... datum project export -f cityscapes -- --label-map mycolormap.txt ``` -or you can use original cityscapes colomap: +or you can use original cityscapes colormap: ``` bash datum project export -f cityscapes -- --label-map cityscapes ``` diff --git a/docs/source/docs/data-formats/formats/coco.md b/docs/source/docs/data-formats/formats/coco.md index a6ed039f70..c007cfc552 100644 --- a/docs/source/docs/data-formats/formats/coco.md +++ b/docs/source/docs/data-formats/formats/coco.md @@ -122,7 +122,7 @@ For the panoptic task, a dataset directory should have the following structure: Annotation files must have the names like `_.json`. The year is treated as a part of the subset name. -If the annotation file name does't match this pattern, use one of the +If the annotation file name doesn't match this pattern, use one of the task-specific formats instead of plain `coco`: `coco_captions`, `coco_image_info`, `coco_instances`, `coco_labels`, `coco_panoptic`, `coco_person_keypoints`, `coco_stuff`. In this case all items of the diff --git a/docs/source/docs/data-formats/formats/datumaro.md b/docs/source/docs/data-formats/formats/datumaro.md index b12f1af6a1..0e9f1abfe8 100644 --- a/docs/source/docs/data-formats/formats/datumaro.md +++ b/docs/source/docs/data-formats/formats/datumaro.md @@ -73,6 +73,8 @@ A Datumaro dataset directory should have the following structure: └── ... ``` +Note that the subset name shouldn't contain path separators. + If your dataset is not following the above directory structure, it cannot detect and import your dataset as the Datumaro format properly. diff --git a/docs/source/docs/data-formats/formats/datumaro_binary.md b/docs/source/docs/data-formats/formats/datumaro_binary.md index 7b724b3734..a970d135a5 100644 --- a/docs/source/docs/data-formats/formats/datumaro_binary.md +++ b/docs/source/docs/data-formats/formats/datumaro_binary.md @@ -113,6 +113,8 @@ A DatumaroBinary dataset directory should have the following structure: └── ... ``` +Note that the subset name shouldn't contain path separators. + If your dataset is not following the above directory structure, it cannot detect and import your dataset as the DatumaroBinary format properly. diff --git a/docs/source/docs/data-formats/formats/kaggle.md b/docs/source/docs/data-formats/formats/kaggle.md index e7d25b38b8..dd517934cc 100644 --- a/docs/source/docs/data-formats/formats/kaggle.md +++ b/docs/source/docs/data-formats/formats/kaggle.md @@ -46,7 +46,7 @@ At this time, it's essential to specify the column names for media and label suc ## Import Kaggle Image Txt dataset -Another `kaggle_image_txt` format replaces only `columns` with an order of informations in `.txt`. +Another `kaggle_image_txt` format replaces only `columns` with an order of information in `.txt`. For instance, dataset can be created by ```python diff --git a/docs/source/docs/data-formats/formats/kitti.md b/docs/source/docs/data-formats/formats/kitti.md index 5c5b2fbc63..12896b05ba 100644 --- a/docs/source/docs/data-formats/formats/kitti.md +++ b/docs/source/docs/data-formats/formats/kitti.md @@ -175,7 +175,7 @@ Extra options for exporting to KITTI format: datum project export -f kitti -- --label-map mycolormap.txt ``` -or you can use original kitti colomap: +or you can use original kitti colormap: ``` bash datum project export -f kitti -- --label-map kitti ``` diff --git a/docs/source/docs/data-formats/formats/mapillary_vistas.md b/docs/source/docs/data-formats/formats/mapillary_vistas.md index 95e4990c01..9bb9f58b5b 100644 --- a/docs/source/docs/data-formats/formats/mapillary_vistas.md +++ b/docs/source/docs/data-formats/formats/mapillary_vistas.md @@ -12,7 +12,7 @@ Supported annotation types: - `Mask` (class, instances, panoptic) - `Polygon` -Supported atttibutes: +Supported attributes: - `is_crowd`(boolean; on panoptic `mask`): Indicates that the annotation covers multiple instances of the same class. diff --git a/docs/source/docs/data-formats/formats/mot.md b/docs/source/docs/data-formats/formats/mot.md index 040055f51a..a03ffc7479 100644 --- a/docs/source/docs/data-formats/formats/mot.md +++ b/docs/source/docs/data-formats/formats/mot.md @@ -18,7 +18,7 @@ Supported annotation attributes: You can download the MOT challenge dataset [here](https://motchallenge.net). -A Datumaro project with the MOT challange source can be created in the following way: +A Datumaro project with the MOT challenge source can be created in the following way: ``` bash datum project create @@ -43,7 +43,7 @@ The MOT challenge dataset directory should have the following structure: └── seqinfo.ini (optional) ``` -`seqinfo.ini` is provided by the MOT challange dataset but it is optional in Datumaro. +`seqinfo.ini` is provided by the MOT challenge dataset but it is optional in Datumaro. It includes `imdir` field which is the name of directory having image files. If this file is given, Datumaro will find the image files from the directory written in the `imdir` field. @@ -52,7 +52,7 @@ run `datum project info`, which will display the project information. ## Export to other formats -Datumaro can convert the MOT challange dataset into any other format [Datumaro supports](/docs/data-formats/formats/index.rst). +Datumaro can convert the MOT challenge dataset into any other format [Datumaro supports](/docs/data-formats/formats/index.rst). Such conversion will only be successful if the output format can represent the type of dataset you want to convert, diff --git a/docs/source/docs/data-formats/formats/mots.md b/docs/source/docs/data-formats/formats/mots.md index 782bab9544..e382d82843 100644 --- a/docs/source/docs/data-formats/formats/mots.md +++ b/docs/source/docs/data-formats/formats/mots.md @@ -16,9 +16,9 @@ Supported annotation attributes: ## Import MOTS dataset -You can download the PNG format of MOTS challange dataset [here](https://www.vision.rwth-aachen.de/page/mots). +You can download the PNG format of MOTS challenge dataset [here](https://www.vision.rwth-aachen.de/page/mots). -A Datumaro project with the MOTS challange source can be created in the following way: +A Datumaro project with the MOTS challenge source can be created in the following way: ``` bash datum project create @@ -28,7 +28,7 @@ datum project import --format mots It is possible to specify project name and project directory. Run `datum project create --help` for more information. -The MOTS challange dataset directory should have the following structure: +The MOTS challenge dataset directory should have the following structure: ``` @@ -55,7 +55,7 @@ run `datum project info`, which will display the project information. ## Export to other formats -Datumaro can convert the MOTS challange dataset into any other format [Datumaro supports](/docs/data-formats/formats/index.rst). +Datumaro can convert the MOTS challenge dataset into any other format [Datumaro supports](/docs/data-formats/formats/index.rst). Such conversion will only be successful if the output format can represent the type of dataset you want to convert, diff --git a/docs/source/docs/data-formats/formats/pascal_voc.md b/docs/source/docs/data-formats/formats/pascal_voc.md index ee70abb2ad..f6f457d209 100644 --- a/docs/source/docs/data-formats/formats/pascal_voc.md +++ b/docs/source/docs/data-formats/formats/pascal_voc.md @@ -221,7 +221,7 @@ datum project export -f voc -- --tasks detection,classification # person:255,0,0:head: datum project export -f voc_segmentation -- --label-map mycolormap.txt ``` -or you can use original voc colomap: +or you can use original voc colormap: ``` bash datum project export -f voc_segmentation -- --label-map voc ``` diff --git a/docs/source/docs/data-formats/formats/yolo_ultralytics.md b/docs/source/docs/data-formats/formats/yolo_ultralytics.md index a550499bb6..ca1b82fd48 100644 --- a/docs/source/docs/data-formats/formats/yolo_ultralytics.md +++ b/docs/source/docs/data-formats/formats/yolo_ultralytics.md @@ -93,7 +93,7 @@ To add custom classes, you can use [`dataset_meta.json`](/docs/data-formats/form ## Export to YOLO-Ultralytics format Datumaro can convert [any other image dataset format](/docs/data-formats/formats/index.rst) which has bounding box annotations into YOLO-Ultralytics format. -After the successful conversion, you can train your own detecter with the exported dataset and [Ultralytics YOLOv8 trainer](https://github.com/ultralytics/ultralytics). +After the successful conversion, you can train your own detector with the exported dataset and [Ultralytics YOLOv8 trainer](https://github.com/ultralytics/ultralytics). > Note, if you want to see the end-to-end Jupyter-notebook example from the dataset conversion to the training, please see this [link](https://github.com/openvinotoolkit/datumaro/blob/develop/notebooks/08_e2e_example_yolo_ultralytics_trainer.ipynb). diff --git a/docs/source/docs/jupyter_notebook_examples/e2e_example.rst b/docs/source/docs/jupyter_notebook_examples/e2e_example.rst index 9c214881c0..0cc20b7843 100644 --- a/docs/source/docs/jupyter_notebook_examples/e2e_example.rst +++ b/docs/source/docs/jupyter_notebook_examples/e2e_example.rst @@ -11,6 +11,7 @@ Here we provide E2E examples from Datumaro to model trainers. notebooks/10_noisy_label_detection_cls notebooks/13_noisy_label_detection_det notebooks/16_missing_annotation_detection + notebooks/22_framework_converter .. grid:: 1 2 2 2 :gutter: 2 @@ -42,3 +43,10 @@ Here we provide E2E examples from Datumaro to model trainers. :color: primary :outline: :expand: + + .. grid-item-card:: + + .. button-ref:: notebooks/22_framework_converter + :color: primary + :outline: + :expand: diff --git a/docs/source/docs/level-up/advanced_skills/12_project_versioning.rst b/docs/source/docs/level-up/advanced_skills/13_project_versioning.rst similarity index 99% rename from docs/source/docs/level-up/advanced_skills/12_project_versioning.rst rename to docs/source/docs/level-up/advanced_skills/13_project_versioning.rst index 161b63c78b..d4fffd91e0 100644 --- a/docs/source/docs/level-up/advanced_skills/12_project_versioning.rst +++ b/docs/source/docs/level-up/advanced_skills/13_project_versioning.rst @@ -1,5 +1,5 @@ ============================ -Level 12: Project Versioning +Level 13: Project Versioning ============================ Project versioning is a concept unique to Datumaro. Datumaro project includes a data source and revision tree, diff --git a/docs/source/docs/level-up/advanced_skills/13_pseudo_label_generation.rst b/docs/source/docs/level-up/advanced_skills/14_pseudo_label_generation.rst similarity index 68% rename from docs/source/docs/level-up/advanced_skills/13_pseudo_label_generation.rst rename to docs/source/docs/level-up/advanced_skills/14_pseudo_label_generation.rst index 99c632f8fe..cd444be1e2 100644 --- a/docs/source/docs/level-up/advanced_skills/13_pseudo_label_generation.rst +++ b/docs/source/docs/level-up/advanced_skills/14_pseudo_label_generation.rst @@ -1,5 +1,5 @@ ================================= -Level 13: Pseudo Label Generation +Level 14: Pseudo Label Generation ================================= TBD diff --git a/docs/source/docs/level-up/advanced_skills/14_data_pruning.rst b/docs/source/docs/level-up/advanced_skills/15_data_pruning.rst similarity index 99% rename from docs/source/docs/level-up/advanced_skills/14_data_pruning.rst rename to docs/source/docs/level-up/advanced_skills/15_data_pruning.rst index 66c044f7e8..5c299e50f3 100644 --- a/docs/source/docs/level-up/advanced_skills/14_data_pruning.rst +++ b/docs/source/docs/level-up/advanced_skills/15_data_pruning.rst @@ -1,5 +1,5 @@ ===================================================== -Level 14: Dataset Pruning +Level 15: Dataset Pruning ===================================================== diff --git a/docs/source/docs/level-up/advanced_skills/index.rst b/docs/source/docs/level-up/advanced_skills/index.rst index 59cfeefd1b..36e5cb26e5 100644 --- a/docs/source/docs/level-up/advanced_skills/index.rst +++ b/docs/source/docs/level-up/advanced_skills/index.rst @@ -5,16 +5,16 @@ Advanced Skills :maxdepth: 1 :hidden: - 12_project_versioning - 13_pseudo_label_generation - 14_data_pruning + 13_project_versioning + 14_pseudo_label_generation + 15_data_pruning .. grid:: 1 2 2 2 :gutter: 2 .. grid-item-card:: - .. button-ref:: 12_project_versioning + .. button-ref:: 13_project_versioning :color: primary :outline: :expand: @@ -25,7 +25,7 @@ Advanced Skills .. grid-item-card:: - .. button-ref:: 13_pseudo_label_generation + .. button-ref:: 14_pseudo_label_generation :color: primary :outline: :expand: @@ -36,7 +36,7 @@ Advanced Skills .. grid-item-card:: - .. button-ref:: 14_data_pruning + .. button-ref:: 15_data_pruning :color: primary :outline: :expand: 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 index f0345e3201..a40251bfe3 100644 --- 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 @@ -19,7 +19,7 @@ Convert data format =================== Users sometimes need 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 +system. To achieve this, Datumaro not only has ``import`` and ``export`` functionalities, but also provides ``convert``, which shortens the import and export into a single command line. Let's convert the Cityscapes data into the MS-COCO format, which is described in :ref:`here `. diff --git a/docs/source/docs/level-up/intermediate_skills/11_data_generation.rst b/docs/source/docs/level-up/intermediate_skills/11_data_generation.rst index 90a00ee5d6..b0a2aa5ffd 100644 --- a/docs/source/docs/level-up/intermediate_skills/11_data_generation.rst +++ b/docs/source/docs/level-up/intermediate_skills/11_data_generation.rst @@ -10,7 +10,7 @@ since the manual annotations is quite expensive work. Base on the [FractalDB]_, Datumaro provides a fractal image dataset (FractalDB) generator that can be utilized to pre-train the vision models. Learning visual features of FractalDB is known to increase the performance of Vision Transformer (ViT) models. -Note that a fractal patterns in FractalDB is calculated mathmatically using the interated function system (IFS) with random parameters. +Note that a fractal patterns in FractalDB is calculated mathematically using the integrated function system (IFS) with random parameters. We thus don't need to concern about any privacy issues. diff --git a/docs/source/docs/level-up/intermediate_skills/12_framework_conversion.rst b/docs/source/docs/level-up/intermediate_skills/12_framework_conversion.rst new file mode 100644 index 0000000000..f3468c326b --- /dev/null +++ b/docs/source/docs/level-up/intermediate_skills/12_framework_conversion.rst @@ -0,0 +1,56 @@ +============================ +Level 12: Framework Conversion +============================ + +Datumaro allows seamless conversion of datasets to popular deep learning frameworks, such as PyTorch and TensorFlow. +This is particularly useful when you are working with a dataset that needs to be used across different frameworks +without manual reformatting. + +Datumaro provides the FrameworkConverter class, which can be used to convert a dataset for various tasks +like classification, detection, and segmentation. + +Supported Tasks + - Classification + - Multilabel Classification + - Detection + - Instance Segmentation + - Semantic Segmentation + - Tabular Data + +.. tab-set:: + + .. tab-item:: Python + + With the PyTorch framework, you can convert a Datumaro dataset like this: + + .. code-block:: python + + from datumaro.plugins.framework_converter import FrameworkConverter + from torchvision import transforms + + transform = transforms.Compose([transforms.ToTensor()]) + dm_dataset = ... # Load your dataset here + + First, we have to specify the dataset, subset, and task + + .. code-block:: python + + multi_framework_dataset = FrameworkConverter(dm_dataset, subset="train", task="classification") + train_dataset = multi_framework_dataset.to_framework(framework="torch", transform=transform) + + Through this, we convert the dataset to PyTorch format + + .. code-block:: python + + from torch.utils.data import DataLoader + train_loader = DataLoader(train_dataset, batch_size=32) + + Now we can use the train_dataset with PyTorch DataLoader + +In this example: + +- `subset="train"` indicates that we are working with the training portion of the dataset. + +- `task="classification"` specifies that this is a classification task. + +- The dataset is converted to PyTorch-compatible format using the `to_framework` method. diff --git a/docs/source/docs/level-up/intermediate_skills/index.rst b/docs/source/docs/level-up/intermediate_skills/index.rst index 8e7e2bba20..a4ec91fd3c 100644 --- a/docs/source/docs/level-up/intermediate_skills/index.rst +++ b/docs/source/docs/level-up/intermediate_skills/index.rst @@ -13,6 +13,7 @@ Intermediate Skills 09_data_filtering 10_data_exploration 11_data_generation + 12_framework_conversion .. grid:: 1 2 2 2 :gutter: 2 @@ -102,3 +103,14 @@ Intermediate Skills :bdg-info:`CLI` :bdg-warning:`Python` + + .. grid-item-card:: + + .. button-ref:: 12_framework_conversion + :color: primary + :outline: + :expand: + + Level 12: Framework Conversion + + :bdg-warning:`Python` diff --git a/docs/source/docs/release_notes.rst b/docs/source/docs/release_notes.rst index 221e4658b0..ceb18dfc7f 100644 --- a/docs/source/docs/release_notes.rst +++ b/docs/source/docs/release_notes.rst @@ -4,6 +4,53 @@ Release Notes .. toctree:: :maxdepth: 1 +v1.10.0 (2024 Q4) + +New features +^^^^^^^^^^^^ +- Support KITTI 3D format +- Add PseudoLabeling transform for unlabeled dataset + +Enhancements +^^^^^^^^^^^^ +- Raise an appropriate error when exporting a datumaro dataset if its subset name contains path separators. +- Update docs for transform plugins +- Update ov ir model for explorer openvino launcher with CLIP ViT-L/14@336px model +- Optimize path assignment to handle point cloud in JSON without images +- Set TabularTransform to process clean transform in parallel + +Bug fixes +^^^^^^^^^ +- Fix datumaro format to load visibility information from Points annotations + +v1.9.1 (2024 Q3) +---------------- + +Enhancements +^^^^^^^^^^^^ +- Support multiple labels for kaggle format +- Use DataFrame.map instead of DataFrame.applymap + +Bug fixes +^^^^^^^^^ +- Fix StreamDataset merging when importing in eager mode + +v1.9.0 (2024 Q3) +---------------- + +New features +^^^^^^^^^^^^ +- Add a new CLI command: datum format +- Support language dataset for DmTorchDataset + +Enhancements +^^^^^^^^^^^^ +- Change _Shape to Shape and add comments for subclasses of Shape + +Bug fixes +^^^^^^^^^ +- Fix KITTI-3D importer and exporter + v1.8.0 (2024 Q3) ---------------- diff --git a/notebooks/21_kaggle_data_cleaning.ipynb b/notebooks/21_kaggle_data_cleaning.ipynb index 97980509b9..6078dcbc8a 100644 --- a/notebooks/21_kaggle_data_cleaning.ipynb +++ b/notebooks/21_kaggle_data_cleaning.ipynb @@ -51,16 +51,24 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/sooah/.pyenv/versions/datum/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, { "data": { "text/plain": [ "['tabular']" ] }, - "execution_count": 3, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } @@ -69,7 +77,7 @@ "import datumaro as dm\n", "from datumaro.components.environment import DEFAULT_ENVIRONMENT\n", "\n", - "data_path = \"/home/sooah/data/corona_nlp\"\n", + "data_path = \"~/data\"\n", "detected_formats = DEFAULT_ENVIRONMENT.detect_dataset(data_path)\n", "detected_formats" ] @@ -83,28 +91,28 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Dataset\n", - "\tsize=44955\n", - "\tsource_path=/home/sooah/data/corona_nlp\n", + "\tsize=2000\n", + "\tsource_path=/home/sooah/data/corona_nlp_1k\n", "\tmedia_type=\n", "\tann_types=set()\n", "\tannotated_items_count=0\n", "\tannotations_count=0\n", "subsets\n", - "\tCorona_NLP_test: # of items=3798, # of annotated items=0, # of annotations=0\n", - "\tCorona_NLP_train: # of items=41157, # of annotated items=0, # of annotations=0\n", + "\ttest: # of items=1000, # of annotated items=0, # of annotations=0\n", + "\ttrain: # of items=1000, # of annotated items=0, # of annotations=0\n", "infos\n", "\tcategories\n", - "\ttabular: []" + "\t14: []" ] }, - "execution_count": 4, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -148,28 +156,28 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Dataset\n", - "\tsize=44955\n", - "\tsource_path=/home/sooah/data/corona_nlp\n", + "\tsize=2000\n", + "\tsource_path=/home/sooah/data/corona_nlp_1k\n", "\tmedia_type=\n", "\tann_types={}\n", - "\tannotated_items_count=44955\n", - "\tannotations_count=44955\n", + "\tannotated_items_count=2000\n", + "\tannotations_count=2000\n", "subsets\n", - "\tCorona_NLP_test: # of items=3798, # of annotated items=3798, # of annotations=3798\n", - "\tCorona_NLP_train: # of items=41157, # of annotated items=41157, # of annotations=41157\n", + "\ttest: # of items=1000, # of annotated items=1000, # of annotations=1000\n", + "\ttrain: # of items=1000, # of annotated items=1000, # of annotations=1000\n", "infos\n", "\tcategories\n", - "\ttabular: ['Sentiment']" + "\t14: ['Sentiment']" ] }, - "execution_count": 5, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -200,16 +208,16 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "DatasetItem(id='0@Corona_NLP_train', subset='Corona_NLP_train', media=TableRow(row_idx:0, data:{'OriginalTweet': '@MeNyrbie @Phil_Gahan @Chrisitv https://t.co/iFz9FAn2Pa and https://t.co/xX6ghGFzCC and https://t.co/I2NlzdxNo8', 'Location': 'London', 'Sentiment': 'Neutral'}), annotations=[Tabular(id=0, attributes={}, group=0, object_id=-1, values={'Sentiment': 'Neutral'})], attributes={})" + "DatasetItem(id='0@test', subset='test', media=TableRow(row_idx:0, data:{'OriginalTweet': 'TRENDING: New Yorkers encounter empty supermarket shelves (pictured, Wegmans in Brooklyn), sold-out online grocers (FoodKick, MaxDelivery) as #coronavirus-fearing shoppers stock up https://t.co/Gr76pcrLWh https://t.co/ivMKMsqdT1', 'Location': 'NYC', 'Sentiment': 'Extremely Negative'}), annotations=[Tabular(id=0, attributes={}, group=0, object_id=-1, values={'Sentiment': 'Extremely Negative'})], attributes={})" ] }, - "execution_count": 7, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -227,15 +235,15 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "media : {'OriginalTweet': '@MeNyrbie @Phil_Gahan @Chrisitv https://t.co/iFz9FAn2Pa and https://t.co/xX6ghGFzCC and https://t.co/I2NlzdxNo8', 'Location': 'London', 'Sentiment': 'Neutral'}\n", - "annotations : [Tabular(id=0, attributes={}, group=0, object_id=-1, values={'Sentiment': 'Neutral'})]\n" + "media : {'OriginalTweet': 'TRENDING: New Yorkers encounter empty supermarket shelves (pictured, Wegmans in Brooklyn), sold-out online grocers (FoodKick, MaxDelivery) as #coronavirus-fearing shoppers stock up https://t.co/Gr76pcrLWh https://t.co/ivMKMsqdT1', 'Location': 'NYC', 'Sentiment': 'Extremely Negative'}\n", + "annotations : [Tabular(id=0, attributes={}, group=0, object_id=-1, values={'Sentiment': 'Extremely Negative'})]\n" ] } ], @@ -266,28 +274,28 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Dataset\n", - "\tsize=44955\n", - "\tsource_path=/home/sooah/data/corona_nlp\n", + "\tsize=2000\n", + "\tsource_path=/home/sooah/data/corona_nlp_1k\n", "\tmedia_type=\n", "\tann_types={}\n", - "\tannotated_items_count=44955\n", - "\tannotations_count=44955\n", + "\tannotated_items_count=2000\n", + "\tannotations_count=2000\n", "subsets\n", - "\tCorona_NLP_test: # of items=3798, # of annotated items=3798, # of annotations=3798\n", - "\tCorona_NLP_train: # of items=41157, # of annotated items=41157, # of annotations=41157\n", + "\ttest: # of items=1000, # of annotated items=1000, # of annotations=1000\n", + "\ttrain: # of items=1000, # of annotated items=1000, # of annotations=1000\n", "infos\n", "\tcategories\n", - "\tlabel: ['Sentiment:Extremely Negative', 'Sentiment:Extremely Positive', 'Sentiment:Negative', 'Sentiment:Neutral', 'Sentiment:Positive']" + "\t1: ['Sentiment:Extremely Negative', 'Sentiment:Extremely Positive', 'Sentiment:Negative', 'Sentiment:Neutral', 'Sentiment:Positive']" ] }, - "execution_count": 9, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -299,14 +307,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "annotations : [Label(id=0, attributes={}, group=0, object_id=-1, label=3)]\n" + "annotations : [Label(id=0, attributes={}, group=0, object_id=-1, label=0)]\n" ] } ], @@ -344,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -377,7 +385,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -385,17 +393,17 @@ "output_type": "stream", "text": [ "Statistics summary\n", - "Total number of annotation : 44955\n", + "Total number of annotation : 2000\n", "The number of items without any annotation : 0\n", "The number of items with missing annotation : 0\n", "\n", "\n", "Result of label distribution\n", " Sentiment:Extremely Negative Sentiment:Extremely Positive \\\n", - "0 6073 7223 \n", + "0 309 310 \n", "\n", " Sentiment:Negative Sentiment:Neutral Sentiment:Positive \n", - "0 10958 8332 12369 \n", + "0 568 318 495 \n", "The number of empty label for Sentiment is 0\n", "\n", "\n" @@ -403,7 +411,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABNQAAAF2CAYAAACmpMXLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAABJCElEQVR4nO3deVhV5f7//xeDDA4bxAEkFc3UMDXLkXKOxKKSssy0nFDPKbAcjqYnczp27GA5ZmmTNmiZfcrMAcUpyswBswzTrJzKQHNgO4EI9+8Pf6yvW1BZuhGH5+O69pX7Xve613st9s3evVh7LQ9jjBEAAAAAAACAQvEs7gIAAAAAAACAawmBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAC4JNWqVVOPHj2Ku4xr0unTpzVkyBBVqVJFnp6eiomJKdLttW7dWq1bt7a93q5du+Th4aFXXnnFbbWsXr1aHh4eWr169SWtP2rUKHl4eLi0XanXYt7xmDVrltXWo0cPlS5dusi3ncfDw0OjRo26YtsDAAAFI1ADAACaNWuWPDw8tHHjxgKXt27dWnXr1r3s7SxevJgwQNK7776r8ePH69FHH9V7772nAQMGFHdJN5yr+bV4NdcGAADO8C7uAgAAwLVp+/bt8vS097e5xYsXa9q0aTd8WLBy5UrddNNNmjhxYnGXcl24Uq/FsLAwnTx5UiVKlLBZoT0Xqu3kyZPy9uYjPAAAxY0z1AAAwCXx9fUt8mDB3Y4fP17cJUiS9u/fr8DAQLeNl5ubq8zMTLeNd60p6tfi6dOnderUKXl4eMjPz09eXl5Ftq2L8fPzI1ADAOAqQKAGAAAuybnXrcrOztbo0aNVs2ZN+fn5qVy5cmrevLmSkpIknbnW1LRp0ySduQ5U3iPP8ePHNWjQIFWpUkW+vr6qXbu2XnnlFRljXLZ78uRJPfvssypfvrzKlCmjhx56SH/++We+a0vlXWtr69at6tKli8qWLavmzZtLkn788Uf16NFDN998s/z8/BQSEqJevXrp4MGDLtvKG+OXX37Rk08+qYCAAFWoUEEvvviijDHau3evOnToIIfDoZCQEL366qsXPGZ51+BatWqVUlNTrWOQdz2xwh4DDw8PxcfHa/bs2brtttvk6+urxMTEi//Q/n+nTp3SiBEj1LBhQwUEBKhUqVJq0aKFVq1add51Jk6cqLCwMPn7+6tVq1b66aef8vXZtm2bHn30UQUFBcnPz0+NGjXSggULCl3Xub755hs1btxYfn5+qlGjhmbMmFFgP3e+Fs++btykSZNUo0YN+fr6auvWrQVeQy3P77//rqioKJUqVUqhoaEaM2aMy8/tfNeOO3fMi82Tgq6h9v333+u+++6Tw+FQ6dKldc899+i7775z6ZP3te41a9Zo4MCBqlChgkqVKqWHH35YBw4cKPgHAAAAzos/bwEAAEtGRob+/vvvfO3Z2dkXXXfUqFEaN26cevfurSZNmsjpdGrjxo3atGmT7r33Xv3jH//Qvn37lJSUpA8++MBlXWOMHnroIa1atUqxsbFq0KCBli5dqsGDB+vPP/90+Wpkjx499Mknn+ipp55Ss2bN9NVXXyk6Ovq8dT322GOqWbOm/vvf/1oBR1JSkn7//Xf17NlTISEhSk1N1ZtvvqnU1FR99913+S56//jjjys8PFwvv/yyFi1apLFjxyooKEgzZsxQ27Zt9b///U+zZ8/Wv/71LzVu3FgtW7YssJYKFSrogw8+0EsvvaRjx45p3LhxkqTw8HBbx0A687XRTz75RPHx8SpfvryqVat20Z9RHqfTqbfffltPPPGE+vTpo6NHj+qdd95RVFSU1q9frwYNGrj0f//993X06FHFxcUpMzNTkydPVtu2bbVlyxYFBwdLklJTU3X33Xfrpptu0tChQ1WqVCl98skniomJ0f/93//p4YcfLnR9krRlyxa1a9dOFSpU0KhRo3T69GmNHDnS2t6FXM5rMc/MmTOVmZmpvn37ytfXV0FBQcrNzS2wb05Ojtq3b69mzZopISFBiYmJGjlypE6fPq0xY8bY2u/C1Ha21NRUtWjRQg6HQ0OGDFGJEiU0Y8YMtW7dWl999ZWaNm3q0r9fv34qW7asRo4cqV27dmnSpEmKj4/X3LlzbdUJAMANzwAAgBvezJkzjaQLPm677TaXdcLCwkz37t2t57fffruJjo6+4Hbi4uJMQR8/5s+fbySZsWPHurQ/+uijxsPDw/z666/GGGNSUlKMJNO/f3+Xfj169DCSzMiRI622kSNHGknmiSeeyLe9EydO5Gv76KOPjCSTnJycb4y+fftabadPnzaVK1c2Hh4e5uWXX7baDx8+bPz9/V2Oyfm0atUq3/Es7DEwxhhJxtPT06Smpl50W3nba9Wqlcs+ZGVlufQ5fPiwCQ4ONr169bLadu7caSQZf39/88cff1jt69atM5LMgAEDrLZ77rnH1KtXz2RmZlptubm55q677jI1a9a02latWmUkmVWrVl2w5piYGOPn52d2795ttW3dutV4eXnlew2587WYt88Oh8Ps37+/wGUzZ8602rp3724kmX79+lltubm5Jjo62vj4+JgDBw5ccL8LGvN8tRlj8r3OY2JijI+Pj/ntt9+stn379pkyZcqYli1bWm15czwyMtLk5uZa7QMGDDBeXl7myJEjBW4PAAAUjK98AgAAy7Rp05SUlJTvUb9+/YuuGxgYqNTUVO3YscP2dhcvXiwvLy89++yzLu2DBg2SMUZLliyRJOtrjc8884xLv379+p137H/+85/52vz9/a1/Z2Zm6u+//1azZs0kSZs2bcrXv3fv3ta/vby81KhRIxljFBsba7UHBgaqdu3a+v33389by4UU9hjkadWqlerUqXNJ2/Ly8pKPj4+kM9dfO3TokE6fPq1GjRoVuP8xMTG66aabrOdNmjRR06ZNtXjxYknSoUOHtHLlSnXq1ElHjx7V33//rb///lsHDx5UVFSUduzYoT///LPQ9eXk5Gjp0qWKiYlR1apVrfbw8HBFRUVddP3LeS3m6dixoypUqFDo/vHx8da/876Se+rUKS1fvvySa7iYnJwcLVu2TDExMbr55put9kqVKqlLly765ptv5HQ6Xdbp27evyxmYLVq0UE5Ojnbv3l1kdQIAcD0iUAMAAJYmTZooMjIy36Ns2bIXXXfMmDE6cuSIatWqpXr16mnw4MH68ccfC7Xd3bt3KzQ0VGXKlHFpDw8Pt5bn/dfT01PVq1d36XfLLbecd+xz+0pnAqDnnntOwcHB8vf3V4UKFax+GRkZ+fqfHepIUkBAgPz8/FS+fPl87YcPHz5vLRdS2GOQp6D9suO9995T/fr1rWuMVahQQYsWLSpw/2vWrJmvrVatWtq1a5ck6ddff5UxRi+++KIqVKjg8hg5cqSkMzdiKKwDBw7o5MmTBW63du3aF13/cl6LeewcX09PT5dASzpzfCRZx6goHDhwQCdOnCjwmISHhys3N1d79+51aT/3tZw3ty/1dQsAwI2Ka6gBAAC3aNmypX777Td98cUXWrZsmd5++21NnDhR06dPdznD60o7+2y0PJ06ddK3336rwYMHq0GDBipdurRyc3PVvn37Aq+TVdBdHc93p0dzzg0EikpB+1VYH374oXr06KGYmBgNHjxYFStWlJeXl8aNG6fffvvN9nh5x+xf//rXec8gu1Do6W7ueC1ezvEtyLnX5cuTk5Pj1u1cTHG/bgEAuF4QqAEAALcJCgpSz5491bNnTx07dkwtW7bUqFGjrBDjfKFCWFiYli9frqNHj7qcobVt2zZred5/c3NztXPnTpezl3799ddC13j48GGtWLFCo0eP1ogRI6z2y/l6oDsU9hi4w6effqqbb75Zn332mcvPJO9ssnMVdGx++eUX60YIeWdnlShRQpGRkZddX4UKFeTv71/gdrdv316oMS71tXgpcnNz9fvvv1tnpUlnjo8k6xjlnQl25MgRl3UL+qplYWurUKGCSpYsWeAx2bZtmzw9PVWlSpVCjQUAAOzhK58AAMAtDh486PK8dOnSuuWWW5SVlWW1lSpVSlL+UOH+++9XTk6OXnvtNZf2iRMnysPDQ/fdd58kWWc/vf766y79pk6dWug6887QOfeMnEmTJhV6jKJQ2GPgDgUdg3Xr1mnt2rUF9p8/f77LNdDWr1+vdevWWTVVrFhRrVu31owZM/TXX3/lW//AgQO264uKitL8+fO1Z88eq/3nn3/W0qVLL7r+5bwWL9XZPzdjjF577TWVKFFC99xzj6QzgaiXl5eSk5Nd1jv3tWynNi8vL7Vr105ffPGFy1dL09PTNWfOHDVv3lwOh+MS9wgAAFwIZ6gBAAC3qFOnjlq3bq2GDRsqKChIGzdu1KeffupysfaGDRtKkp599llFRUXJy8tLnTt31oMPPqg2bdrohRde0K5du3T77bdr2bJl+uKLL9S/f3/VqFHDWr9jx46aNGmSDh48qGbNmumrr76yzgYqzJk9DodDLVu2VEJCgrKzs3XTTTdp2bJl2rlzZxEclcIr7DFwhwceeECfffaZHn74YUVHR2vnzp2aPn266tSpo2PHjuXrf8stt6h58+Z6+umnlZWVpUmTJqlcuXIaMmSI1WfatGlq3ry56tWrpz59+ujmm29Wenq61q5dqz/++EM//PCDrRpHjx6txMREtWjRQs8884xOnz6tqVOn6rbbbrvo9dAu57V4Kfz8/JSYmKju3buradOmWrJkiRYtWqR///vf1o0NAgIC9Nhjj2nq1Kny8PBQjRo1tHDhwgKvLWentrFjxyopKUnNmzfXM888I29vb82YMUNZWVlKSEi4pP0BAAAXR6AGAADc4tlnn9WCBQu0bNkyZWVlKSwsTGPHjtXgwYOtPo888oj69eunjz/+WB9++KGMMercubM8PT21YMECjRgxQnPnztXMmTNVrVo1jR8/XoMGDXLZzvvvv6+QkBB99NFH+vzzzxUZGam5c+eqdu3a8vPzK1Stc+bMUb9+/TRt2jQZY9SuXTstWbJEoaGhbj0mdtg5BperR48eSktL04wZM7R06VLVqVNHH374oebNm6fVq1fn69+tWzd5enpq0qRJ2r9/v5o0aaLXXntNlSpVsvrUqVNHGzdu1OjRozVr1iwdPHhQFStW1B133OHy1drCql+/vpYuXaqBAwdqxIgRqly5skaPHq2//vrrooHa5bwWL4WXl5cSExP19NNPa/DgwSpTpoxGjhyZb7+nTp2q7OxsTZ8+Xb6+vurUqZPGjx+vunXruvSzU9ttt92mr7/+WsOGDdO4ceOUm5urpk2b6sMPP1TTpk0vaX8AAMDFeRiuQAoAAK5xmzdv1h133KEPP/xQXbt2Le5yAAAAcJ3jGmoAAOCacvLkyXxtkyZNkqenp1q2bFkMFQEAAOBGw1c+AQDANSUhIUEpKSlq06aNvL29tWTJEi1ZskR9+/bljoYAAAC4IvjKJwAAuKYkJSVp9OjR2rp1q44dO6aqVavqqaee0gsvvCBvb/5WCAAAgKJHoAYAAAAAAADYwDXUAAAAAAAAABsI1AAAAAAAAAAbbugLjeTm5mrfvn0qU6aMPDw8irscAAAAAAAAFBNjjI4eParQ0FB5el74HLQbOlDbt28fdwMDAAAAAACAZe/evapcufIF+9zQgVqZMmUknTlQDoejmKsBAAAAAABAcXE6napSpYqVF13IDR2o5X3N0+FwEKgBAAAAAACgUJcF46YEAAAAAAAAgA0EagAAAAAAAIANBGoAAAAAAACADQRqAAAAAAAAgA0EagAAAAAAAIANBGoAAAAAAACADQRqAAAAAAAAgA0EagAAAAAAAIANBGoAAAAAAACADQRqAAAAAAAAgA0EagAAAAAAAIANBGoAAAAAAACADd7FXQAAAAAAADeqakMXFXcJwCXb9XJ0cZdQbDhDDQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsMF2oJacnKwHH3xQoaGh8vDw0Pz5861l2dnZev7551WvXj2VKlVKoaGh6tatm/bt2+cyxqFDh9S1a1c5HA4FBgYqNjZWx44dc+nz448/qkWLFvLz81OVKlWUkJCQr5Z58+bp1ltvlZ+fn+rVq6fFixfb3R0AAAAAAADAFtuB2vHjx3X77bdr2rRp+ZadOHFCmzZt0osvvqhNmzbps88+0/bt2/XQQw+59OvatatSU1OVlJSkhQsXKjk5WX379rWWO51OtWvXTmFhYUpJSdH48eM1atQovfnmm1afb7/9Vk888YRiY2P1/fffKyYmRjExMfrpp5/s7hIAAAAAAABQaB7GGHPJK3t46PPPP1dMTMx5+2zYsEFNmjTR7t27VbVqVf3888+qU6eONmzYoEaNGkmSEhMTdf/99+uPP/5QaGio3njjDb3wwgtKS0uTj4+PJGno0KGaP3++tm3bJkl6/PHHdfz4cS1cuNDaVrNmzdSgQQNNnz69UPU7nU4FBAQoIyNDDofjEo8CAAAAAACXhrt84lp2vd3l005OVOTXUMvIyJCHh4cCAwMlSWvXrlVgYKAVpklSZGSkPD09tW7dOqtPy5YtrTBNkqKiorR9+3YdPnzY6hMZGemyraioKK1du7aI9wgAAAAAAAA3Mu+iHDwzM1PPP/+8nnjiCSvZS0tLU8WKFV2L8PZWUFCQ0tLSrD7Vq1d36RMcHGwtK1u2rNLS0qy2s/vkjVGQrKwsZWVlWc+dTuel7xwAAAAAAABuSEV2hlp2drY6deokY4zeeOONotqMLePGjVNAQID1qFKlSnGXBAAAAAAAgGtMkQRqeWHa7t27lZSU5PK905CQEO3fv9+l/+nTp3Xo0CGFhIRYfdLT01365D2/WJ+85QUZNmyYMjIyrMfevXsvfScBAAAAAABwQ3J7oJYXpu3YsUPLly9XuXLlXJZHREToyJEjSklJsdpWrlyp3NxcNW3a1OqTnJys7Oxsq09SUpJq166tsmXLWn1WrFjhMnZSUpIiIiLOW5uvr68cDofLAwAAAAAAALDDdqB27Ngxbd68WZs3b5Yk7dy5U5s3b9aePXuUnZ2tRx99VBs3btTs2bOVk5OjtLQ0paWl6dSpU5Kk8PBwtW/fXn369NH69eu1Zs0axcfHq3PnzgoNDZUkdenSRT4+PoqNjVVqaqrmzp2ryZMna+DAgVYdzz33nBITE/Xqq69q27ZtGjVqlDZu3Kj4+Hg3HBYAAAAAAACgYB7GGGNnhdWrV6tNmzb52rt3765Ro0blu5lAnlWrVql169aSpEOHDik+Pl5ffvmlPD091bFjR02ZMkWlS5e2+v/444+Ki4vThg0bVL58efXr10/PP/+8y5jz5s3T8OHDtWvXLtWsWVMJCQm6//77C70vdm6HCgAAAACAu1Ubuqi4SwAu2a6Xo4u7BLeykxPZDtSuJwRqAAAAAIDiRKCGa9mNHKgV2V0+AQAAAAAAgOsRgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYIN3cRcAAABQGNWGLiruEoBLtuvl6OIuAQAAuBFnqAEAAAAAAAA2EKgBAAAAAAAANhCoAQAAAAAAADYQqAEAAAAAAAA2EKgBAAAAAAAANhCoAQAAAAAAADYQqAEAAAAAAAA2EKgBAAAAAAAANhCoAQAAAAAAADYQqAEAAAAAAAA2EKgBAAAAAAAANhCoAQAAAAAAADYQqAEAAAAAAAA2EKgBAAAAAAAANhCoAQAAAAAAADYQqAEAAAAAAAA2EKgBAAAAAAAANhCoAQAAAAAAADbYDtSSk5P14IMPKjQ0VB4eHpo/f77LcmOMRowYoUqVKsnf31+RkZHasWOHS59Dhw6pa9eucjgcCgwMVGxsrI4dO+bS58cff1SLFi3k5+enKlWqKCEhIV8t8+bN06233io/Pz/Vq1dPixcvtrs7AAAAAAAAgC22A7Xjx4/r9ttv17Rp0wpcnpCQoClTpmj69Olat26dSpUqpaioKGVmZlp9unbtqtTUVCUlJWnhwoVKTk5W3759reVOp1Pt2rVTWFiYUlJSNH78eI0aNUpvvvmm1efbb7/VE088odjYWH3//feKiYlRTEyMfvrpJ7u7BAAAAAAAABSahzHGXPLKHh76/PPPFRMTI+nM2WmhoaEaNGiQ/vWvf0mSMjIyFBwcrFmzZqlz5876+eefVadOHW3YsEGNGjWSJCUmJur+++/XH3/8odDQUL3xxht64YUXlJaWJh8fH0nS0KFDNX/+fG3btk2S9Pjjj+v48eNauHChVU+zZs3UoEEDTZ8+vVD1O51OBQQEKCMjQw6H41IPAwAAuAKqDV1U3CUAl2zXy9HFXQKAqxTvb7iWXW/vb3ZyIrdeQ23nzp1KS0tTZGSk1RYQEKCmTZtq7dq1kqS1a9cqMDDQCtMkKTIyUp6enlq3bp3Vp2XLllaYJklRUVHavn27Dh8+bPU5ezt5ffK2U5CsrCw5nU6XBwAAAAAAAGCHWwO1tLQ0SVJwcLBLe3BwsLUsLS1NFStWdFnu7e2toKAglz4FjXH2Ns7XJ295QcaNG6eAgADrUaVKFbu7CAAAAAAAgBvcDXWXz2HDhikjI8N67N27t7hLAgAAAAAAwDXGrYFaSEiIJCk9Pd2lPT093VoWEhKi/fv3uyw/ffq0Dh065NKnoDHO3sb5+uQtL4ivr68cDofLAwAAAAAAALDDrYFa9erVFRISohUrVlhtTqdT69atU0REhCQpIiJCR44cUUpKitVn5cqVys3NVdOmTa0+ycnJys7OtvokJSWpdu3aKlu2rNXn7O3k9cnbDgAAAAAAAFAUbAdqx44d0+bNm7V582ZJZ25EsHnzZu3Zs0ceHh7q37+/xo4dqwULFmjLli3q1q2bQkNDrTuBhoeHq3379urTp4/Wr1+vNWvWKD4+Xp07d1ZoaKgkqUuXLvLx8VFsbKxSU1M1d+5cTZ48WQMHDrTqeO6555SYmKhXX31V27Zt06hRo7Rx40bFx8df/lEBAAAAAAAAzsPb7gobN25UmzZtrOd5IVf37t01a9YsDRkyRMePH1ffvn115MgRNW/eXImJifLz87PWmT17tuLj43XPPffI09NTHTt21JQpU6zlAQEBWrZsmeLi4tSwYUOVL19eI0aMUN++fa0+d911l+bMmaPhw4fr3//+t2rWrKn58+erbt26l3QgAAAAAAAAgMLwMMaY4i6iuDidTgUEBCgjI4PrqQEAcJWrNnRRcZcAXLJdL0cXdwkArlK8v+Fadr29v9nJiW6ou3wCAAAAAAAAl4tADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwwbu4CwAAAABwdak2dFFxlwBcsl0vRxd3CQBuAJyhBgAAAAAAANhAoAYAAAAAAADY4PZALScnRy+++KKqV68uf39/1ahRQ//5z39kjLH6GGM0YsQIVapUSf7+/oqMjNSOHTtcxjl06JC6du0qh8OhwMBAxcbG6tixYy59fvzxR7Vo0UJ+fn6qUqWKEhIS3L07AAAAAAAAgAu3B2r/+9//9MYbb+i1117Tzz//rP/9739KSEjQ1KlTrT4JCQmaMmWKpk+frnXr1qlUqVKKiopSZmam1adr165KTU1VUlKSFi5cqOTkZPXt29da7nQ61a5dO4WFhSklJUXjx4/XqFGj9Oabb7p7lwAAAAAAAACL229K8O2336pDhw6Kjj5zIchq1arpo48+0vr16yWdOTtt0qRJGj58uDp06CBJev/99xUcHKz58+erc+fO+vnnn5WYmKgNGzaoUaNGkqSpU6fq/vvv1yuvvKLQ0FDNnj1bp06d0rvvvisfHx/ddttt2rx5syZMmOASvAEAAAAAAADu5PYz1O666y6tWLFCv/zyiyTphx9+0DfffKP77rtPkrRz506lpaUpMjLSWicgIEBNmzbV2rVrJUlr165VYGCgFaZJUmRkpDw9PbVu3TqrT8uWLeXj42P1iYqK0vbt23X48GF37xYAAAAAAAAgqQjOUBs6dKicTqduvfVWeXl5KScnRy+99JK6du0qSUpLS5MkBQcHu6wXHBxsLUtLS1PFihVdC/X2VlBQkEuf6tWr5xsjb1nZsmXz1ZaVlaWsrCzrudPpvJxdBQAAAAAAwA3I7WeoffLJJ5o9e7bmzJmjTZs26b333tMrr7yi9957z92bsm3cuHEKCAiwHlWqVCnukgAAAAAAAHCNcXugNnjwYA0dOlSdO3dWvXr19NRTT2nAgAEaN26cJCkkJESSlJ6e7rJeenq6tSwkJET79+93WX769GkdOnTIpU9BY5y9jXMNGzZMGRkZ1mPv3r2XubcAAAAAAAC40bg9UDtx4oQ8PV2H9fLyUm5uriSpevXqCgkJ0YoVK6zlTqdT69atU0REhCQpIiJCR44cUUpKitVn5cqVys3NVdOmTa0+ycnJys7OtvokJSWpdu3aBX7dU5J8fX3lcDhcHgAAAAAAAIAdbg/UHnzwQb300ktatGiRdu3apc8//1wTJkzQww8/LEny8PBQ//79NXbsWC1YsEBbtmxRt27dFBoaqpiYGElSeHi42rdvrz59+mj9+vVas2aN4uPj1blzZ4WGhkqSunTpIh8fH8XGxio1NVVz587V5MmTNXDgQHfvEgAAAAAAAGBx+00Jpk6dqhdffFHPPPOM9u/fr9DQUP3jH//QiBEjrD5DhgzR8ePH1bdvXx05ckTNmzdXYmKi/Pz8rD6zZ89WfHy87rnnHnl6eqpjx46aMmWKtTwgIEDLli1TXFycGjZsqPLly2vEiBHq27evu3cJAAAAAAAAsHgYY0xxF1FcnE6nAgIClJGRwdc/AQC4ylUbuqi4SwAu2a6Xo4u7BFuYb7iWMd+AK+dam28XYycncvtXPgEAAAAAAIDrGYEaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGADgRoAAAAAAABgA4EaAAAAAAAAYAOBGgAAAAAAAGCDd3EXAADXsmpDFxV3CcAl2/VydHGXAAAAAFyTOEMNAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwgUANAAAAAAAAsIFADQAAAAAAALCBQA0AAAAAAACwoUgCtT///FNPPvmkypUrJ39/f9WrV08bN260lhtjNGLECFWqVEn+/v6KjIzUjh07XMY4dOiQunbtKofDocDAQMXGxurYsWMufX788Ue1aNFCfn5+qlKlihISEopidwAAAAAAAACL2wO1w4cP6+6771aJEiW0ZMkSbd26Va+++qrKli1r9UlISNCUKVM0ffp0rVu3TqVKlVJUVJQyMzOtPl27dlVqaqqSkpK0cOFCJScnq2/fvtZyp9Opdu3aKSwsTCkpKRo/frxGjRqlN9980927BAAAAAAAAFi83T3g//73P1WpUkUzZ8602qpXr2792xijSZMmafjw4erQoYMk6f3331dwcLDmz5+vzp076+eff1ZiYqI2bNigRo0aSZKmTp2q+++/X6+88opCQ0M1e/ZsnTp1Su+++658fHx02223afPmzZowYYJL8HajqTZ0UXGXAFyWXS9HF3cJAAAAAABckNvPUFuwYIEaNWqkxx57TBUrVtQdd9yht956y1q+c+dOpaWlKTIy0moLCAhQ06ZNtXbtWknS2rVrFRgYaIVpkhQZGSlPT0+tW7fO6tOyZUv5+PhYfaKiorR9+3YdPny4wNqysrLkdDpdHgAAAAAAAIAdbg/Ufv/9d73xxhuqWbOmli5dqqefflrPPvus3nvvPUlSWlqaJCk4ONhlveDgYGtZWlqaKlas6LLc29tbQUFBLn0KGuPsbZxr3LhxCggIsB5VqlS5zL0FAAAAAADAjcbtgVpubq7uvPNO/fe//9Udd9yhvn37qk+fPpo+fbq7N2XbsGHDlJGRYT327t1b3CUBAAAAAADgGuP2QK1SpUqqU6eOS1t4eLj27NkjSQoJCZEkpaenu/RJT0+3loWEhGj//v0uy0+fPq1Dhw659ClojLO3cS5fX185HA6XBwAAAAAAAGCH2wO1u+++W9u3b3dp++WXXxQWFibpzA0KQkJCtGLFCmu50+nUunXrFBERIUmKiIjQkSNHlJKSYvVZuXKlcnNz1bRpU6tPcnKysrOzrT5JSUmqXbu2yx1FAQAAAAAAAHdye6A2YMAAfffdd/rvf/+rX3/9VXPmzNGbb76puLg4SZKHh4f69++vsWPHasGCBdqyZYu6deum0NBQxcTESDpzRlv79u3V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We644w7l5ORo//79atGixSWP4876Tp06pSeffFKPP/64ateurd69e2vLli2qWLGi7W3deeed2r59+0UDBLsuZdx69eqpUaNGeuuttzRnzhy99tprtrbZr18/TZkyRZMnT85Xy9y5c1WxYkU5HI4C1w0ODtaGDRvUsmVLSWf+8rFp0yY1aNBAknTw4EFt375db731lvU6+Oabb1zG8PHxsda9ED8/Pz3yyCOaPXu2fv31V9WuXVt33nmnS71F8TPJw/y7PMy//+d84aPD4VBoaKjWrFmjVq1aWe1r1qxxOYPH4XDo8ccf1+OPP65HH31U7du316FDhxQUFOQy3rUyt65mefOeOX9hvI+gKDEPC+dy5mGFChX0119/Wc9zcnL0008/udw51E5da9as0euvv677779fkrR37179/fffdncJVxhz7fz47IbCIFC7Shw8eFCPPfaYevXqpfr166tMmTLauHGjEhIS1KFDB0lSZGSkIiIiFBMTo4SEBNWqVUv79u3TokWL9PDDD+f7itT5VKtWTevWrdOuXbtUunTpfJP6UtWqVUtdu3ZVt27d9Oqrr+qOO+7QgQMHtGLFCtWvX1/R0dGFrm/p0qXavn27ypUrp4CAAJUoUULdunXTTTfdpHHjxll9jTFKS0vLN0bFihXl6empF154QRkZGZoyZYpKly6txYsXq1evXlq4cKG1rZ07d2rz5s2qXLmyypQpc96vpI0YMUIPPPCAqlatqkcffVSenp764Ycf9NNPP2ns2LGXcMQub9zevXsrPj5epUqV0sMPP2xrm35+fho9erTi4uJc2rt27arx48erQ4cOGjNmjCpXrqzdu3frs88+05AhQ1S5cmX169dP48aN0y233KJbb71VU6dO1eHDh62/VJYtW1blypXTm2++qUqVKmnPnj0aOnSoy3YqVqwof39/JSYmqnLlyvLz8zvvraG7du2qBx54QKmpqXryySfdcuzOxfxzrY/5V7Tz71yDBw/WyJEjVaNGDTVo0EAzZ87U5s2brTMLJkyYoEqVKumOO+6Qp6en5s2bp5CQEJe/GOe52ubW1exi8545n3/On433EbgD8/D/1Xel52Hbtm01cOBALVq0SDVq1NCECRN05MiRfHUlJyerc+fO8vX1Vfny5c+7DzVr1tQHH3ygRo0ayel0avDgwfkuA4Hiw1z7f/Vdylw7F5/dkMezuAvAGaVLl1bTpk01ceJEtWzZUnXr1tWLL76oPn36WGc/eHh4aPHixWrZsqV69uypWrVqqXPnztq9e7d1raDC+Ne//iUvLy/VqVNHFSpUcOu1DWbOnKlu3bpp0KBBql27tmJiYrRhwwZVrVq10GP06dNHtWvXVqNGjVShQgWtWbNG0pnrppz9lzTpzCm3lSpVyvfYv3+/Vq9erUmTJumDDz6Qw+GQp6enPvjgA3399dd64403JJ25lkD79u3Vpk0bVahQQR999NF564qKitLChQu1bNkyNW7cWM2aNdPEiRML/VVLd4/7xBNPyNvbW0888cQlncbbvXt33XzzzS5tJUuWVHJysqpWrapHHnlE4eHhio2NVWZmpvUXzueff15PPPGEunXrpoiICJUuXVpRUVFWDZ6envr444+VkpKiunXrasCAARo/frzLdry9vTVlyhTNmDFDoaGhVmhVkLZt2yooKEjbt29Xly5dXJa562fC/Pt/mH9XZv6d7dlnn9XAgQM1aNAg1atXT4mJiVqwYIFq1qwp6cxXDhISEtSoUSM1btxYu3bt0uLFi61rZpztaptbV7OLzXvmfP45fy7eR3C5mIdnFMc87NWrl7p3765u3bqpVatWuvnmm13OTpOkMWPGaNeuXapRo8ZFv7L9zjvv6PDhw7rzzjv11FNP6dlnn7XOSEfxY66dcTlz7Wx8dkMeD3O5X+AHUCzyPuBs2LDB5XTfKy03N1fh4eHq1KmT/vOf/xRbHcCVdLXMP+B6wPsIAAC4FvGVT+Aak52drYMHD2r48OFq1qzZFf+f+d27d2vZsmVq1aqVsrKy9Nprr2nnzp35/koCXI+Ke/4B1wPeRwAAwPWAr3wC15g1a9aoUqVK2rBhg6ZPn37Ft+/p6alZs2apcePGuvvuu7VlyxYtX75c4eHhV7wW4Eor7vkHXA94HwEAANcDvvIJAAAAAAAA2MAZagAAAAAAAIANBGoAAAAAAACADQRqAAAAAAAAgA0EagAAAAAAAIANBGoAAAAAAACADQRqAAAAAAAAgA0EagAAAAAAAIANBGoAAAAAAACADQRqAAAAAAAAgA3/H4V5aDsatZDjAAAAAElFTkSuQmCC", + "image/png": "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L7CNJ5980mo7d+6cqV69unFzczMvvfSS1X7s2DHj5+fndE6K0qZNmwLns7jnwBhjJBl3d3ezY8eOy+4rf39t2rRxOoasrCynPseOHTNhYWHm8ccft9r27t1rJBk/Pz/zyy+/WO2bN282ksyQIUOstnvuucc0bNjQZGZmWm15eXnmzjvvNLfccovVtmbNGiPJrFmz5pJj7tq1q/H19TX79++32nbu3Gk8PDwKPIbK8rGYf8wOh8Okp6cXuiw5OdlqS0hIMJLMoEGDrLa8vDwTHx9vvL29zZEjRy553IVts6ixGWMKPM67du1qvL29zZ49e6y2Q4cOmcDAQNO6dWurLb/GY2NjTV5entU+ZMgQ4+HhYY4fP17o/gAAQEF8NBIAgOtEUlKSVq5cWeDWqFGjy65buXJl7dixQz/99JPL+12yZIk8PDz09NNPO7UPGzZMxhgtXbpUkqyP/z311FNO/QYNGlTktv/85z8XaPPz87P+n5mZqd9++00tW7aUJG3durVA/yeeeML6v4eHh5o2bSpjjPr162e1V65cWfXq1dPPP/9c5FgupbjnIF+bNm0UHR1don15eHjI29tb0vn5xX7//XedO3dOTZs2LfT4u3btqhtvvNG637x5c7Vo0UJLliyRJP3+++/64osv1KNHD508eVK//fabfvvtNx09elRxcXH66aef9OuvvxZ7fLm5uVq+fLm6du2qmjVrWu1RUVGKi4u77PqleSzm6969u0JCQordf+DAgdb/8z+6mp2drVWrVpV4DJeTm5urFStWqGvXrqpdu7bVXq1aNT322GP697//rYyMDKd1nnzySacrHu+++27l5uZq//79V2ycAABcbwjCAAC4TjRv3lyxsbEFblWqVLnsuuPHj9fx48dVt25dNWzYUMOHD9d///vfYu13//79ioiIUGBgoFN7VFSUtTz/X3d3d910001O/W6++eYit31xX+l8cPOXv/xFYWFh8vPzU0hIiNXvxIkTBfpfGMZIUlBQkHx9fXXDDTcUaD927FiRY7mU4p6DfIUdlyveeecdNWrUyJpDKyQkRIsXLy70+G+55ZYCbXXr1tW+ffskSbt375YxRs8//7xCQkKcbmPGjJF0/gsCiuvIkSM6e/ZsofutV6/eZdcvzWMxnyvn193d3SmIks6fH0nWOboSjhw5ojNnzhR6TqKiopSXl6eDBw86tV/8WM6v7ZI+bgEAsCPmCAMAAGrdurX27NmjTz/9VCtWrNBbb72lqVOnasaMGU5XVJW3C6/+ytejRw999dVXGj58uBo3bqyAgADl5eWpY8eOhc4DVdi3BBb1zYHmoontr5TCjqu43nvvPfXp00ddu3bV8OHDFRoaKg8PD02cOFF79uxxeXv55+yZZ54p8oqtS4WVZa0sHoulOb+FuXjeuXy5ubllup/LqejHLQAA1wOCMAAAIEkKDg5W37591bdvX506dUqtW7fW2LFjrfChqDAgMjJSq1at0smTJ52uiPrhhx+s5fn/5uXlae/evU5XC+3evbvYYzx27JhWr16tcePGafTo0VZ7aT5GVxaKew7KwkcffaTatWvr448/dvqZ5F+9dbHCzs2PP/5oTdCffzWUl5eXYmNjSz2+kJAQ+fn5FbrfXbt2FWsbJX0slkReXp5+/vln6yow6fz5kWSdo/wrr44fP+60bmEfSSzu2EJCQlSpUqVCz8kPP/wgd3d31ahRo1jbAgAAxcdHIwEAgI4ePep0PyAgQDfffLOysrKsNn9/f0kFw4A//OEPys3N1euvv+7UPnXqVLm5ualTp06SZF1tNH36dKd+r732WrHHmX9FzMVXwEybNq3Y27gSinsOykJh52Dz5s3auHFjof0XLlzoNMfX119/rc2bN1tjCg0NVdu2bTVz5kwdPny4wPpHjhxxeXxxcXFauHChDhw4YLV///33Wr58+WXXL81jsaQu/LkZY/T666/Ly8tL99xzj6TzQaaHh4fWr1/vtN7Fj2VXxubh4aEOHTro008/dfoIZlpamubNm6dWrVrJ4XCU8IgAAEBRuCIMAAAoOjpabdu2VZMmTRQcHKxvvvlGH330kdMk4k2aNJEkPf3004qLi5OHh4ceeeQRde7cWe3atdPf/vY37du3T7fddptWrFihTz/9VIMHD1adOnWs9bt3765p06bp6NGjatmypdatW2ddfVOcK2kcDodat26tSZMmKScnRzfeeKNWrFihvXv3XoGzUnzFPQdl4b777tPHH3+sBx54QPHx8dq7d69mzJih6OhonTp1qkD/m2++Wa1atdKAAQOUlZWladOmqWrVqhoxYoTVJykpSa1atVLDhg3Vv39/1a5dW2lpadq4caN++eUXffvtty6Ncdy4cVq2bJnuvvtuPfXUUzp37pxe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2LFq3LhxyQd7jcg/Vx07dnRqP378uNzc3LR27dpyHc++ffvk5uambdu2ObW/8sormj17drmO5ULU4HnUYNm78Fy5u7urevXq6tu3r9LT08tk+4cPH1anTp0kXb31dbUqj7q/3mqe1xKUNerw6q3DWrVqadq0aWW6TVQcaq1k73N574aieFb0AK4H3bt3V3Z2tt555x3Vrl1baWlpWr16tY4ePXrF9+3t7a3w8PArvp+y5nA4tGvXLqc2Nze3Mt9PTk6OvLy8yny75cnT01OrVq3SmjVr1K5du4oeTqEq+sWRGnQdNVh8+ecqLy9P3377rfr27atDhw5p+fLlpd52cR47FV1fV6uKqvtrteZ5LcGVQB265mqrw9zcXCsswNWNWnMN791wWQalcuzYMSPJrF279rL9+vXrZ2644QYTGBho2rVrZ7Zt22YtHzNmjLntttvMu+++ayIjI43D4TAPP/ywycjIMMYYk5CQYCQ53fbu3WvWrFljJJljx44ZY4xJTk42QUFB5vPPPzd169Y1fn5+pnv37ub06dNm9uzZJjIy0lSuXNkMGjTInDt3ztp/ZmamGTZsmImIiDCVKlUyzZs3N2vWrLGW52932bJlpn79+sbf39/ExcWZQ4cOWeO/eHwXrn+h/G0VJT093YSFhZkXX3zRatuwYYPx8vIyq1atMsnJyQX2lZycbIwxRpKZPn266dy5s6lUqZIZM2aMMcaYhQsXmttvv934+PiYm266yYwdO9bk5ORY25dkZsyYYeLj442fn5+pX7+++eqrr8xPP/1k2rRpYypVqmRiYmLM7t27ncZanO1+8sknxhhj2rVrZxITEwsca/5xXepc9e/f3zRv3txqz3/cXXiODxw4YB566CETFBRkqlSpYu6//36zd+9ea3lOTo4ZNGiQCQoKMsHBwWbEiBGmd+/epkuXLlafpUuXmrvuusvqEx8f73TMF5/3Nm3aGGPOPz7ztzNz5kxTrVo1k5ub63Qs999/v+nbt2+xz11xUYPUYHnU4IVefPFF4+7ubs6cOWNyc3PNuHHjzI033mi8vb3NbbfdZpYuXWr1zcrKMomJiSY8PNz4+PiYmjVrmr///e+Fju9qrK+rVXHqnpo3BbbFawnKEnVYcXXYpk0b85e//MVp+126dDEJCQnW8ovHdeEYPv30UxMVFWU8PDzM3r17zddff21iY2NN1apVjcPhMK1btzYpKSlO27/w9Qrli1or/ftc3rvhYgRhpZSTk2MCAgLM4MGDTWZmZpH9YmNjTefOnc2WLVvMjz/+aIYNG2aqVq1qjh49aow5X9wBAQGmW7duZvv27Wb9+vUmPDzc/PWvfzXGGHP8+HETExNj+vfvbw4fPmwOHz5szp07V+iTk5eXl7n33nvN1q1bzbp160zVqlVNhw4dTI8ePcyOHTvM559/bry9vc38+fOt8T3xxBPmzjvvNOvXrze7d+82kydPNj4+PubHH3902m5sbKzZsmWLSUlJMVFRUeaxxx4zxhhz8uRJ06NHD9OxY0drfFlZWcaY8y/G+S/M+du61C/hxhizePFi4+XlZbZs2WIyMjJM7dq1zZAhQ4wxxpw5c8YMGzbM3Hrrrda+zpw5Y4w5/2QUGhpq3n77bbNnzx6zf/9+s379euNwOMzs2bPNnj17zIoVK0ytWrXM2LFjrf1JMjfeeKP54IMPzK5du0zXrl1NrVq1TPv27c2yZcvMzp07TcuWLU3Hjh2tdYq73fwnyrlz55oqVao4PU6mTJliatWqZfLy8go9D/nn6tdffzV+fn5mwYIFxpiCb5qys7NNVFSUefzxx81///tfs3PnTvPYY4+ZevXqWT+HCRMmmODgYPPxxx+b77//3vz5z382DofD6ZeXjz76yPzrX/8yP/30k/nPf/5jOnfubBo2bGg9cX/99ddGklm1apU5fPiw9fi98Mn+999/N97e3k7BwtGjR53ainPuiosapAbLowYvNGXKFCPJZGRkmClTphiHw2Hef/9988MPP5gRI0YYLy8v6+c2efJkU6NGDbN+/Xqzb98+8+WXX5p58+YVOr6rsb6uVsWpe2o+wdoXryW4EqjDiqvDywVhR48eNdWrVzfjx4+3xnXh8dx5551mw4YN5ocffjCnT582q1evNnPmzDHff/+92blzp+nXr58JCwuzQhJjCMIqErVW+ve5vHfDxQjCysBHH31kqlSpYnx9fc2dd95pRo4cab799ltr+ZdffmkcDkeBJ646deqYmTNnGmPOPzlVqlTJ6QVn+PDhpkWLFtb9wl70CntykuT0l9c//elPplKlSubkyZNWW1xcnPnTn/5kjDFm//79xsPDw/z6669O277nnnvMyJEji9xuUlKSCQsLs+5fWPAX6tWrl3nuuees+/nb8vf3d7pd+AuuMcY89dRTpm7duuaxxx4zDRs2dDp/+X/VuJgkM3jw4ALHcWGKb4wxc+bMMdWqVXNab9SoUdb9jRs3Gklm1qxZVtv7779vfH19Xd5u/hPl2bNnTZUqVcwHH3xgLW/UqNEln+AufCJ/7rnnTN26dU1OTk6BN01z5swx9erVc/plPisry/j5+Znly5cbY4wJCwszkydPtpafO3fO1KxZs9CfWb4jR44YSWb79u3GGGP27t1rJJn//Oc/Tv0u/tl36dLFPP7449b9mTNnmoiICOuXoOKcO1dQg+dRg1e2Bo0x5scffzR169Y1TZs2NcYYExER4XTlnDHGNGvWzDz11FPGGGMGDRpk2rdvX2TQduH4rtb6ulpdqu6p+YI1z2sJrgTqsGLq8HJBmDHGREZGmqlTpzr1yT+eC68UKkxubq4JDAw0n3/+udVGEFaxqLWS1ZoxvHdD4ZgjrAx0795d8fHx+vLLL7Vp0yYtXbpUkyZN0ltvvaU+ffro22+/1alTp1S1alWn9c6ePas9e/ZY92vVqqXAwEDrfrVq1Uo0qV+lSpVUp04d635YWJhq1aqlgIAAp7b8bW/fvl25ubmqW7eu03aysrKcxnzxdos7vnfffbdAW2BgoLZu3erU5ufn53T/5ZdfVoMGDbRgwQKlpKTIx8fnsvuSpKZNmzrd//bbb7Vhwwa9+OKLVltubq4yMzN15swZVapUSZLUqFEja3lYWJgkqWHDhk5tmZmZysjIkMPhKPZ28/n6+qpXr156++231aNHD23dulXfffedPvvss2Id17PPPquZM2da6198jLt373Z6/EhSZmam9uzZoxMnTigtLU3Nmze3lnl4eKhJkybKy8uz2n766SeNHj1amzdv1m+//WYtO3DggBo0aFCscUpSz5491b9/f02fPl0+Pj6aO3euHnnkEWsOClfP3eVQg5dGDZ5X0ho8ceKEAgIClJeXp8zMTLVq1UpvvfWWMjIydOjQId11111O/e+66y59++23ks5P7HrvvfeqXr166tixo+677z516NDhsufwUsq7vq5Wl6r706dPU/NF4LUEZYk6LNqVqsPS8vb2dnq9laS0tDSNGjVKa9euVXp6unJzc3XmzBkdOHCg1PtD2aDWilZYrfHeDZdDEFZGfH19de+99+ree+/V888/ryeeeEJjxoxRnz59dOrU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", "text/plain": [ "
" ] @@ -464,18 +472,18 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'OriginalTweet': '@MeNyrbie @Phil_Gahan @Chrisitv https://t.co/iFz9FAn2Pa and https://t.co/xX6ghGFzCC and https://t.co/I2NlzdxNo8',\n", - " 'Location': 'London',\n", - " 'Sentiment': 'Neutral'}" + "{'OriginalTweet': 'TRENDING: New Yorkers encounter empty supermarket shelves (pictured, Wegmans in Brooklyn), sold-out online grocers (FoodKick, MaxDelivery) as #coronavirus-fearing shoppers stock up https://t.co/Gr76pcrLWh https://t.co/ivMKMsqdT1',\n", + " 'Location': 'NYC',\n", + " 'Sentiment': 'Extremely Negative'}" ] }, - "execution_count": 13, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -486,18 +494,18 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'OriginalTweet': 'I hate grocery shopping in general but I swear IÂ\\x92m doing it online next shop, can not deal with the swathes of panic buyers at all! #COVID?19 #coronavirus #coronavirusuk #anxiety #panicbuyinguk #morons',\n", - " 'Location': 'Portsmouth, England',\n", - " 'Sentiment': 'Extremely Negative'}" + "{'OriginalTweet': '@NileshShah68 I have summarized the most important points from the paper in this thread:\\r\\r\\nhttps://t.co/dTZg4vg8VM',\n", + " 'Location': 'Hyderabad, India',\n", + " 'Sentiment': 'Positive'}" ] }, - "execution_count": 14, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -520,7 +528,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -529,18 +537,18 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'OriginalTweet': 'menyrbie philgahan chrisitv',\n", - " 'Location': 'london',\n", - " 'Sentiment': 'Neutral'}" + "{'OriginalTweet': 'trending new yorkers encounter empty supermarket shelves pictured wegmans brooklyn soldout online grocers foodkick maxdelivery coronavirusfearing shoppers stock',\n", + " 'Location': 'nyc',\n", + " 'Sentiment': 'Extremely Negative'}" ] }, - "execution_count": 16, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -551,18 +559,18 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'OriginalTweet': 'hate grocery shopping general swear im online next shop deal swathes panic buyers covid coronavirus coronavirusuk anxiety panicbuyinguk morons',\n", - " 'Location': 'portsmouth england',\n", - " 'Sentiment': 'Extremely Negative'}" + "{'OriginalTweet': 'nileshshah summarized important points paper thread',\n", + " 'Location': 'hyderabad india',\n", + " 'Sentiment': 'Positive'}" ] }, - "execution_count": 17, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -581,15 +589,16 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Before Clean : I hate grocery shopping in general but I swear I’m doing it online next shop, can not deal with the swathes of panic buyers at all! #COVID?19 #coronavirus #coronavirusuk #anxiety #panicbuyinguk #morons\n", - "After Clean : hate grocery shopping general swear im online next shop deal swathes panic buyers covid coronavirus coronavirusuk anxiety panicbuyinguk morons\n" + "Before Clean : @NileshShah68 I have summarized the most important points from the paper in this thread:\n", + "https://t.co/dTZg4vg8VM\n", + "After Clean : nileshshah summarized important points paper thread\n" ] } ], @@ -602,20 +611,391 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Export refiend data into Datumaro format\n", + "## Convert Datumaro dataset into PyTorch dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "train_iter = iter([value.media.data[\"OriginalTweet\"] for value in result])" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-10-16 16:36:27.631387: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", + "2024-10-16 16:36:27.645753: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", + "2024-10-16 16:36:27.649957: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n", + "2024-10-16 16:36:27.659912: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2024-10-16 16:36:28.583817: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n" + ] + } + ], + "source": [ + "from datumaro.plugins.framework_converter import FrameworkConverter\n", + "from torchtext.data.utils import get_tokenizer\n", + "from torchtext.vocab import build_vocab_from_iterator\n", + "\n", + "tokenizer = get_tokenizer(\"basic_english\")\n", + "\n", + "\n", + "def yield_tokens(data_iter):\n", + " for _, text in data_iter:\n", + " yield tokenizer(text)\n", + "\n", + "\n", + "vocab = build_vocab_from_iterator(train_iter, specials=[\"\"])\n", + "vocab.set_default_index(vocab[\"\"])\n", + "\n", + "train_dataset = FrameworkConverter(result, subset=\"train\", task=\"tabular\")\n", + "dm_torch_train_dataset = train_dataset.to_framework(\n", + " framework=\"torch\", target={\"input\": \"OriginalTweet\"}, tokenizer=tokenizer, vocab=vocab\n", + ")\n", + "val_dataset = FrameworkConverter(result, subset=\"test\", task=\"tabular\")\n", + "dm_torch_val_dataset = val_dataset.to_framework(\n", + " framework=\"torch\", target={\"input\": \"OriginalTweet\"}, tokenizer=tokenizer, vocab=vocab\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Modeling\n", "\n", - "We can export the refined data in the Datumaro format. Additionally, it is possible to export the data in various other formats. For more details, please refer to this [link](https://openvinotoolkit.github.io/datumaro/latest/docs/command-reference/context/export.html#export).\n" + "- Showcase how to use your tool for tasks such as feature extraction, model training, or evaluation on the dataset.\n", + "- Compare it with standard methods to show its advantages." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/sooah/.pyenv/versions/datum/lib/python3.11/site-packages/torch/nn/modules/rnn.py:82: UserWarning: dropout option adds dropout after all but last recurrent layer, so non-zero dropout expects num_layers greater than 1, but got dropout=0.5 and num_layers=1\n", + " warnings.warn(\"dropout option adds dropout after all but last \"\n" + ] + } + ], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "\n", + "# Define a simple RNN-based model for text classification\n", + "\n", + "\n", + "class SentimentRNN(nn.Module):\n", + " def __init__(self, vocab_size, embed_size, hidden_size, output_size, num_layers=1, dropout=0.5):\n", + " super(SentimentRNN, self).__init__()\n", + " self.embedding = nn.Embedding(vocab_size, embed_size)\n", + " self.rnn = nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True, dropout=dropout)\n", + " self.fc = nn.Linear(hidden_size, output_size)\n", + "\n", + " def forward(self, x):\n", + " x = self.embedding(x)\n", + " _, (hidden, _) = self.rnn(x)\n", + " out = self.fc(hidden[-1])\n", + " return out\n", + "\n", + "\n", + "# Example: Model initialization\n", + "vocab_size = len(vocab) # This should be the size of your vocabulary\n", + "embed_size = 128\n", + "hidden_size = 256\n", + "output_size = 5 # Assume we have 3 sentiment classes: positive, neutral, negative\n", + "\n", + "model = SentimentRNN(vocab_size, embed_size, hidden_size, output_size)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ - "save_path = \"/home/sooah/data/refined_corona_nlp\"\n", - "result.export(save_path, \"datumaro\", save_media=True)" + "import numpy as np\n", + "from torch.utils.data import DataLoader\n", + "\n", + "# Define Loss and Optimizer\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = optim.Adam(model.parameters(), lr=0.001)\n", + "\n", + "\n", + "def custom_collate_fn(batch):\n", + " # Separate inputs and outputs\n", + " inputs, outputs = zip(*batch)\n", + "\n", + " # Find the maximum length in the inputs and outputs\n", + " max_input_length = max(len(input_) for input_ in inputs)\n", + "\n", + " # Pad all inputs and outputs to the maximum length\n", + " padded_inputs = [\n", + " np.pad(input_, (0, max_input_length - len(input_)), mode=\"constant\") for input_ in inputs\n", + " ]\n", + "\n", + " # Convert to tensors\n", + " padded_inputs = torch.tensor(padded_inputs, dtype=torch.long)\n", + " padded_outputs = torch.stack(outputs) # Assuming labels are integers for classification\n", + "\n", + " return padded_inputs, padded_outputs\n", + "\n", + "\n", + "# Create DataLoader for your dataset\n", + "train_loader = DataLoader(\n", + " dm_torch_train_dataset, batch_size=32, shuffle=True, collate_fn=custom_collate_fn\n", + ")\n", + "val_loader = DataLoader(dm_torch_val_dataset, batch_size=32, collate_fn=custom_collate_fn)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_633257/52718613.py:18: UserWarning: Creating a tensor from a list of numpy.ndarrays is extremely slow. Please consider converting the list to a single numpy.ndarray with numpy.array() before converting to a tensor. (Triggered internally at ../torch/csrc/utils/tensor_new.cpp:261.)\n", + " padded_inputs = torch.tensor(padded_inputs, dtype=torch.long)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1, Loss: 1.590895850211382 | Validation Loss: 1.5764841102063656\n", + "Epoch 2, Loss: 1.5879455283284187 | Validation Loss: 1.5764116831123829\n", + "Epoch 3, Loss: 1.581930335611105 | Validation Loss: 1.5686759762465954\n", + "Epoch 4, Loss: 1.5809518098831177 | Validation Loss: 1.5715479329228401\n", + "Epoch 5, Loss: 1.5830248109996319 | Validation Loss: 1.5709160640835762\n" + ] + } + ], + "source": [ + "# Training Loop\n", + "def train(model, train_loader, val_loader, criterion, optimizer, num_epochs=100):\n", + " model.train()\n", + " train_losses = []\n", + " val_losses = []\n", + " for epoch in range(num_epochs):\n", + " running_loss = 0.0\n", + " for batch in train_loader:\n", + " inputs, labels = batch\n", + " outputs = model(inputs)\n", + "\n", + " loss = criterion(outputs, labels)\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " running_loss += loss.item()\n", + " # print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader)}')\n", + " train_losses.append(running_loss)\n", + "\n", + " # Validation Loop (optional)\n", + " model.eval()\n", + " val_loss = 0.0\n", + " with torch.no_grad():\n", + " for batch in val_loader:\n", + " inputs, labels = batch\n", + " outputs = model(inputs)\n", + " loss = criterion(outputs, labels)\n", + " val_loss += loss.item()\n", + " val_losses.append(val_loss)\n", + "\n", + " if epoch % 5 == 0:\n", + " print(\n", + " f\"Epoch {epoch+1}, Loss: {running_loss/len(train_loader)} | Validation Loss: {val_loss/len(val_loader)}\"\n", + " )\n", + " return train_losses, val_losses\n", + "\n", + "\n", + "# Run the training\n", + "train_losses, val_losses = train(model, train_loader, val_loader, criterion, optimizer)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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jxo1ajB16lTuvfdeLBYLX375ZbkOayrKzJkz+fPPP+nfvz933HGH/QtWhw4d7EOQCtKmTRtCQ0N5+OGHOX78OAEBAfz4448XNbdl9OjR9O/fn8cff5wjR47Qrl07fvrpp2LP1/Hz82Ps2LH2eUk5h9oBXHnllfz000+MGzeOK664gvDwcN5//33atWtHQkJCse5lW+/ppZde4sorr2TUqFFs3bqVRYsW5eodst131qxZ3HTTTfTr14+dO3fy9ddf55lrEhoaSlBQEO+//z7+/v74+vrSu3fvfOe7jB49miFDhvDUU09x5MgROnfuzJ9//smvv/7K/fffn6tIQ2n466+/SElJybN/7NixTJ8+nQ8++IBp06bx77//0rRpU3744QfWrl3L7Nmz7b0bt956K9HR0Vx66aU0atSIiIgI5syZQ5cuXezzZ9q1a8fgwYPp3r07NWvWZPPmzfzwww+5ihfk5/HHH+fbb79l5MiR3HvvvdSsWZPPP/+c8PBwfvzxxzzzpa699lqmTJnCu+++y/DhwwkKCsr1+iOPPMKCBQu48sor7aWvExMT2blzJz/88ANHjhzJ83sujuPHj/PVV1/l2W/7DNt4eXmxePFipk6dSu/evVm0aBF//PEHTz75pH2un6OfhRYtWvDUU0/x/PPPM2DAAMaPH4+npyebNm2iQYMGvPTSS8V6DyX9XYmIE5R/QT0RqQ4KKgHevn37fI9fu3at0adPH8Pb29to0KCB8eijjxpLliwxAGP58uX24woqAZ5fuWUuKEldUAnwu+66K8+5TZo0yVWS2jAM46+//jK6du1qeHh4GKGhocbHH39sPPTQQ4aXl1cBP4Vse/bsMYYOHWr4+fkZtWvXNm677TZ7Semc5aunTp1q+Pr65jk/v9ijoqKMG264wQgICDACAwONG264wdi6davDJcBt/vjjDwMw6tevn2+Z4xdffNFo0qSJ4enpaXTt2tX4/fff8/weDKPoEuCGYRiZmZnGc889Z9SvX9/w9vY2Bg8ebOzatSvPzzslJcV46KGH7Mf179/fWL9+vTFo0CBj0KBBue7766+/Gu3atbOXY7e99/xijI+PNx544AGjQYMGhru7u9GyZUvjtddey1XO2fZeHP1cXMj2mSzo8eWXXxqGYRinT582brrpJqN27dqGh4eH0bFjxzy/tx9++MEYNmyYUadOHcPDw8No3LixMWPGDOPkyZP2Y1544QWjV69eRlBQkOHt7W20adPG+O9//2ukpaUVGqdhGEZYWJhx9dVXG0FBQYaXl5fRq1cv4/fff8/32Li4OMPb2ztP6fKc4uPjjSeeeMJo0aKF4eHhYdSuXdvo16+f8frrr9vjcaRE+oUKKwGe83ds+/MTFhZmDBs2zPDx8THq1q1rPPvss3k+245+FgzDMD799FOja9euhqenp1GjRg1j0KBBxtKlS3PFl19p7ws/rxfzuxKR8mUxjAr0X7UiIpXM2LFjVdJXpIKYNm0aP/zwQ7F7OUVELqQ5SSIiDkpOTs71/ODBgyxcuJDBgwc7JyAREREpE5qTJCLioObNmzNt2jSaN29OREQE7733Hh4eHjz66KPODk1ERERKkZIkEREHjRgxgm+//ZZTp07h6elJ3759efHFF/MsjioiIiKVm+YkiYiIiIiI5KA5SSIiIiIiIjkoSRIREREREcmhys9JslqtnDhxAn9/fywWi7PDERERERERJzEMg/j4eBo0aJBn0eycqnySdOLECUJCQpwdhoiIiIiIVBCRkZE0atSowNerfJLk7+8PmD+IgIAAJ0cjIiIiIiLOEhcXR0hIiD1HKEiVT5JsQ+wCAgKUJImIiIiISJHTcFS4QUREREREJAclSSIiIiIiIjkoSRIREREREcmhys9JcoRhGGRkZJCZmensUKSKcXd3x9XV1dlhiIiIiEgxVPskKS0tjZMnT5KUlOTsUKQKslgsNGrUCD8/P2eHIiIiIiIOqtZJktVqJTw8HFdXVxo0aICHh4cWnJVSYxgGZ8+e5dixY7Rs2VI9SiIiIiKVRLVOktLS0rBarYSEhODj4+PscKQKCg4O5siRI6SnpytJEhEREakkVLgBcHHRj0HKhnomRURERCofZQciIiIiIiI5KEkSERERERHJQUmSANC0aVNmz57t8PErVqzAYrEQExNTZjGJiIiIiDiDkqRKxmKxFPqYOXNmia67adMmpk+f7vDx/fr14+TJkwQGBpbofo5SMiYiIiIi5a1aV7erjE6ePGlvf/fddzzzzDPs37/fvi/nejyGYZCZmYmbW9G/5uDg4GLF4eHhQb169Yp1joiIiIhIZaCepBwMwyApLcMpD8MwHIqxXr169kdgYCAWi8X+fN++ffj7+7No0SK6d++Op6cna9asISwsjDFjxlC3bl38/Pzo2bMny5Yty3XdC4fbWSwWPv74Y8aNG4ePjw8tW7ZkwYIF9tcv7OGZO3cuQUFBLFmyhLZt2+Ln58eIESNyJXUZGRnce++9BAUFUatWLR577DGmTp3K2LFjS/w7O3/+PDfeeCM1atTAx8eHkSNHcvDgQfvrERERjB49mho1auDr60v79u1ZuHCh/dzJkycTHByMt7c3LVu25LPPPitxLCIiIiJSNagnKYfk9EzaPbPEKffeM2s4Ph6l8+t4/PHHef3112nevDk1atQgMjKSUaNG8d///hdPT0+++OILRo8ezf79+2ncuHGB13nuued49dVXee2115gzZw6TJ08mIiKCmjVr5nt8UlISr7/+Ol9++SUuLi5MmTKFhx9+mK+//hqAV155ha+//prPPvuMtm3b8uabb/LLL78wZMiQEr/XadOmcfDgQRYsWEBAQACPPfYYo0aNYs+ePbi7u3PXXXeRlpbGqlWr8PX1Zc+ePfbetqeffpo9e/awaNEiateuzaFDh0hOTi5xLCIiIiJSNShJqoJmzZrF5Zdfbn9es2ZNOnfubH/+/PPP8/PPP7NgwQLuvvvuAq8zbdo0Jk2aBMCLL77IW2+9xcaNGxkxYkS+x6enp/P+++8TGhoKwN13382sWbPsr8+ZM4cnnniCcePGAfD222/be3VKwpYcrV27ln79+gHw9ddfExISwi+//MLEiRM5evQoEyZMoGPHjgA0b97cfv7Ro0fp2rUrPXr0AMzeNBERERERJUk5eLu7smfWcKfdu7TYvvTbJCQkMHPmTP744w9OnjxJRkYGycnJHD16tNDrdOrUyd729fUlICCAM2fOFHi8j4+PPUECqF+/vv342NhYTp8+Ta9eveyvu7q60r17d6xWa7Hen83evXtxc3Ojd+/e9n21atWidevW7N27F4B7772XO+64gz///JOhQ4cyYcIE+/u64447mDBhAlu2bGHYsGGMHTvWnmyJiIiISCmwWmH1/6DHzeBby9nROExzknKwWCz4eLg55WGxWErtffj6+uZ6/vDDD/Pzzz/z4osvsnr1arZt20bHjh1JS0sr9Dru7u55fj6FJTT5He/oXKuycuutt3L48GFuuOEGdu7cSY8ePZgzZw4AI0eOJCIiggceeIATJ05w2WWX8fDDDzs1XhEREZEqZd1bsPwF+ORyyEx3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RCoTW5nthj3MxV9FRKTUKUkSERGpTCriUDsRkSpGSZKIiEhlYc2EwyvMtpIkEZEyoyRJRESksji5DZLPg2cANOzu7GhERKosJUkiIiKVhW2oXbOB4KoCtSIiZUVJkoiISGURllW0QUPtRETKlJIkERGRyiA1HiL/MdtKkkREypSSJBERkcrgyBqwZkCNZlCzmbOjERGp0pQkiYiIVAYq/S0iUm5KlCRFRkZy7Ngx+/ONGzdy//338+GHH5ZaYCIiIpKDkiQRkXJToiTp+uuvZ/lyc/LoqVOnuPzyy9m4cSNPPfUUs2bNKtUARUREqr3zERB1CCyu0GyAs6MREanySpQk7dq1i169egEwf/58OnTowLp16/j666+ZO3duacYnIiIih7Oq2jXqCV6Bzo1FRKQaKFGSlJ6ejqenJwDLli3jqquuAqBNmzacPHmy9KITERERDbUTESlnJUqS2rdvz/vvv8/q1atZunQpI0aMAODEiRPUqlWrVAMUERGp1qyZcHiF2VaSJCJSLkqUJL3yyit88MEHDB48mEmTJtG5c2cAFixYYB+GJyIiIqXgxFZIiTWH2TXo6uxoRESqBbeSnDR48GDOnTtHXFwcNWrUsO+fPn06Pj4+pRaciIhItWcbatdsELiW6J9tEREpphL1JCUnJ5OammpPkCIiIpg9ezb79++nTp06pRqgiIhItab5SCIi5a5ESdKYMWP44osvAIiJiaF3797873//Y+zYsbz33nulGqCIiEi1lRIHkRvNdugQ58YiIlKNlKjffsuWLbzxxhsA/PDDD9StW5etW7fy448/8swzz3DHHXeUapAiIiJVWmIUnNtvroUUFZa9PR8ORibUDIUaTZ0dpYhItVGiJCkpKQl/f38A/vzzT8aPH4+Liwt9+vQhIiKiVAMUERGp0rZ/B7/eBdb0/F939YA++s9HEZHyVKIkqUWLFvzyyy+MGzeOJUuW8MADDwBw5swZAgICSjVAERGRKismEv54yEyQAkOgdkuo1cLsOarVAmo1h8DGKtggIlLOSvS37jPPPMP111/PAw88wKWXXkrfvn0Bs1epa1eVJxURESmSYcDv90NaPIT0hpsWgYurs6MSERHAYhiGUZITT506xcmTJ+ncuTMuLmb9h40bNxIQEECbNm1KNciLERcXR2BgILGxserlEhGRimPbN/DLHeDqCbev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IiIiIiIiJShJEhERERERKUFJkoiIiIiISAlKkkREREREREpQkiQiIiIiIlKCkiQREREREZESlCSJiIiIiIiUoCRJRERERESkBCdHB1DVDMMAID093cGRiIiIiIiII53KCU7lCGdz0SdJGRkZADRs2NDBkYiIiIiISE2QkZGBj4/PWc+bjPOlUbWczWYjISEBLy8vTCaTQ2NJT0+nYcOGxMXF4e3t7dBYpPbQuJGK0tiRitC4kYrQuJGKqu6xYxgGGRkZ1KtXD7P57G8eXfQzSWazmQYNGjg6jFK8vb31PxApN40bqSiNHakIjRupCI0bqajqHDvnmkE6RYUbRERERERESlCSJCIiIiIiUoKSpGpktVp57rnnsFqtjg5FahGNG6kojR2pCI0bqQiNG6momjp2LvrCDSIiIiIiIuWhmSQREREREZESlCSJiIiIiIiUoCRJRERERESkBCVJIiIiIiIiJShJqkYfffQRjRs3xtXVlS5duhATE+PokKQGmThxIp06dcLLy4vAwECuvfZadu3aVapNbm4uo0ePxs/PD09PT4YOHUpSUpKDIpaa6NVXX8VkMvHII4/Yj2ncyJnEx8dz66234ufnh5ubG61bt2bt2rX284Zh8OyzzxISEoKbmxv9+vUjNjbWgRFLTVBUVMSECRNo0qQJbm5uhIeH8+KLL1KyDpjGjixZsoQhQ4ZQr149TCYTv/76a6nzZRkjKSkpDB8+HG9vb3x9fbn77rvJzMystmdQklRNfvjhBx599FGee+451q9fT9u2bRk4cCBHjx51dGhSQyxevJjRo0ezatUq5s+fT0FBAQMGDCArK8veZuzYscyaNYvp06ezePFiEhISuP766x0YtdQka9as4dNPP6VNmzaljmvcyD+dOHGC7t274+zszNy5c9m+fTtvvfUWderUsbd5/fXXef/99/nkk09YvXo1Hh4eDBw4kNzcXAdGLo722muvMWnSJD788EN27NjBa6+9xuuvv84HH3xgb6OxI1lZWbRt25aPPvrojOfLMkaGDx/Otm3bmD9/PrNnz2bJkiWMHDmyuh4BDKkWnTt3NkaPHm3/uqioyKhXr54xceJEB0YlNdnRo0cNwFi8eLFhGIaRmppqODs7G9OnT7e32bFjhwEYK1eudFSYUkNkZGQYERERxvz5841evXoZY8aMMQxD40bObPz48UaP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T1xcHB4eHgDMmTOHIUOGkJCQQFBQEPXr1+fOO+/kpZdeOmMMJpOJ//u//+PFF18EihMvT09P5s6dy6BBg7j66qvx9/fnyy+/rKLvgoiIOJreSRIRkRqlT58+pZIggLp169r/HR0dXepcdHQ0GzduBGDHjh20bdvWniABdO/eHZvNxq5duzCZTCQkJNC3b99zxtCmTRv7vz08PPD29ubo0aMA3H///QwdOpT169czYMAArr32Wrp161ahZxURkZpJSZKIiNQoHh4epy1/qyxubm5laufs7Fzqa5PJhM1mA2Dw4MEcPHiQOXPmMH/+fPr27cvo0aN58803Kz1eERFxDL2TJCIitcqqVatO+7p58+YANG/enE2bNpGVlWU/v3z5csxmM5GRkXh5edG4cWMWLFhwQTEEBAQwYsQIpkyZwrvvvstnn312Qf2JiEjNopkkERGpUfLy8khMTCx1zMnJyV4cYfr06XTs2JEePXrw3XffERMTwxdffAHA8OHDee655xgxYgTPP/88x44d46GHHuK2224jKCgIgOeff55Ro0YRGBjI4MGDycjIYPny5Tz00ENliu/ZZ5+lQ4cOtGzZkry8PGbPnm1P0kRE5OKgJElERGqUefPmERISUupYZGQkO3fuBIorz02bNo0HHniAkJAQpk6dSosWLQBwd3fnjz/+YMyYMXTq1Al3d3eGDh3K22+/be9rxIgR5Obm8s477zBu3Dj8/f254YYbyhyfi4sLTz31FAcOHMDNzY2ePXsybdq0SnhyERGpKVTdTkREag2TycSMGTO49tprHR2KiIhcxPROkoiIiIiISAlKkkRERERERErQO0kiIlJraIW4iIhUB80kiYiIiIiIlKAkSUREREREpAQlSSIiIiIiIiUoSRIRERERESlBSZKIiIiIiEgJSpJERERERERKUJIkIiIiIiJSgpIkERERERGREv4fipAEobyFDOgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def plot_loss(train_losses, val_losses):\n", + " plt.figure(figsize=(10, 6))\n", + " plt.plot(train_losses, label=\"Training Loss\")\n", + " plt.plot(val_losses, label=\"Validation Loss\")\n", + " plt.xlabel(\"Epochs\")\n", + " plt.ylabel(\"Loss\")\n", + " plt.title(\"Training and Validation Loss over Epochs\")\n", + " plt.legend()\n", + " plt.show()\n", + "\n", + "\n", + "# Assuming you have stored losses in lists\n", + "plot_loss(train_losses, val_losses)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: seaborn in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (0.13.2)\n", + "Requirement already satisfied: numpy!=1.24.0,>=1.20 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from seaborn) (1.26.4)\n", + "Requirement already satisfied: pandas>=1.2 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from seaborn) (2.2.3)\n", + "Requirement already satisfied: matplotlib!=3.6.1,>=3.4 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from seaborn) (3.9.2)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (1.3.0)\n", + "Requirement already satisfied: cycler>=0.10 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (4.54.1)\n", + "Requirement already satisfied: kiwisolver>=1.3.1 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (1.4.7)\n", + "Requirement already satisfied: packaging>=20.0 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (24.1)\n", + "Requirement already satisfied: pillow>=8 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (10.4.0)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (3.2.0)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (2.9.0.post0)\n", + "Requirement already satisfied: pytz>=2020.1 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from pandas>=1.2->seaborn) (2024.2)\n", + "Requirement already satisfied: tzdata>=2022.7 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from pandas>=1.2->seaborn) (2024.2)\n", + "Requirement already satisfied: six>=1.5 in /home/sooah/.pyenv/versions/3.11.9/envs/datum/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib!=3.6.1,>=3.4->seaborn) (1.16.0)\n" + ] + } + ], + "source": [ + "! pip install seaborn" ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxEAAAJwCAYAAAD2uOwtAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABZG0lEQVR4nO3dd3gUZdvG4WsTyIaEFJKQQgtNEpCOCJGqIEVUEAsoShF7QAFBDSpNJVgQRGm+SJGi2MBXRBBBQJSOkSoCIqgQCCAJCSmQ7PcHL/vtGmAzkOyk/E6POQ73mdmZe3ch7J1rnhmLzWazCQAAAADyyMPsAgAAAAAULTQRAAAAAAyhiQAAAABgCE0EAAAAAENoIgAAAAAYQhMBAAAAwBCaCAAAAACG0EQAAAAAMIQmAgAAAIAhNBEAcAn79u1Thw4dFBAQIIvFosWLF+fr/v/44w9ZLBbNnj07X/dblLVt21Zt27Y1uwwAQB7QRAAotA4cOKDHH39c1atXl7e3t/z9/dWiRQu98847Sk9PL9Bj9+nTRzt27NBrr72muXPn6oYbbijQ47lT3759ZbFY5O/vf8n3cd++fbJYLLJYLHrrrbcM7//IkSMaNWqUEhIS8qFaAEBhVMrsAgDgUr7++mvde++9slqt6t27t+rWrausrCytW7dOw4YN065du/T+++8XyLHT09O1fv16vfjiixowYECBHCMyMlLp6ekqXbp0gezflVKlSuns2bP66quvdN999zmtmz9/vry9vZWRkXFV+z5y5IhGjx6tqlWrqmHDhnl+3rfffntVxwMAuB9NBIBC5+DBg+rZs6ciIyO1atUqRURE2NfFxsZq//79+vrrrwvs+ElJSZKkwMDAAjuGxWKRt7d3ge3fFavVqhYtWuijjz7K1UQsWLBAXbp00eeff+6WWs6ePSsfHx95eXm55XgAgGvH6UwACp033nhDqamp+uCDD5waiItq1qypZ555xv74/PnzeuWVV1SjRg1ZrVZVrVpVw4cPV2ZmptPzqlatqttvv13r1q3TjTfeKG9vb1WvXl0ffvihfZtRo0YpMjJSkjRs2DBZLBZVrVpV0oXTgC7+v6NRo0bJYrE4ja1YsUItW7ZUYGCgypYtq6ioKA0fPty+/nJzIlatWqVWrVrJ19dXgYGB6tq1q/bs2XPJ4+3fv199+/ZVYGCgAgIC1K9fP509e/byb+y/PPDAA/rmm290+vRp+9jmzZu1b98+PfDAA7m2P3XqlIYOHap69eqpbNmy8vf3V+fOnfXLL7/Yt1m9erWaNm0qSerXr5/9tKiLr7Nt27aqW7eutm7dqtatW8vHx8f+vvx7TkSfPn3k7e2d6/V37NhR5cqV05EjR/L8WgEA+YsmAkCh89VXX6l69eq66aab8rT9I488ohEjRqhx48aaMGGC2rRpo/j4ePXs2TPXtvv379c999yjW2+9VePHj1e5cuXUt29f7dq1S5LUvXt3TZgwQZJ0//33a+7cuZo4caKh+nft2qXbb79dmZmZGjNmjMaPH68777xTP/744xWf991336ljx446fvy4Ro0apSFDhuinn35SixYt9Mcff+Ta/r777tOZM2cUHx+v++67T7Nnz9bo0aPzXGf37t1lsVj0xRdf2McWLFig6OhoNW7cONf2v//+uxYvXqzbb79db7/9toYNG6YdO3aoTZs29i/0tWvX1pgxYyRJjz32mObOnau5c+eqdevW9v2cPHlSnTt3VsOGDTVx4kTdfPPNl6zvnXfeUfny5dWnTx9lZ2dLkqZPn65vv/1W7777ripUqJDn1woAyGc2AChEkpOTbZJsXbt2zdP2CQkJNkm2Rx55xGl86NChNkm2VatW2cciIyNtkmxr1661jx0/ftxmtVptzz77rH3s4MGDNkm2N99802mfffr0sUVGRuaqYeTIkTbHH6cTJkywSbIlJSVdtu6Lx5g1a5Z9rGHDhrbQ0FDbyZMn7WO//PKLzcPDw9a7d+9cx3v44Yed9nnXXXfZgoODL3tMx9fh6+trs9lstnvuucfWrl07m81ms2VnZ9vCw8Nto0ePvuR7kJGRYcvOzs71OqxWq23MmDH2sc2bN+d6bRe1adPGJsk2bdq0S65r06aN09jy5cttkmyvvvqq7ffff7eVLVvW1q1bN5evEQBQsEgiABQqKSkpkiQ/P788bb906VJJ0pAhQ5zGn332WUnKNXeiTp06atWqlf1x+fLlFRUVpd9///2qa/63i3MpvvzyS+Xk5OTpOUePHlVCQoL69u2roKAg+3j9+vV166232l+noyeeeMLpcatWrXTy5En7e5gXDzzwgFavXq3ExEStWrVKiYmJlzyVSbowj8LD48I/G9nZ2Tp58qT9VK1t27bl+ZhWq1X9+vXL07YdOnTQ448/rjFjxqh79+7y9vbW9OnT83wsAEDBoIkAUKj4+/tLks6cOZOn7Q8dOiQPDw/VrFnTaTw8PFyBgYE6dOiQ03iVKlVy7aNcuXL6559/rrLi3Hr06KEWLVrokUceUVhYmHr27KlPPvnkig3FxTqjoqJyratdu7ZOnDihtLQ0p/F/v5Zy5cpJkqHXctttt8nPz08LFy7U/Pnz1bRp01zv5UU5OTmaMGGCrrvuOlmtVoWEhKh8+fLavn27kpOT83zMihUrGppE/dZbbykoKEgJCQmaNGmSQkND8/xcAEDBoIkAUKj4+/urQoUK2rlzp6Hn/Xti8+V4enpectxms131MS6er39RmTJltHbtWn333Xd66KGHtH37dvXo0UO33nprrm2vxbW8lousVqu6d++uOXPmaNGiRZdNISRp7NixGjJkiFq3bq158+Zp+fLlWrFiha6//vo8Jy7ShffHiJ9//lnHjx+XJO3YscPQcwEABYMmAkChc/vtt+vAgQNav369y20jIyOVk5Ojffv2OY0fO3ZMp0+ftl9pKT+UK1fO6UpGF/077ZAkDw8PtWvXTm+//bZ2796t1157TatWrdL3339/yX1frHPv3r251v36668KCQmRr6/vtb2Ay3jggQf0888/68yZM5ecjH7RZ599pptvvlkffPCBevbsqQ4dOqh9+/a53pO8NnR5kZaWpn79+qlOnTp67LHH9MYbb2jz5s35tn8AwNWhiQBQ6Dz33HPy9fXVI488omPHjuVaf+DAAb3zzjuSLpyOIynXFZTefvttSVKXLl3yra4aNWooOTlZ27dvt48dPXpUixYtctru1KlTuZ578aZr/77s7EURERFq2LCh5syZ4/SlfOfOnfr222/tr7Mg3HzzzXrllVf03nvvKTw8/LLbeXp65ko5Pv30U/39999OYxebnUs1XEY9//zzOnz4sObMmaO3335bVatWVZ8+fS77PgIA3IObzQEodGrUqKEFCxaoR48eql27ttMdq3/66Sd9+umn6tu3rySpQYMG6tOnj95//32dPn1abdq00aZNmzRnzhx169btspcPvRo9e/bU888/r7vuuktPP/20zp49q6lTp6pWrVpOE4vHjBmjtWvXqkuXLoqMjNTx48c1ZcoUVapUSS1btrzs/t9880117txZMTEx6t+/v9LT0/Xuu+8qICBAo0aN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hEskEQAAAAAMIYkAAAAAHDAnwjWSCAAAAACGkEQAAAAADggiXCOJAAAAAGAISQQAAADggDkRrpFEAAAAADCEJAIAAABwwH0iXCOJAAAAAGAISQQAAADggDkRrpFEAAAAADCEJAIAAABwQA7hGkkEAAAAUASsXbtWd9xxhypUqCCLxaLFixc7re/bt68sFovT0qlTJ6dtTp06pV69esnf31+BgYHq37+/UlNTDddCEwEAAAA48LBY3LYYkZaWpgYNGmjy5MmX3aZTp046evSoffnoo4+c1vfq1Uu7du3SihUrtGTJEq1du1aPPfaY4feI05kAAACAIqBz587q3LnzFbexWq0KDw+/5Lo9e/Zo2bJl2rx5s2644QZJ0rvvvqvbbrtNb731lipUqJDnWkgiAAAAAJNkZmYqJSXFacnMzLzq/a1evVqhoaGKiorSk08+qZMnT9rXrV+/XoGBgfYGQpLat28vDw8Pbdy40dBxrqqJ+OGHH/Tggw8qJiZGf//9tyRp7ty5Wrdu3dXsDgAAACg0LBb3LfHx8QoICHBa4uPjr6ruTp066cMPP9TKlSv1+uuva82aNercubOys7MlSYmJiQoNDXV6TqlSpRQUFKTExERDxzJ8OtPnn3+uhx56SL169dLPP/9s75SSk5M1duxYLV261OguAQAAgBIpLi5OQ4YMcRqzWq1Xta+ePXva/79evXqqX7++atSoodWrV6tdu3bXVOe/GU4iXn31VU2bNk3/+c9/VLp0aft4ixYttG3btnwtDgAAAHC3f1/hqCAXq9Uqf39/p+Vqm4h/q169ukJCQrR//35JUnh4uI4fP+60zfnz53Xq1KnLzqO4HMNNxN69e9W6detc4wEBATp9+rTR3QEAAAAoAH/99ZdOnjypiIgISVJMTIxOnz6trVu32rdZtWqVcnJy1KxZM0P7Nnw6U3h4uPbv36+qVas6ja9bt07Vq1c3ujsAAACgUDF45VW3SU1NtacKknTw4EElJCQoKChIQUFBGj16tO6++26Fh4frwIEDeu6551SzZk117NhRklS7dm116tRJjz76qKZNm6Zz585pwIAB6tmzp6ErM0lXkUQ8+uijeuaZZ7Rx40ZZLBYdOXJE8+fP19ChQ/Xkk08a3R0AAACAPNiyZYsaNWqkRo0aSZKGDBmiRo0aacSIEfL09NT27dt15513qlatWurfv7+aNGmiH374wen0qPnz5ys6Olrt2rXTbbfdppYtW+r99983XIvhJOKFF15QTk6O2rVrp7Nnz6p169ayWq0aOnSoBg4caLgAAAAAoDAxehM4d2nbtq1sNttl1y9fvtzlPoKCgrRgwYJrrsVwE2GxWPTiiy9q2LBh2r9/v1JTU1WnTh2VLVv2mouBcR/8Z7pWfvet/jj4u6ze3mrQsJEGDR6qqtU4taw4+3jBfM2Z9YFOnEhSrahovTD8ZdWrX9/ssmDA0Ic7qNstDVSrapjSM89p4y+/68V3vtS+Q/8/4S0s2E9jB92lW5pHy8/Xqt/+OK43PliuxSsTJEmtmlynb2c8c8n9t+z1hrbuPuyOl4J88snHH+mzhR/pyJELl06vXrOmHnsiVi1b5Z6HiKJn29bNmjt7pn7ds0snkpL05oR31faW9vb1NptN06e8q8VffKrUM2dUv2EjvfDiSFWJrGpe0cAVXPXN5ry8vFSnTh3deOONNBAm2rplk3rc30sfLvhE096fpfPnzuvJx/or/exZs0tDAVn2zVK99Ua8Hn8qVh9/ukhRUdF68vH+TjeTQeHXqnFNTVu4Vm16v6Xbn3xPpUp5asnUAfLx9rJvM+OV3qpVNVT3DpquG+4dqy9XJWje6w+rQVQlSdKGX35X1fZxTsvML37Uwb9O0EAUQWHhYRo4+FnN/+RzzV/4mW68sbkGD4zVgf37zC4N+SA9PV21oqL0XNzLl1z/4awZWvjRPMW9NEqz5i1UmTI+Gvjko9d00zFcPXfeJ6KostiulIlcws033yzLFV7xqlWrrrmoa5V+zuwKzHPq1Cnd0jpGH8yepyY3NDW7HLcoyn8Br0avnvfq+rr1NPylEZKknJwcdWjXRvc/8JD6P/qYydUVvHJNB5hdQoEIKVdWf64ap/b9J+jHbQckSUk/jtfTYz/WR19vtm/31/ev66VJizV70fpc+yhVykMHlr+mqR+v0bj/LHNb7QXp5KZ3zS7BVG1uaqZBzw7TXXffY3YpbnE+29BXkiKraYPaTkmEzWZT5/at1at3Pz3U52FJUuqZM+p4S0uNHDNWHTp3MbPcAuPvfdW/yy5wT32x223HmtK9jtuOlZ8Mf3oNGzZUgwYN7EudOnWUlZWlbdu2qV69egVRIwxITT0j6cIld1H8nMvK0p7du9Q85ib7mIeHh5o3v0nbf/nZxMpwrfzLekuS/kn+/xRxwy+/654OTVTO30cWi0X3dmwib2sprd1y6d9M396mvoIDfDX3yw1uqRkFJzs7W8uWfq309LOq37Ch2eWggP399186eeKEbmwWYx8r6+en6+vV1/btv5hYWcnlzvtEFFWG50RMmDDhkuOjRo1Samqq4QLS09O1detWBQUFqU4d504sIyNDn3zyiXr37n3Z52dmZuaK+nI8rPl2k46iJCcnR2+OG6uGjRqr5nW1zC4HBeCf0/8oOztbwcHBTuPBwcE6ePB3k6rCtbJYLHpz6D366ecD2n3gqH38wedmau7rD+vImjd07ly2zmZkqceQ/+j3P09ccj99usVoxfo9+vv4aTdVjvy277e96tPrfmVlZaqMj4/Gv/OeatSoaXZZKGAnT1z4O537Z3uITp5IMqMkwKV8y5EefPBBzZw509BzfvvtN9WuXVutW7dWvXr11KZNGx09+v//gCYnJ6tfv35X3Ed8fLwCAgKcljdfj7+q11DUxb86Wvv379Prb1660QNQOE2Mu0/X14xQ7xdmOY2PjL1dgX5l1PnxSWrx4BuaNG+V5r3xsK6vmfta3hVDA3VrTG3NWZz7NCcUHVWrVdPHny/ShwsW6t77emrEiy/owIH9rp8IIF95uHEpqvKt9vXr18vb29vQc55//nnVrVtXx48f1969e+Xn56cWLVro8OG8TwiMi4tTcnKy0zLs+Tij5Rd58a+N0do1qzVj5hyFGbxtOYqOcoHl5OnpmWsS9cmTJxUSEmJSVbgWE56/V7e1qquOj05yShCqVQrRkz3b6PFR87R602/a8dvfGvv+N9q2+7Ae75H7aj0PdW2uk8lpWrJmuxurR34rXdpLVapEqs71dfX04GdVKypaH8370OyyUMCC//fzO/fP9hMKDilvRkmAS4ZPZ+revbvTY5vNpqNHj2rLli16+eVLX3Hgcn766Sd99913CgkJUUhIiL766is99dRTatWqlb7//nv5+vq63IfVmvvUpZI0sdpms2nc2Fe0auUKzZg1VxUrVTa7JBSg0l5eql3nem3csF63tLswIS8nJ0cbN65Xz/sfNLk6GDXh+Xt15y0N1OHRd3ToiPOXh4tXacr517UvsrNtl7x+ee87m2vBkk06fz6n4AqG29lycpSVlWV2GShgFStWUnBIiDZv3KCo6NqSLtyZeNeO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+AMA3KGU2QUAQFHSqVMnzZo1S5mZmVq6dKliY2NVunRpxcXFOW2XlZUlLy+vfDlmUFBQvuwHAID8QhIBAAZYrVaFh4crMjJSTz75pNq3b6///ve/9lOQXnvtNVWoUEFRUVGSpD///FP33XefAgMDFRQUpK5du+qPP/6w7y87O1tDhgxRYGCggoOD9dxzz8lmszkd89+nM2VmZur5559X5cqVZbVaVbNmTX3wwQf6448/dPPNN0uSypUrJ4vFor59+0qScnJyFB8fr2rVqqlMmTJq0KCBPvvsM6fjLF26VLVq1VKZMmV08803O9UJAIAjmggAuAZlypRRVlaWJGnlypXau3evVqxYoSVLlujcuXPq2LGj/Pz89MMPP+jHH39U2bJl1alTJ/tzxo8fr9mzZ2vmzJlat26dTp06pUWLFl3xmL1799ZHH32kSZMmac+ePZo+fbrKli2rypUr6/PPP5ck7d27V0ePHtU777wjSYqPj9eHH36oadOmadeuXRo8eLAefPBBrVmzRtKFZqd79+664447lJCQoEceeUQvvPBCQb1tAIAijtOZAOAq2Gw2rVy5UsuXL9fAgQOVlJQkX19fzZgxw34a07x585STk6MZM2bIYrFIkmbNmqXAwECtXr1aHTp00MSJExUXF6fu3btLkqZNm6bly5df9ri//fabPvnkE61YsULt27eXJFWvXt2+/uKpT6GhoQoMDJR0IbkYO3asvvvuO8XExNifs27dOk2fPl1t2rTR1KlTVaNGDY0fP16SFBUVpR07duj111/Px3cNAFBc0EQAgAFLlixR2bJlde7cOeXk5OiBBx7QqFGjFBsbq3r16jnNg/jll1+0f/9++fn5Oe0jIyNDBw4cUHJyso4ePapmzZrZ15UqVUo33HBDrlOaLkpISJCnp6fatGmT55r379+vs2fP6tZbb3Uaz8rKUqNGjSRJe/bscapDkr3hAADg32giAMCAm2++WVOnTpWXl5cqVKigUqX+/8eor6+v07apqalq0qSJ5s+fn2s/5cuXv6rjlylTxvBzUlNTJUlff/21Klas6LTOarVeVR0AgJKNJgIADPD19VXNmjXztG3jxo21cOFChYaGyt/f/5LbREREaOPGjWrdurUk6fz589q6dasaN258ye3r1aunnJwcrVmzxn46k6OLSUh2drZ9rE6dOrJarTp8+PBlE4zatWvrv//9r9PYhg0bXL9IAECJxMRqACggvXr1UkhIiLp27aoffvhBBw8e1OrVq/X000/rr7/+kiQ988wzGjdunBYvXqxff/1VTz311BXv8VC1alX16dNHDz/8sBYvXmzf5yeffCJJioyMlMVi0ZIlS5SUlKTU1FT5+flp6NChGjx4sObMmaMDBw5o27ZtevfddzVnzhxJ0hNPPKF9+/Zp2LBh2rt3rxYsWKDZs2cX9FsEACiiaCIAoID4+Pho7dq1qlKlirp3767atWurf//+ysjIsCcTzz77rB566CH16dNHMTEx8vPz01133XXF/U6dOlX33HOPnnrqKUVHR+vRRx9VWlqaJKlixYoaPXq0XnjhBYWFhWnAgAGSpFdeeUUvv/yy4uPjVbt2bXXq1Elff/21qlWrJkmqUqWKPv/8cy1evFgNGjTQtGnTNHbs2AJ8dwAARZnFdrnZewAAAABwCSQRAAAAAAyhiQAAAABgCE0EAAAAAENoIgAAAAAYQhMBAAAAwBCaCAAAAACG0EQAAAAAMIQmAgAAAIAhNBEAAAAADKGJAAAAAGAITQQAAAAAQ/4PaYxm2U4COnQAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from sklearn.metrics import confusion_matrix\n", + "import seaborn as sns\n", + "\n", + "\n", + "def plot_confusion_matrix(model, val_loader):\n", + " all_preds = []\n", + " all_labels = []\n", + "\n", + " model.eval()\n", + " with torch.no_grad():\n", + " for batch in val_loader:\n", + " inputs, labels = batch\n", + " outputs = model(inputs)\n", + " preds = torch.argmax(outputs, dim=1)\n", + " all_preds.extend(preds.cpu().numpy())\n", + " all_labels.extend(labels.cpu().numpy())\n", + "\n", + " cm = confusion_matrix(all_labels, all_preds)\n", + "\n", + " plt.figure(figsize=(10, 7))\n", + " sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\")\n", + " plt.xlabel(\"Predicted\")\n", + " plt.ylabel(\"True\")\n", + " plt.title(\"Confusion Matrix\")\n", + " plt.show()\n", + "\n", + "\n", + "plot_confusion_matrix(model, val_loader)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accuracy of class 0: 0.00%\n", + "Accuracy of class 1: 0.62%\n", + "Accuracy of class 2: 95.03%\n", + "Accuracy of class 3: 0.00%\n", + "Accuracy of class 4: 1.68%\n" + ] + } + ], + "source": [ + "def per_class_accuracy(model, val_loader, num_classes):\n", + " class_correct = [0] * num_classes\n", + " class_total = [0] * num_classes\n", + "\n", + " model.eval()\n", + " with torch.no_grad():\n", + " for batch in val_loader:\n", + " inputs, labels = batch\n", + " outputs = model(inputs)\n", + " preds = torch.argmax(outputs, dim=1)\n", + "\n", + " for i in range(len(labels)):\n", + " label = labels[i]\n", + " if preds[i] == label:\n", + " class_correct[label] += 1\n", + " class_total[label] += 1\n", + "\n", + " for i in range(num_classes):\n", + " print(f\"Accuracy of class {i}: {100 * class_correct[i] / class_total[i]:.2f}%\")\n", + "\n", + "\n", + "# Assuming the dataset has 5 classes\n", + "per_class_accuracy(model, val_loader, 5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -634,7 +1014,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/notebooks/22_framework_converter.ipynb b/notebooks/22_framework_converter.ipynb new file mode 100644 index 0000000000..9ba6cdb1b1 --- /dev/null +++ b/notebooks/22_framework_converter.ipynb @@ -0,0 +1,443 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data Framework Convert\n", + "\n", + "In this notebook, we will demonstrate how to leverage the Datumaro to manage datasets and seamlessly integrate them into a PyTorch training pipeline. This tutorial will walk through preparing a dataset using Datumaro and converting it into a format suitable for PyTorch model training and validation.\n", + "\n", + "Specifically, we will:\n", + "\n", + "- Load and inspect a dataset using Datumaro.\n", + "- Convert the dataset to a PyTorch-friendly format.\n", + "- Implement a simple training and validation pipeline using PyTorch.\n", + "\n", + "By the end of this notebook, you will understand how Datumaro can simplify dataset management tasks and improve the efficiency of your deep learning pipelines.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisite\n", + "\n", + "### Download dataset\n", + "\n", + "We will be using a dataset from Kaggle for this tutorial. First, we’ll download the dataset. Please refer to [this guide](20_kaggle_data_import.ipynb) on how to download datasets from Kaggle.\n", + "\n", + "In this notebook, we choose [ananthu017/emotion-detection-fer](https://www.kaggle.com/datasets/ananthu017/emotion-detection-fer/data) dataset as below." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# !kaggle datasets download ananthu017/emotion-detection-fer --unzip --path ./emotion-detection-fer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dataset Preparation\n", + "\n", + "### Import a dataset\n", + "\n", + "\n", + "The dataset is organized in the following directory structure:\n", + "\n", + "```\n", + ".\n", + "├── test\n", + "│ ├── angry\n", + "│ ├── disgusted\n", + "│ ├── fearful\n", + "│ ├── happy\n", + "│ ├── neutral\n", + "│ ├── sad\n", + "│ └── surprised\n", + "└── train\n", + " ├── angry\n", + " ├── disgusted\n", + " ├── fearful\n", + " ├── happy\n", + " ├── neutral\n", + " ├── sad\n", + " └── surprised\n", + "```\n", + "\n", + "In our `emotion_detection_fer` folder, the dataset is divided into two main directories: `train` and `test`. Each of these directories contains subfolders for each emotion category, including \"angry,\" \"disgusted,\" \"fearful,\" \"happy,\" \"neutral,\" \"sad,\" and \"surprised.\" Each subfolder contains images corresponding to that emotion, allowing for organized access during training and testing phases. I used `datumaro` to inspect the dataset directory structure, and it appears that the dataset is well-structured for a classification task." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Detected data format is 'imagenet_with_subset_dirs'\n", + "Dataset\n", + "\tsize=35887\n", + "\tsource_path=/home/sooah/data/emotion-detection-fer\n", + "\tmedia_type=\n", + "\tann_types={}\n", + "\tannotated_items_count=35887\n", + "\tannotations_count=35887\n", + "subsets\n", + "\ttest: # of items=7178, # of annotated items=7178, # of annotations=7178\n", + "\ttrain: # of items=28709, # of annotated items=28709, # of annotations=28709\n", + "infos\n", + "\tcategories\n", + "\t1: ['angry', 'disgusted', 'fearful', 'happy', 'neutral', 'sad', 'surprised']\n", + "\n" + ] + } + ], + "source": [ + "import datumaro as dm\n", + "\n", + "dataset_dir = \"/home/sooah/data/emotion-detection-fer\"\n", + "formats = dm.Dataset.detect(dataset_dir)\n", + "print(f\"Detected data format is '{formats}'\")\n", + "\n", + "dataset = dm.Dataset.import_from(dataset_dir, formats)\n", + "print(dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on the information provided:\n", + "- The total size of the dataset is 35,887 items.\n", + "- The dataset is divided into two subsets:\n", + " - The 'test' subset contains 7,178 items.\n", + " - The 'train' subset contains 28,709 items.\n", + "\n", + "This breakdown gives us insight into the scale of our dataset and the distribution of items across its subsets, with a clear emphasis on a larger training set to enhance model performance.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Convert Datumaro dataset into PyTorch dataset\n", + "\n", + "The process of converting a Datumaro dataset into a PyTorch dataset involves utilizing the `FrameworkConverter` from the Datumaro library. This allows us to seamlessly transform our dataset for compatibility with PyTorch's training and validation pipeline. In the code, we first define a set of transformations using `torchvision.transforms`, specifically converting images to tensor format. We then create PyTorch-compatible datasets for both the training and testing subsets by specifying the respective subset names and the classification task. Finally, we can check the number of items in both datasets to ensure they have been correctly prepared for model training and evaluation. This approach not only streamlines the data preprocessing step but also leverages the robust capabilities of the PyTorch framework for building and deploying deep learning models." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-10-23 15:32:28.371272: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", + "2024-10-23 15:32:28.383616: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", + "2024-10-23 15:32:28.387695: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n", + "2024-10-23 15:32:28.396903: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2024-10-23 15:32:29.470910: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Converted train dataset len is '28709'\n", + "Converted train dataset len is '7178'\n" + ] + } + ], + "source": [ + "from torchvision import transforms\n", + "from datumaro.plugins.framework_converter import FrameworkConverter\n", + "\n", + "transform = transforms.Compose([transforms.ToTensor()])\n", + "\n", + "multi_framework_dataset = FrameworkConverter(dataset, subset=\"train\", task=\"classification\")\n", + "train_dataset = multi_framework_dataset.to_framework(\n", + " framework=\"torch\",\n", + " transform=transform,\n", + ")\n", + "\n", + "multi_framework_dataset = FrameworkConverter(dataset, subset=\"test\", task=\"classification\")\n", + "val_dataset = multi_framework_dataset.to_framework(\n", + " framework=\"torch\",\n", + " transform=transform,\n", + ")\n", + "\n", + "print(f\"Converted train dataset len is '{len(train_dataset)}'\")\n", + "print(f\"Converted train dataset len is '{len(val_dataset)}'\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building the PyTorch Training and Validation Pipeline\n", + "\n", + "### Creating Data Loaders for Efficient Data Handling\n", + "\n", + "In this section, we establish our data loaders for both training and validation datasets, which are essential for efficient data handling during the model training process. By utilizing PyTorch's `DataLoader`, we ensure that our training data is shuffled randomly for better generalization, while the validation data is loaded in a deterministic manner to facilitate accurate performance evaluation. The specified batch size of 4 allows for manageable processing of data during each training iteration. With these loaders in place, we can seamlessly feed our datasets into the training loop for effective model training and validation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training Loader Batches: 449\n", + "Validation Loader Batches: 113\n" + ] + } + ], + "source": [ + "from torch.utils.data import DataLoader\n", + "\n", + "training_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n", + "validation_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)\n", + "\n", + "print(f\"Training Loader Batches: {len(training_loader)}\")\n", + "print(f\"Validation Loader Batches: {len(validation_loader)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Modeling\n", + "\n", + "### Model Architecture Definition\n", + "\n", + "In this section, we define our model architecture by leveraging the pre-trained ResNet-50 model, which is well-suited for image classification tasks. By utilizing transfer learning, we can capitalize on the learned features from the ImageNet dataset, which enhances our model's performance on the emotion detection task. We modify the final fully connected layer to match the number of classes in our specific dataset, ensuring the model outputs predictions relevant to the emotions present in the images. Finally, we transfer the model to the GPU, enabling efficient training and inference processes. This approach helps us build a robust foundation for our emotion detection pipeline." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from torchvision.models import mobilenet_v2\n", + "import torch\n", + "\n", + "model = mobilenet_v2(weights=\"IMAGENET1K_V1\")\n", + "model.features[0] = torch.nn.Conv2d(\n", + " 1, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n", + ")\n", + "# Get the number of input features for the last layer\n", + "num_features = model.classifier[1].in_features\n", + "\n", + "# Create a new classifier layer with the number of classes\n", + "num_classes = len(dataset.categories()[dm.AnnotationType.label])\n", + "model.classifier[1] = torch.nn.Linear(num_features, num_classes)\n", + "\n", + "# Move the model to GPU if available\n", + "model = model.cuda() # If using GPU" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Training and Validation Loop\n", + "\n", + "In this section, we implement the training and validation loop for our emotion detection model. The `top_k_accuracy` function calculates the top-k accuracy for the model predictions, allowing us to evaluate performance more robustly. We define a cross-entropy loss function suitable for multi-class classification tasks and use the Stochastic Gradient Descent (SGD) optimizer to adjust the model's parameters. Throughout the training process, we report the loss for every 100 batches, providing insights into the model's learning progress. After each epoch, we evaluate the model on the validation dataset, calculating the average accuracy to gauge its effectiveness in classifying the emotions." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EPOCH 1:\n", + "\t [TRAIN] batch 100 loss: 1.8547\n", + "\t [TRAIN] batch 200 loss: 1.7383\n", + "\t [TRAIN] batch 300 loss: 1.6348\n", + "\t [TRAIN] batch 400 loss: 1.5933\n", + "\t [VAL] validation accuracy: 40.84%\n", + "EPOCH 2:\n", + "\t [TRAIN] batch 100 loss: 1.4857\n", + "\t [TRAIN] batch 200 loss: 1.4612\n", + "\t [TRAIN] batch 300 loss: 1.4012\n", + "\t [TRAIN] batch 400 loss: 1.3967\n", + "\t [VAL] validation accuracy: 48.21%\n", + "EPOCH 3:\n", + "\t [TRAIN] batch 100 loss: 1.2735\n", + "\t [TRAIN] batch 200 loss: 1.2806\n", + "\t [TRAIN] batch 300 loss: 1.2650\n", + "\t [TRAIN] batch 400 loss: 1.2792\n", + "\t [VAL] validation accuracy: 51.14%\n", + "EPOCH 4:\n", + "\t [TRAIN] batch 100 loss: 1.1394\n", + "\t [TRAIN] batch 200 loss: 1.1445\n", + "\t [TRAIN] batch 300 loss: 1.1760\n", + "\t [TRAIN] batch 400 loss: 1.1557\n", + "\t [VAL] validation accuracy: 52.51%\n", + "EPOCH 5:\n", + "\t [TRAIN] batch 100 loss: 1.0302\n", + "\t [TRAIN] batch 200 loss: 1.0563\n", + "\t [TRAIN] batch 300 loss: 1.0757\n", + "\t [TRAIN] batch 400 loss: 1.0815\n", + "\t [VAL] validation accuracy: 52.39%\n", + "EPOCH 6:\n", + "\t [TRAIN] batch 100 loss: 0.9378\n", + "\t [TRAIN] batch 200 loss: 0.9302\n", + "\t [TRAIN] batch 300 loss: 1.0006\n", + "\t [TRAIN] batch 400 loss: 0.9811\n", + "\t [VAL] validation accuracy: 51.48%\n", + "EPOCH 7:\n", + "\t [TRAIN] batch 100 loss: 0.8105\n", + "\t [TRAIN] batch 200 loss: 0.8475\n", + "\t [TRAIN] batch 300 loss: 0.9001\n", + "\t [TRAIN] batch 400 loss: 0.9265\n", + "\t [VAL] validation accuracy: 54.57%\n", + "EPOCH 8:\n", + "\t [TRAIN] batch 100 loss: 0.7378\n", + "\t [TRAIN] batch 200 loss: 0.7624\n", + "\t [TRAIN] batch 300 loss: 0.8293\n", + "\t [TRAIN] batch 400 loss: 0.8538\n", + "\t [VAL] validation accuracy: 54.17%\n", + "EPOCH 9:\n", + "\t [TRAIN] batch 100 loss: 0.6630\n", + "\t [TRAIN] batch 200 loss: 0.6890\n", + "\t [TRAIN] batch 300 loss: 0.7210\n", + "\t [TRAIN] batch 400 loss: 0.7865\n", + "\t [VAL] validation accuracy: 52.42%\n", + "EPOCH 10:\n", + "\t [TRAIN] batch 100 loss: 0.5968\n", + "\t [TRAIN] batch 200 loss: 0.6473\n", + "\t [TRAIN] batch 300 loss: 0.6737\n", + "\t [TRAIN] batch 400 loss: 0.7167\n", + "\t [VAL] validation accuracy: 55.29%\n" + ] + } + ], + "source": [ + "def top_k_accuracy(output, labels, k=1):\n", + " \"\"\"Compute the top-k accuracy given model output and labels.\"\"\"\n", + " with torch.no_grad():\n", + " batch_size = labels.size(0)\n", + " _, pred = output.topk(k, 1, True, True)\n", + " pred = pred.t()\n", + " correct = pred.eq(labels.view(1, -1).expand_as(pred))\n", + " correct_k = correct[:k].view(-1).float().sum(0, keepdim=True)\n", + " return correct_k.mul_(100.0 / batch_size).item()\n", + "\n", + "\n", + "# Define loss function and optimizer\n", + "loss_fn = torch.nn.CrossEntropyLoss()\n", + "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)\n", + "\n", + "EPOCHS = 10\n", + "for epoch in range(EPOCHS):\n", + " print(f\"EPOCH {epoch + 1}:\")\n", + "\n", + " # Training phase\n", + " model.train()\n", + " running_loss = 0.0\n", + " for i, data in enumerate(training_loader):\n", + " inputs, labels = data\n", + " inputs, labels = inputs.cuda(), labels.cuda()\n", + "\n", + " optimizer.zero_grad()\n", + " outputs = model(inputs)\n", + "\n", + " loss = loss_fn(outputs, labels)\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " # Gather data and report\n", + " running_loss += loss.item()\n", + " if (i + 1) % 100 == 0:\n", + " print(f\"\\t [TRAIN] batch {i + 1} loss: {running_loss / 100:.4f}\")\n", + " running_loss = 0.0\n", + "\n", + " # Validation phase\n", + " model.eval()\n", + " accs = 0.0\n", + " with torch.no_grad():\n", + " for i, vdata in enumerate(validation_loader):\n", + " inputs, labels = vdata\n", + " inputs, labels = inputs.cuda(), labels.cuda()\n", + "\n", + " outputs = model(inputs)\n", + " top1_acc = top_k_accuracy(outputs, labels, k=1)\n", + " accs += top1_acc\n", + "\n", + " avg_accs = accs / (i + 1)\n", + " print(f\"\\t [VAL] validation accuracy: {avg_accs:.2f}%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Model Fine-Tuning and Further Improvements\n", + "\n", + "While MobileNetV2 provided a solid baseline performance for this emotion detection task, further fine-tuning can help improve results. Experimenting with different architectures—such as ResNet or EfficientNet—or adjusting layers and hyperparameters in MobileNetV2 could yield a better fit to the unique characteristics of the dataset. Additionally, applying transfer learning from models pretrained on large face or emotion recognition datasets might enhance the model's ability to capture subtle facial expressions, leading to higher accuracy in emotion detection." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we explored the use of Datumaro for data management, transforming the emotion-detection-fer dataset into a PyTorch-compatible format. This process enabled us to easily handle image-based datasets, including various pre-processing steps and dataset partitioning for training and validation.\n", + "\n", + "Leveraging MobileNetV2, a lightweight yet effective model architecture, we demonstrated its application for facial emotion recognition. MobileNetV2, with its efficient design and lower computational requirements, performed well on the dataset, making it a practical choice for projects that prioritize speed and model efficiency.\n", + "\n", + "Through the completed training and validation pipeline, we showcased how MobileNetV2 can be fine-tuned for specific emotion detection tasks. Datumaro’s robust data management features allowed us to streamline the dataset preparation, ensuring efficient handling and compatibility with PyTorch.\n", + "\n", + "Future improvements could involve experimenting with data augmentation, testing more complex model architectures, or further tuning hyperparameters to optimize accuracy. We hope this notebook serves as a comprehensive guide for leveraging Datumaro and MobileNetV2 in similar emotion detection or classification tasks." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "datum", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements-core.txt b/requirements-core.txt index bb0c7a38a1..a1cf376c06 100644 --- a/requirements-core.txt +++ b/requirements-core.txt @@ -7,7 +7,7 @@ matplotlib>=3.3.1 networkx>=2.6 nibabel>=3.2.1 numpy<2,>=1.23.4 -orjson==3.10.6 +orjson==3.10.12 Pillow>=10.3.0 ruamel.yaml>=0.17.0 shapely>=1.7 @@ -64,3 +64,6 @@ json-stream # TabularValidator nltk + +# torch converter for language +portalocker diff --git a/setup.py b/setup.py index acc6925fdc..91b1b51e8c 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ def parse_requirements(filename=CORE_REQUIREMENTS_FILE): extras_require={ "tf": ["tensorflow"], "tfds": ["tensorflow-datasets<4.9.3"], - "torch": ["torch", "torchvision"], + "torch": ["torch", "torchvision", "torchtext==0.16.0"], "default": DEFAULT_REQUIREMENTS, }, ext_modules=ext_modules, diff --git a/src/datumaro/components/algorithms/hash_key_inference/base.py b/src/datumaro/components/algorithms/hash_key_inference/base.py index 0eb7c6101f..9b9d9a578b 100644 --- a/src/datumaro/components/algorithms/hash_key_inference/base.py +++ b/src/datumaro/components/algorithms/hash_key_inference/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2023-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -21,13 +21,13 @@ def __init__(self, *datasets: Sequence[Dataset]) -> None: @property def model(self): if self._model is None: - self._model = explorer.ExplorerLauncher(model_name="clip_visual_ViT-B_32") + self._model = explorer.ExplorerLauncher(model_name="clip_visual_vit_l_14_336px_int8") return self._model @property def text_model(self): if self._text_model is None: - self._text_model = explorer.ExplorerLauncher(model_name="clip_text_ViT-B_32") + self._text_model = explorer.ExplorerLauncher(model_name="clip_text_vit_l_14_336px_int8") return self._text_model def _compute_hash_key(self, datasets, datasets_to_infer): diff --git a/src/datumaro/components/annotation.py b/src/datumaro/components/annotation.py index c68af00c4a..1a16ef2ed6 100644 --- a/src/datumaro/components/annotation.py +++ b/src/datumaro/components/annotation.py @@ -24,6 +24,7 @@ ) import attr +import cv2 import numpy as np import shapely.geometry as sg from attr import asdict, attrs, field @@ -50,6 +51,7 @@ class AnnotationType(IntEnum): feature_vector = 13 tabular = 14 rotated_bbox = 15 + cuboid_2d = 16 COORDINATE_ROUNDING_DIGITS = 2 @@ -260,8 +262,8 @@ class HashKey(Annotation): @hash_key.validator def _validate(self, attribute, value: np.ndarray): - """Check whether value is a 1D Numpy array having 64 np.uint8 values""" - if value.ndim != 1 or value.shape[0] != 64 or value.dtype != np.uint8: + """Check whether value is a 1D Numpy array having 96 np.uint8 values""" + if value.ndim != 1 or value.shape[0] != 96 or value.dtype != np.uint8: raise ValueError(value) def __eq__(self, other): @@ -1363,6 +1365,224 @@ def wrap(item, **kwargs): return attr.evolve(item, **d) +@attrs(slots=True, init=False, order=False) +class Cuboid2D(Annotation): + """ + Cuboid2D annotation class. This class represents a 3D bounding box defined by its point coordinates + in the following way: + [(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x5, y5), (x6, y6), (x7, y7), (x8, y8)]. + + + 2---3 + /| /| + 1-+-4 | + | 5 + 6 + |/ |/ + 8---7 + + Attributes: + _type (AnnotationType): The type of annotation, set to `AnnotationType.cuboid_2d`. + + Methods: + __init__: Initializes the Cuboid2D with its coordinates. + wrap: Creates a new Cuboid2D instance with updated attributes. + """ + + _type = AnnotationType.cuboid_2d + points = field(default=None) + label: Optional[int] = field( + converter=attr.converters.optional(int), default=None, kw_only=True + ) + z_order: int = field(default=0, validator=default_if_none(int), kw_only=True) + y_3d: float = field(default=None, validator=default_if_none(float), kw_only=True) + + def __init__( + self, + _points: Iterable[Tuple[float, float]], + *args, + **kwargs, + ): + kwargs.pop("points", None) # comes from wrap() + self.__attrs_init__(points=_points, *args, **kwargs) + + @staticmethod + def _get_plane_equation(points): + """Calculates coefficients of the plane equation from three points.""" + x1, y1, z1 = points[0, 0], points[0, 1], points[0, 2] + x2, y2, z2 = points[1, 0], points[1, 1], points[1, 2] + x3, y3, z3 = points[2, 0], points[2, 1], points[2, 2] + a1 = x2 - x1 + b1 = y2 - y1 + c1 = z2 - z1 + a2 = x3 - x1 + b2 = y3 - y1 + c2 = z3 - z1 + a = b1 * c2 - b2 * c1 + b = a2 * c1 - a1 * c2 + c = a1 * b2 - b1 * a2 + d = -a * x1 - b * y1 - c * z1 + return np.array([a, b, c, d]) + + @staticmethod + def _get_denorm(Tr_velo_to_cam_homo): + """Calculates the denormalized vector perpendicular to the image plane. + Args: + Tr_velo_to_cam_homo (np.ndarray): Homogeneous (4x4) LiDAR-to-camera transformation matrix + Returns: + np.ndarray: vector""" + ground_points_lidar = np.array([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]) + ground_points_lidar = np.concatenate( + (ground_points_lidar, np.ones((ground_points_lidar.shape[0], 1))), axis=1 + ) + ground_points_cam = np.matmul(Tr_velo_to_cam_homo, ground_points_lidar.T).T + denorm = -1 * Cuboid2D._get_plane_equation(ground_points_cam) + return denorm + + @staticmethod + def _get_3d_points(dim, location, rotation_y, denorm): + """Get corner points according to the 3D bounding box parameters. + + Args: + dim (List[float]): The dimensions of the 3D bounding box as [l, w, h]. + location (List[float]): The location of the 3D bounding box as [x, y, z]. + rotation_y (float): The rotation angle around the y-axis. + + Returns: + np.ndarray: The corner points of the 3D bounding box. + """ + + c, s = np.cos(rotation_y), np.sin(rotation_y) + R = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]], dtype=np.float32) + l, w, h = dim[2], dim[1], dim[0] + x_corners = [l / 2, l / 2, -l / 2, -l / 2, l / 2, l / 2, -l / 2, -l / 2] + y_corners = [0, 0, 0, 0, -h, -h, -h, -h] + z_corners = [w / 2, -w / 2, -w / 2, w / 2, w / 2, -w / 2, -w / 2, w / 2] + + corners = np.array([x_corners, y_corners, z_corners], dtype=np.float32) + corners_3d = np.dot(R, corners) + + denorm = denorm[:3] + denorm_norm = denorm / np.sqrt(denorm[0] ** 2 + denorm[1] ** 2 + denorm[2] ** 2) + ori_denorm = np.array([0.0, -1.0, 0.0]) + theta = -1 * math.acos(np.dot(denorm_norm, ori_denorm)) + n_vector = np.cross(denorm, ori_denorm) + n_vector_norm = n_vector / np.sqrt(n_vector[0] ** 2 + n_vector[1] ** 2 + n_vector[2] ** 2) + rotation_matrix, j = cv2.Rodrigues(theta * n_vector_norm) + corners_3d = np.dot(rotation_matrix, corners_3d) + corners_3d = corners_3d + np.array(location, dtype=np.float32).reshape(3, 1) + return corners_3d.transpose(1, 0) + + @staticmethod + def _project_to_2d(pts_3d, P): + """Project 3D points to 2D image plane. + + Args: + pts_3d (np.ndarray): The 3D points to project. + P (np.ndarray): The projection matrix. + + Returns: + np.ndarray: The 2D points projected to the image + """ + # Convert to homogeneous coordinates + pts_3d = pts_3d.T + pts_3d_homo = np.vstack((pts_3d, np.ones(pts_3d.shape[1]))) + pts_2d = P @ pts_3d_homo + pts_2d[0, :] = np.divide(pts_2d[0, :], pts_2d[2, :]) + pts_2d[1, :] = np.divide(pts_2d[1, :], pts_2d[2, :]) + pts_2d = pts_2d[:2, :].T + + return pts_2d + + @classmethod + def from_3d( + cls, + dim: np.ndarray, + location: np.ndarray, + rotation_y: float, + P: np.ndarray, + Tr_velo_to_cam: np.ndarray, + ) -> Cuboid2D: + """Creates an instance of Cuboid2D class from 3D bounding box parameters. + + Args: + dim (np.ndarray): 3 scalars describing length, width and height of a 3D bounding box + location (np.ndarray): (x, y, z) coordinates of the middle of the top face. + rotation_y (np.ndarray): rotation along the Y-axis (from -pi to pi) + P (np.ndarray): Camera-to-Image transformation matrix (3x4) + Tr_velo_to_cam (np.ndarray): LiDAR-to-Camera transformation matrix (3x4) + + Returns: + Cuboid2D: Projection points for the given bounding box + """ + Tr_velo_to_cam_homo = np.eye(4) + Tr_velo_to_cam_homo[:3, :4] = Tr_velo_to_cam + denorm = cls._get_denorm(Tr_velo_to_cam_homo) + pts_3d = cls._get_3d_points(dim, location, rotation_y, denorm) + y_3d = np.mean(pts_3d[:4, 1]) + pts_2d = cls._project_to_2d(pts_3d, P) + + return cls(list(map(tuple, pts_2d)), y_3d=y_3d) + + def to_3d(self, P_inv: np.ndarray) -> tuple[np.ndarray, np.ndarray, float]: + """Reconstructs 3D object Velodyne coordinates (dimensions, location and rotation along the Y-axis) + from the given Cuboid2D instance. + + Args: + P_inv (np.ndarray): Pseudo-inverse of Camera-to-Image projection matrix + Returns: + tuple: dimensions, location and rotation along the Y-axis + """ + recon_3d = [] + for idx, coord_2d in enumerate(self.points): + coord_2d = np.append(coord_2d, 1) + coord_3d = P_inv @ coord_2d + if idx < 4: + coord_3d = coord_3d * self.y_3d / coord_3d[1] + else: + coord_3d = coord_3d * recon_3d[idx - 4][0] / coord_3d[0] + recon_3d.append(coord_3d[:3]) + recon_3d = np.array(recon_3d) + + x = np.mean(recon_3d[:, 0]) + z = np.mean(recon_3d[:, 2]) + + yaws = [] + pairs = [(0, 1), (3, 2), (4, 5), (7, 6)] + for p in pairs: + delta_x = recon_3d[p[0]][0] - recon_3d[p[1]][0] + delta_z = recon_3d[p[0]][2] - recon_3d[p[1]][2] + yaws.append(np.arctan2(delta_x, delta_z)) + yaw = np.mean(yaws) + + widths = [] + pairs = [(0, 1), (2, 3), (4, 5), (6, 7)] + for p in pairs: + delta_x = np.sqrt( + (recon_3d[p[0]][0] - recon_3d[p[1]][0]) ** 2 + + (recon_3d[p[0]][2] - recon_3d[p[1]][2]) ** 2 + ) + widths.append(delta_x) + w = np.mean(widths) + + lengths = [] + pairs = [(1, 2), (0, 3), (5, 6), (4, 7)] + for p in pairs: + delta_z = np.sqrt( + (recon_3d[p[0]][0] - recon_3d[p[1]][0]) ** 2 + + (recon_3d[p[0]][2] - recon_3d[p[1]][2]) ** 2 + ) + lengths.append(delta_z) + l = np.mean(lengths) + + heights = [] + pairs = [(0, 4), (1, 5), (2, 6), (3, 7)] + for p in pairs: + delta_y = np.abs(recon_3d[p[0]][1] - recon_3d[p[1]][1]) + heights.append(delta_y) + h = np.mean(heights) + return np.array([h, w, l]), np.array([x, self.y_3d, z]), yaw + + @attrs(slots=True, order=False) class PointsCategories(Categories): """ diff --git a/src/datumaro/components/annotations/matcher.py b/src/datumaro/components/annotations/matcher.py index db9322722a..eb7c874cc4 100644 --- a/src/datumaro/components/annotations/matcher.py +++ b/src/datumaro/components/annotations/matcher.py @@ -35,6 +35,7 @@ "ImageAnnotationMatcher", "HashKeyMatcher", "FeatureVectorMatcher", + "Cuboid2DMatcher", ] @@ -378,3 +379,8 @@ def distance(self, a, b): b = Points([p for pt in b.as_polygon() for p in pt]) return OKS(a, b, sigma=self.sigma) + + +@attrs +class Cuboid2DMatcher(ShapeMatcher): + pass diff --git a/src/datumaro/components/annotations/merger.py b/src/datumaro/components/annotations/merger.py index c1c356f81b..8ff7593a61 100644 --- a/src/datumaro/components/annotations/merger.py +++ b/src/datumaro/components/annotations/merger.py @@ -12,6 +12,7 @@ AnnotationMatcher, BboxMatcher, CaptionsMatcher, + Cuboid2DMatcher, Cuboid3dMatcher, FeatureVectorMatcher, HashKeyMatcher, @@ -210,3 +211,8 @@ class TabularMerger(AnnotationMerger, TabularMatcher): @attrs class RotatedBboxMerger(_ShapeMerger, RotatedBboxMatcher): pass + + +@attrs +class Cuboid2DMerger(_ShapeMerger, Cuboid2DMatcher): + pass diff --git a/src/datumaro/components/dataset.py b/src/datumaro/components/dataset.py index 2652c99a7d..574ec7cc33 100644 --- a/src/datumaro/components/dataset.py +++ b/src/datumaro/components/dataset.py @@ -1023,17 +1023,22 @@ class _MergedStreamDataset(cls): def __init__(self, *sources: IDataset): from datumaro.components.hl_ops import HLOps - self.merged = HLOps.merge(*sources, merge_policy=merge_policy) + self._merged = HLOps.merge(*sources, merge_policy=merge_policy) + self._data = self._merged._data + self._env = env + self._format = DEFAULT_FORMAT + self._source_path = None + self._options = {} def __iter__(self): - yield from self.merged + yield from self._merged @property def is_stream(self): return True def subsets(self) -> Dict[str, DatasetSubset]: - return self.merged.subsets() + return self._merged.subsets() return _MergedStreamDataset(*sources) diff --git a/src/datumaro/components/environment.py b/src/datumaro/components/environment.py index 52080e5eda..150125aedd 100644 --- a/src/datumaro/components/environment.py +++ b/src/datumaro/components/environment.py @@ -275,7 +275,7 @@ def merge(cls, envs: Sequence["Environment"]) -> "Environment": merged = Environment() def _register(registry: PluginRegistry): - merged.register_plugins(plugin for plugin in registry) + merged.register_plugins(list(registry._items.values())) for env in envs: _register(env.extractors) diff --git a/src/datumaro/components/errors.py b/src/datumaro/components/errors.py index c850fcc551..446c16a548 100644 --- a/src/datumaro/components/errors.py +++ b/src/datumaro/components/errors.py @@ -342,6 +342,16 @@ def __str__(self): return f"Item {self.item_id} is repeated in the source sequence." +@define(auto_exc=False) +class PathSeparatorInSubsetNameError(DatasetError): + subset: str = field() + + def __str__(self): + return ( + f"Failed to export the subset '{self.subset}': subset name contains path separator(s)." + ) + + class DatasetQualityError(DatasetError): pass diff --git a/src/datumaro/components/hl_ops/__init__.py b/src/datumaro/components/hl_ops/__init__.py index 54091aa703..22aaa32aa2 100644 --- a/src/datumaro/components/hl_ops/__init__.py +++ b/src/datumaro/components/hl_ops/__init__.py @@ -282,13 +282,14 @@ def merge( merger = get_merger(merge_policy, **kwargs) merged = merger(*datasets) env = Environment.merge( - dataset.env - for dataset in datasets - if hasattr( - dataset, "env" - ) # TODO: Sometimes, there is dataset which is not exactly "Dataset", - # e.g., VocClassificationBase. this should be fixed and every object from - # Dataset.import_from should have "Dataset" type. + [ + dataset.env + for dataset in datasets + if hasattr(dataset, "env") + # TODO: Sometimes, there is dataset which is not exactly "Dataset", + # e.g., VocClassificationBase. this should be fixed and every object from + # Dataset.import_from should have "Dataset" type. + ] ) if report_path: merger.save_merge_report(report_path) diff --git a/src/datumaro/components/merge/intersect_merge.py b/src/datumaro/components/merge/intersect_merge.py index 26677661ea..bb545f950d 100644 --- a/src/datumaro/components/merge/intersect_merge.py +++ b/src/datumaro/components/merge/intersect_merge.py @@ -19,6 +19,7 @@ AnnotationMerger, BboxMerger, CaptionsMerger, + Cuboid2DMerger, Cuboid3dMerger, EllipseMerger, FeatureVectorMerger, @@ -455,6 +456,8 @@ def _for_type(t, **kwargs): return _make(TabularMerger, **kwargs) elif t is AnnotationType.rotated_bbox: return _make(RotatedBboxMerger, **kwargs) + elif t is AnnotationType.cuboid_2d: + return _make(Cuboid2DMerger, **kwargs) else: raise NotImplementedError("Type %s is not supported" % t) diff --git a/src/datumaro/components/transformer.py b/src/datumaro/components/transformer.py index c5d743bbc3..3d9b91c660 100644 --- a/src/datumaro/components/transformer.py +++ b/src/datumaro/components/transformer.py @@ -72,6 +72,80 @@ def __iter__(self): yield item +class TabularTransform(Transform): + """A transformation class for processing dataset items in batches with optional parallelism. + + This class takes a dataset extractor, batch size, and number of worker threads to process + dataset items. Depending on the number of workers specified, it can process items either + sequentially (single-process) or in parallel (multi-process), making it efficient for + batch transformations. + + Parameters: + extractor: The dataset extractor to obtain items from. + batch_size: The batch size for processing items. Default is 1. + num_workers: The number of worker threads to use for parallel processing. + Set to 0 for single-process mode. Default is 0. + """ + + def __init__( + self, + extractor: IDataset, + batch_size: int = 1, + num_workers: int = 0, + ): + super().__init__(extractor) + self._batch_size = batch_size + if not (isinstance(num_workers, int) and num_workers >= 0): + raise ValueError( + f"num_workers should be a non negative integer, but it is {num_workers}" + ) + self._num_workers = num_workers + + def __iter__(self) -> Iterator[DatasetItem]: + if self._num_workers == 0: + return self._iter_single_proc() + return self._iter_multi_procs() + + def _iter_multi_procs(self): + with ThreadPool(processes=self._num_workers) as pool: + + def _producer_gen(): + for batch in take_by(self._extractor, self._batch_size): + future = pool.apply_async( + func=self._process_batch, + args=(batch,), + ) + yield future + + with consumer_generator(producer_generator=_producer_gen()) as consumer_gen: + for future in consumer_gen: + for item in future.get(): + yield item + + def _iter_single_proc(self) -> Iterator[DatasetItem]: + for batch in take_by(self._extractor, self._batch_size): + for item in self._process_batch(batch=batch): + yield item + + def transform_item(self, item: DatasetItem) -> Optional[DatasetItem]: + """ + Returns a modified copy of the input item. + + Avoid changing and returning the input item, because it can lead to + unexpected problems. Use wrap_item() or item.wrap() to simplify copying. + """ + + raise NotImplementedError() + + def _process_batch( + self, + batch: List[DatasetItem], + ) -> List[DatasetItem]: + results = [self.transform_item(item) for item in batch] + + return results + + class ModelTransform(Transform): """A transformation class for applying a model's inference to dataset items. diff --git a/src/datumaro/components/visualizer.py b/src/datumaro/components/visualizer.py index 7030165871..1f184a4e3b 100644 --- a/src/datumaro/components/visualizer.py +++ b/src/datumaro/components/visualizer.py @@ -19,6 +19,7 @@ AnnotationType, Bbox, Caption, + Cuboid2D, Cuboid3d, DepthAnnotation, Ellipse, @@ -661,6 +662,39 @@ def _draw_cuboid_3d( ) -> None: raise NotImplementedError(f"{ann.type} is not implemented yet.") + def _draw_cuboid_2d( + self, + ann: Cuboid2D, + label_categories: Optional[LabelCategories], + fig: Figure, + ax: Axes, + context: List, + ) -> None: + import matplotlib.patches as patches + + points = ann.points + color = self._get_color(ann) + label_text = label_categories[ann.label].name if label_categories is not None else ann.label + + # Define the faces based on vertex indices + + faces = [ + [points[i] for i in [0, 1, 2, 3]], # Top face + [points[i] for i in [4, 5, 6, 7]], # Bottom face + [points[i] for i in [0, 1, 5, 4]], # Front face + [points[i] for i in [1, 2, 6, 5]], # Right face + [points[i] for i in [2, 3, 7, 6]], # Back face + [points[i] for i in [3, 0, 4, 7]], # Left face + ] + ax.text(points[0][0], points[0][1] - self.text_y_offset, label_text, color=color) + + # Draw each face + for face in faces: + polygon = patches.Polygon( + face, fill=False, linewidth=self.bbox_linewidth, edgecolor=color + ) + ax.add_patch(polygon) + def _draw_super_resolution_annotation( self, ann: SuperResolutionAnnotation, diff --git a/src/datumaro/plugins/data_formats/common_semantic_segmentation.py b/src/datumaro/plugins/data_formats/common_semantic_segmentation.py index 4e9f55f625..7845ffc406 100644 --- a/src/datumaro/plugins/data_formats/common_semantic_segmentation.py +++ b/src/datumaro/plugins/data_formats/common_semantic_segmentation.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: MIT import errno -import glob import os.path as osp from typing import List, Optional @@ -69,11 +68,11 @@ def __init__( self._image_prefix = image_prefix self._mask_prefix = mask_prefix - meta_file = glob.glob(osp.join(path, "**", DATASET_META_FILE), recursive=True) - if is_meta_file(meta_file[0]): - self._root_dir = osp.dirname(meta_file[0]) + meta_file = osp.join(path, DATASET_META_FILE) + if is_meta_file(meta_file): + self._root_dir = osp.dirname(meta_file) - label_map = parse_meta_file(meta_file[0]) + label_map = parse_meta_file(meta_file) self._categories = make_categories(label_map) else: raise FileNotFoundError(errno.ENOENT, "Dataset meta info file was not found", path) @@ -163,11 +162,10 @@ def build_cmdline_parser(cls, **kwargs): @classmethod def detect(cls, context: FormatDetectionContext) -> FormatDetectionConfidence: - path = context.require_file(f"**/{DATASET_META_FILE}") - path = osp.dirname(path) + context.require_file(DATASET_META_FILE) - context.require_file(osp.join(path, CommonSemanticSegmentationPath.IMAGES_DIR, "**", "*")) - context.require_file(osp.join(path, CommonSemanticSegmentationPath.MASKS_DIR, "**", "*")) + context.require_file(osp.join(CommonSemanticSegmentationPath.IMAGES_DIR, "**", "*")) + context.require_file(osp.join(CommonSemanticSegmentationPath.MASKS_DIR, "**", "*")) return FormatDetectionConfidence.MEDIUM diff --git a/src/datumaro/plugins/data_formats/datumaro/base.py b/src/datumaro/plugins/data_formats/datumaro/base.py index ee7a8cdc21..5278782822 100644 --- a/src/datumaro/plugins/data_formats/datumaro/base.py +++ b/src/datumaro/plugins/data_formats/datumaro/base.py @@ -11,6 +11,7 @@ AnnotationType, Bbox, Caption, + Cuboid2D, Cuboid3d, Ellipse, GroupType, @@ -182,8 +183,7 @@ def _parse_item(self, item_desc: Dict) -> Optional[DatasetItem]: pcd_info = item_desc.get("point_cloud") if media and pcd_info: raise MediaTypeError(STR_MULTIPLE_MEDIA) - if pcd_info: - pcd_path = pcd_info.get("path") + if pcd_info and (pcd_path := pcd_info.get("path")): point_cloud = osp.join(self._pcd_dir, self._subset, pcd_path) related_images = None @@ -338,6 +338,7 @@ def _load_annotations(self, item: Dict): points, label=label_id, id=ann_id, + visibility=ann.get("visibility"), attributes=attributes, group=group, object_id=object_id, @@ -378,6 +379,18 @@ def _load_annotations(self, item: Dict): elif ann_type == AnnotationType.hash_key: continue + elif ann_type == AnnotationType.cuboid_2d: + loaded.append( + Cuboid2D( + list(map(tuple, points)), + label=label_id, + id=ann_id, + attributes=attributes, + group=group, + object_id=object_id, + z_order=z_order, + ) + ) else: raise NotImplementedError() except Exception as e: diff --git a/src/datumaro/plugins/data_formats/datumaro/exporter.py b/src/datumaro/plugins/data_formats/datumaro/exporter.py index 494492cbe8..a470b66b8f 100644 --- a/src/datumaro/plugins/data_formats/datumaro/exporter.py +++ b/src/datumaro/plugins/data_formats/datumaro/exporter.py @@ -20,6 +20,7 @@ Annotation, Bbox, Caption, + Cuboid2D, Cuboid3d, Ellipse, HashKey, @@ -37,6 +38,7 @@ from datumaro.components.crypter import NULL_CRYPTER from datumaro.components.dataset_base import DatasetItem from datumaro.components.dataset_item_storage import ItemStatus +from datumaro.components.errors import PathSeparatorInSubsetNameError from datumaro.components.exporter import ExportContextComponent, Exporter from datumaro.components.media import Image, MediaElement, PointCloud, Video, VideoFrame from datumaro.util import cast, dump_json_file @@ -184,7 +186,8 @@ def context_save_media( if context.save_media: fname = context.make_video_filename(item) - context.save_video(item, fname=fname, subdir=item.subset) + subdir = item.subset.replace(os.sep, "_") if item.subset else None + context.save_video(item, fname=fname, subdir=subdir) item.media = Video( path=fname, step=video._step, @@ -199,7 +202,8 @@ def context_save_media( if context.save_media: fname = context.make_video_filename(item) - context.save_video(item, fname=fname, subdir=item.subset) + subdir = item.subset.replace(os.sep, "_") if item.subset else None + context.save_video(item, fname=fname, subdir=subdir) item.media = VideoFrame(Video(fname), video_frame.index) yield @@ -209,8 +213,9 @@ def context_save_media( if context.save_media: # Temporarily update image path and save it. - fname = context.make_image_filename(item) - context.save_image(item, encryption=encryption, fname=fname, subdir=item.subset) + fname = context.make_image_filename(item, name=str(item.id).replace(os.sep, "_")) + subdir = item.subset.replace(os.sep, "_") if item.subset else None + context.save_image(item, encryption=encryption, fname=fname, subdir=subdir) item.media = Image.from_file(path=fname, size=image._size) yield @@ -219,14 +224,18 @@ def context_save_media( pcd = item.media_as(PointCloud) if context.save_media: - pcd_fname = context.make_pcd_filename(item) - context.save_point_cloud(item, fname=pcd_fname, subdir=item.subset) + pcd_name = str(item.id).replace(os.sep, "_") + pcd_fname = context.make_pcd_filename(item, name=pcd_name) + subdir = item.subset.replace(os.sep, "_") if item.subset else None + context.save_point_cloud(item, fname=pcd_fname, subdir=subdir) extra_images = [] for i, extra_image in enumerate(pcd.extra_images): extra_images.append( Image.from_file( - path=context.make_pcd_extra_image_filename(item, i, extra_image) + path=context.make_pcd_extra_image_filename( + item, i, extra_image, name=f"{pcd_name}/extra_image_{i}" + ) ) ) @@ -311,6 +320,8 @@ def _gen_item_desc(self, item: DatasetItem, *args, **kwargs) -> Dict: converted_ann = self._convert_ellipse_object(ann) elif isinstance(ann, HashKey): continue + elif isinstance(ann, Cuboid2D): + converted_ann = self._convert_cuboid_2d_object(ann) else: raise NotImplementedError() annotations.append(converted_ann) @@ -435,6 +446,18 @@ def _convert_cuboid_3d_object(self, obj): def _convert_ellipse_object(self, obj: Ellipse): return self._convert_shape_object(obj) + def _convert_cuboid_2d_object(self, obj: Cuboid2D): + converted = self._convert_annotation(obj) + + converted.update( + { + "label_id": cast(obj.label, int), + "points": obj.points, + "z_order": obj.z_order, + } + ) + return converted + class _StreamSubsetWriter(_SubsetWriter): def __init__( @@ -492,18 +515,27 @@ def create_writer( default_image_ext=self._default_image_ext, ) + if os.path.sep in subset: + raise PathSeparatorInSubsetNameError(subset) + return ( _SubsetWriter( context=self, subset=subset, - ann_file=osp.join(self._annotations_dir, subset + self.PATH_CLS.ANNOTATION_EXT), + ann_file=osp.join( + self._annotations_dir, + subset + self.PATH_CLS.ANNOTATION_EXT, + ), export_context=export_context, ) if not self._stream else _StreamSubsetWriter( context=self, subset=subset, - ann_file=osp.join(self._annotations_dir, subset + self.PATH_CLS.ANNOTATION_EXT), + ann_file=osp.join( + self._annotations_dir, + subset + self.PATH_CLS.ANNOTATION_EXT, + ), export_context=export_context, ) ) diff --git a/src/datumaro/plugins/data_formats/datumaro_binary/exporter.py b/src/datumaro/plugins/data_formats/datumaro_binary/exporter.py index a1c86d5061..0b257334fb 100644 --- a/src/datumaro/plugins/data_formats/datumaro_binary/exporter.py +++ b/src/datumaro/plugins/data_formats/datumaro_binary/exporter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -15,7 +15,7 @@ from datumaro.components.crypter import NULL_CRYPTER, Crypter from datumaro.components.dataset_base import DatasetItem, IDataset -from datumaro.components.errors import DatumaroError +from datumaro.components.errors import DatumaroError, PathSeparatorInSubsetNameError from datumaro.components.exporter import ExportContext, ExportContextComponent, Exporter from datumaro.plugins.data_formats.datumaro.exporter import DatumaroExporter from datumaro.plugins.data_formats.datumaro.exporter import _SubsetWriter as __SubsetWriter @@ -309,6 +309,9 @@ def create_writer( default_image_ext=self._default_image_ext, ) + if osp.sep in subset: + raise PathSeparatorInSubsetNameError(subset) + return _SubsetWriter( context=self, subset=subset, diff --git a/src/datumaro/plugins/data_formats/datumaro_binary/mapper/__init__.py b/src/datumaro/plugins/data_formats/datumaro_binary/mapper/__init__.py index cefedf4cbd..01ee56d60a 100644 --- a/src/datumaro/plugins/data_formats/datumaro_binary/mapper/__init__.py +++ b/src/datumaro/plugins/data_formats/datumaro_binary/mapper/__init__.py @@ -22,6 +22,7 @@ "CaptionMapper", "Cuboid3dMapper", "EllipseMapper", + "Cuboid2DMapper", # common "Mapper", "DictMapper", diff --git a/src/datumaro/plugins/data_formats/datumaro_binary/mapper/annotation.py b/src/datumaro/plugins/data_formats/datumaro_binary/mapper/annotation.py index 4c7269719e..c26658bc64 100644 --- a/src/datumaro/plugins/data_formats/datumaro_binary/mapper/annotation.py +++ b/src/datumaro/plugins/data_formats/datumaro_binary/mapper/annotation.py @@ -12,6 +12,7 @@ AnnotationType, Bbox, Caption, + Cuboid2D, Cuboid3d, Ellipse, Label, @@ -270,6 +271,33 @@ def backward(cls, _bytes: bytes, offset: int = 0) -> Tuple[Ellipse, int]: return Ellipse(x, y, x2, y2, **shape_dict), offset +class Cuboid2DMapper(AnnotationMapper): + ann_type = AnnotationType.cuboid_2d + + @classmethod + def forward(cls, ann: Shape) -> bytes: + _bytearray = bytearray() + _bytearray.extend(struct.pack(" Tuple[Ellipse, int]: + ann_dict, offset = super().backward_dict(_bytes, offset) + label, z_order = struct.unpack_from(" bytes: _bytearray.extend(Cuboid3dMapper.forward(ann)) elif isinstance(ann, Ellipse): _bytearray.extend(EllipseMapper.forward(ann)) + elif isinstance(ann, Cuboid2D): + _bytearray.extend(Cuboid2DMapper.forward(ann)) else: raise NotImplementedError() diff --git a/src/datumaro/plugins/data_formats/imagenet.py b/src/datumaro/plugins/data_formats/imagenet.py index 673cfd132d..30b9dfaa5a 100644 --- a/src/datumaro/plugins/data_formats/imagenet.py +++ b/src/datumaro/plugins/data_formats/imagenet.py @@ -48,8 +48,18 @@ def _load_categories(self, path): path = Path(path) for dirname in sorted(d for d in path.rglob("*") if d.is_dir()): dirname = dirname.relative_to(path) + level = len(dirname.parts) if str(dirname) != ImagenetPath.IMAGE_DIR_NO_LABEL: - label_cat.add(str(dirname)) + parent = None + if level > 1: + parent = str(dirname.parents[0]) + if not any([g.name == parent for g in label_cat.label_groups]): + label_cat.add_label_group(parent, [str(dirname.name)], group_type=0) + else: + g = next(x for x in label_cat.label_groups if x.name == parent) + g.labels.append(str(dirname.name)) + label_cat.add(str(dirname), parent) + return {AnnotationType.label: label_cat} def _load_items(self, path): diff --git a/src/datumaro/plugins/data_formats/kaggle/base.py b/src/datumaro/plugins/data_formats/kaggle/base.py index d21b1434c1..06d2ef9a15 100644 --- a/src/datumaro/plugins/data_formats/kaggle/base.py +++ b/src/datumaro/plugins/data_formats/kaggle/base.py @@ -77,13 +77,31 @@ def _parse_bbox_coords(self, bbox_str): # expected to output [x1, y1, x2, y2] return [float(coord.strip()) for coord in coords] - def _load_annotations(self, datas: list, indices: Dict[str, int], bbox_flag: bool): + def _load_annotations( + self, datas: list, indices: Dict[str, Union[int, Dict[str, int]]], bbox_flag: bool + ): if "label" in indices: - label_name = str(datas[indices["label"]]) - label, cat = self._label_cat.find(label_name) - if not cat: - self._label_cat.add(label_name) - label, _ = self._label_cat.find(label_name) + label_indices = indices["label"] + if isinstance(label_indices, dict): + labels = [] + list_values = datas[1:] + index_to_label = {v: k for k, v in label_indices.items()} + present_labels = [ + index_to_label[i + 1] for i, value in enumerate(list_values) if value == "1" + ] + + for label_name in present_labels: + label, cat = self._label_cat.find(label_name) + if not cat: + self._label_cat.add(label_name) + label, _ = self._label_cat.find(label_name) + labels.append(Label(label=label)) + else: + label_name = str(datas[indices["label"]]) + label, cat = self._label_cat.find(label_name) + if not cat: + self._label_cat.add(label_name) + label, _ = self._label_cat.find(label_name) else: _, cat = self._label_cat.find("object") if not cat: @@ -91,7 +109,11 @@ def _load_annotations(self, datas: list, indices: Dict[str, int], bbox_flag: boo label = 0 if "label" in indices and not bbox_flag: + label_indices = indices["label"] + if isinstance(label_indices, dict): + return labels return Label(label=label) + if bbox_flag: if "bbox" in indices: coords = self._parse_bbox_coords(datas[indices["bbox"]]) @@ -125,7 +147,14 @@ def _load_items(self, ann_file: str, columns: Dict[str, Union[str, list]]): indices = {"media": df_fields.index(columns["media"])} if "label" in columns: - indices.update({"label": df_fields.index(columns["label"])}) + label_columns = columns["label"] + if isinstance(label_columns, list): + indices_label = {} + for label in label_columns: + indices_label[label] = df_fields.index(label) + indices.update({"label": indices_label}) + else: + indices.update({"label": df_fields.index(label_columns)}) bbox_flag = False bbox_index = columns.get("bbox") @@ -165,16 +194,30 @@ def _load_items(self, ann_file: str, columns: Dict[str, Union[str, list]]): continue ann = self._load_annotations(data_info, indices, bbox_flag) - self._ann_types.add(ann.type) - if item_id in items: - items[item_id].annotations.append(ann) + if isinstance(ann, list): + for label in ann: + self._ann_types.add(label.type) + if item_id in items: + for label in ann: + items[item_id].annotations.append(label) + else: + items[item_id] = DatasetItem( + id=item_id, + subset=self._subset, + media=Image.from_file(path=media_path), + annotations=ann, + ) else: - items[item_id] = DatasetItem( - id=item_id, - subset=self._subset, - media=Image.from_file(path=media_path), - annotations=[ann], - ) + self._ann_types.add(ann.type) + if item_id in items: + items[item_id].annotations.append(ann) + else: + items[item_id] = DatasetItem( + id=item_id, + subset=self._subset, + media=Image.from_file(path=media_path), + annotations=[ann], + ) return items.values() def categories(self): diff --git a/src/datumaro/plugins/data_formats/kitti/importer.py b/src/datumaro/plugins/data_formats/kitti/importer.py index 2880301901..c1e53fad0c 100644 --- a/src/datumaro/plugins/data_formats/kitti/importer.py +++ b/src/datumaro/plugins/data_formats/kitti/importer.py @@ -99,7 +99,7 @@ class KittiDetectionImporter(KittiImporter): @classmethod def detect(cls, context: FormatDetectionContext) -> FormatDetectionConfidence: # left color camera label files - context.require_file(f"**/label_2/*{cls._ANNO_EXT}") + context.require_file(f"**/label_2/*_*{cls._ANNO_EXT}") return cls.DETECT_CONFIDENCE @classmethod diff --git a/src/datumaro/plugins/data_formats/kitti_3d/__init__.py b/src/datumaro/plugins/data_formats/kitti_3d/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/datumaro/plugins/data_formats/kitti_3d/base.py b/src/datumaro/plugins/data_formats/kitti_3d/base.py new file mode 100644 index 0000000000..c385512e2e --- /dev/null +++ b/src/datumaro/plugins/data_formats/kitti_3d/base.py @@ -0,0 +1,155 @@ +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import glob +import logging +import os +import os.path as osp +from typing import List, Optional, Type, TypeVar + +from datumaro.components.annotation import AnnotationType, Bbox +from datumaro.components.dataset_base import DatasetItem, SubsetBase +from datumaro.components.errors import InvalidAnnotationError +from datumaro.components.importer import ImportContext +from datumaro.components.media import Image +from datumaro.util.image import find_images + +from .format import Kitti3DLabelMap, Kitti3dPath, make_kitti3d_categories + +T = TypeVar("T") + + +class Kitti3dBase(SubsetBase): + # https://www.cvlibs.net/datasets/kitti/eval_object.php?obj_benchmark=3d + + def __init__( + self, + path: str, + *, + subset: Optional[str] = None, + ctx: Optional[ImportContext] = None, + ): + assert osp.isdir(path), path + + self._path = path + + if not subset: + folder_path = path.rsplit(Kitti3dPath.LABEL_DIR, 1)[0] + img_dir = osp.join(folder_path, Kitti3dPath.IMAGE_DIR) + if any(os.path.isdir(os.path.join(img_dir, item)) for item in os.listdir(img_dir)): + subset = osp.split(path)[-1] + self._path = folder_path + super().__init__(subset=subset, ctx=ctx) + + self._categories = make_kitti3d_categories(Kitti3DLabelMap) + self._items = self._load_items() + + def _load_items(self) -> List[DatasetItem]: + items = [] + + image_dir = osp.join(self._path, Kitti3dPath.IMAGE_DIR) + image_path_by_id = { + osp.split(osp.splitext(osp.relpath(p, image_dir))[0])[-1]: p + for p in find_images(image_dir, recursive=True) + } + + if self._subset == "default": + ann_dir = osp.join(self._path, Kitti3dPath.LABEL_DIR) + else: + ann_dir = osp.join(self._path, Kitti3dPath.LABEL_DIR, self._subset) + + label_categories = self._categories[AnnotationType.label] + + for labels_path in sorted(glob.glob(osp.join(ann_dir, "**", "*.txt"), recursive=True)): + item_id = osp.splitext(osp.relpath(labels_path, ann_dir))[0] + anns = [] + + try: + with open(labels_path, "r", encoding="utf-8") as f: + lines = f.readlines() + except IOError as e: + logging.error(f"Error reading file {labels_path}: {e}") + continue + + for line_idx, line in enumerate(lines): + line = line.split() + if len(line) not in [15, 16]: + logging.warning( + f"Unexpected line length {len(line)} in file {labels_path} at line {line_idx + 1}" + ) + continue + + label_name = line[0] + label_id = label_categories.find(label_name)[0] + if label_id is None: + label_id = label_categories.add(label_name) + + try: + x1 = self._parse_field(line[4], float, "bbox left-top x") + y1 = self._parse_field(line[5], float, "bbox left-top y") + x2 = self._parse_field(line[6], float, "bbox right-bottom x") + y2 = self._parse_field(line[7], float, "bbox right-bottom y") + + attributes = { + "truncated": self._parse_field(line[1], float, "truncated"), + "occluded": self._parse_field(line[2], int, "occluded"), + "alpha": self._parse_field(line[3], float, "alpha"), + "dimensions": [ + self._parse_field(line[8], float, "height (in meters)"), + self._parse_field(line[9], float, "width (in meters)"), + self._parse_field(line[10], float, "length (in meters)"), + ], + "location": [ + self._parse_field(line[11], float, "x (in meters)"), + self._parse_field(line[12], float, "y (in meters)"), + self._parse_field(line[13], float, "z (in meters)"), + ], + "rotation_y": self._parse_field(line[14], float, "rotation_y"), + } + except ValueError as e: + logging.error(f"Error parsing line {line_idx + 1} in file {labels_path}: {e}") + continue + + anns.append( + Bbox( + x=x1, + y=y1, + w=x2 - x1, + h=y2 - y1, + id=line_idx, + attributes=attributes, + label=label_id, + ) + ) + self._ann_types.add(AnnotationType.bbox) + + image = image_path_by_id.pop(item_id, None) + if image: + image = Image.from_file(path=image) + + if self._subset == "default": + calib_path = osp.join(self._path, Kitti3dPath.CALIB_DIR, item_id + ".txt") + else: + calib_path = osp.join( + self._path, Kitti3dPath.CALIB_DIR, self._subset, item_id + ".txt" + ) + items.append( + DatasetItem( + id=item_id, + subset=self._subset, + media=image, + attributes={"calib_path": calib_path}, + annotations=anns, + ) + ) + + return items + + def _parse_field(self, value: str, desired_type: Type[T], field_name: str) -> T: + try: + return desired_type(value) + except Exception as e: + raise InvalidAnnotationError( + f"Can't parse {field_name} from '{value}'. Expected {desired_type}" + ) from e diff --git a/src/datumaro/plugins/data_formats/kitti_3d/format.py b/src/datumaro/plugins/data_formats/kitti_3d/format.py new file mode 100644 index 0000000000..c61f2b1f3f --- /dev/null +++ b/src/datumaro/plugins/data_formats/kitti_3d/format.py @@ -0,0 +1,43 @@ +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os.path as osp + +from datumaro.components.annotation import AnnotationType, LabelCategories + + +class Kitti3dPath: + PCD_DIR = osp.join("velodyne") + IMAGE_DIR = "image_2" + LABEL_DIR = "label_2" + CALIB_DIR = "calib" + + +Kitti3DLabelMap = [ + "DontCare", + "Car", + "Pedestrian", + "Van", + "Truck", + "Cyclist", + "Sitter", + "Train", + "Motorcycle", + "Bus", + "Misc", +] + + +def make_kitti3d_categories(label_map=None): + if label_map is None: + label_map = Kitti3DLabelMap + + categories = {} + common_attrs = {"truncated", "occluded", "alpha", "dimensions", "location", "rotation_y"} + label_categories = LabelCategories(attributes=common_attrs) + for label in label_map: + label_categories.add(label) + categories[AnnotationType.label] = label_categories + + return categories diff --git a/src/datumaro/plugins/data_formats/kitti_3d/importer.py b/src/datumaro/plugins/data_formats/kitti_3d/importer.py new file mode 100644 index 0000000000..2840218af7 --- /dev/null +++ b/src/datumaro/plugins/data_formats/kitti_3d/importer.py @@ -0,0 +1,53 @@ +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os.path as osp +from typing import List + +from datumaro.components.errors import DatasetImportError +from datumaro.components.format_detection import FormatDetectionConfidence, FormatDetectionContext +from datumaro.components.importer import Importer + +from .format import Kitti3dPath + + +class Kitti3dImporter(Importer): + _ANNO_EXT = ".txt" + + @classmethod + def detect(cls, context: FormatDetectionContext) -> FormatDetectionConfidence: + context.require_file(f"{Kitti3dPath.CALIB_DIR}/*.txt") + cls._check_ann_file(context.require_file(f"{Kitti3dPath.LABEL_DIR}/*.txt"), context) + return FormatDetectionConfidence.MEDIUM + + @classmethod + def _check_ann_file(cls, fpath: str, context: FormatDetectionContext) -> bool: + with context.probe_text_file( + fpath, "Requirements for the annotation file of Kitti 3D format" + ) as fp: + for line in fp: + fields = line.rstrip("\n").split(" ") + if len(fields) == 15 or len(fields) == 16: + return True + raise DatasetImportError( + f"Kitti 3D format txt file should have 15 or 16 fields for " + f"each line, but the read line has {len(fields)} fields: " + f"fields={fields}." + ) + raise DatasetImportError("Empty file is not allowed.") + + @classmethod + def get_file_extensions(cls) -> List[str]: + return [cls._ANNO_EXT] + + @classmethod + def find_sources(cls, path): + # return [{"url": path, "format": "kitti3d"}] + sources = cls._find_sources_recursive( + path, "", "kitti3d", dirname=Kitti3dPath.LABEL_DIR, file_filter=lambda p: osp.isdir(p) + ) + if len(sources) == 0: + return [{"url": path, "format": "kitti3d"}] + else: + return sources diff --git a/src/datumaro/plugins/data_formats/kitti_raw/base.py b/src/datumaro/plugins/data_formats/kitti_raw/base.py index 92e04cc88e..836ad28574 100644 --- a/src/datumaro/plugins/data_formats/kitti_raw/base.py +++ b/src/datumaro/plugins/data_formats/kitti_raw/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021-2023 Intel Corporation +# Copyright (C) 2021-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -182,7 +182,7 @@ def _parse_attr(cls, value): @classmethod def _parse_track(cls, track_id, track, categories): common_attrs = {k: cls._parse_attr(v) for k, v in track["attributes"].items()} - scale = [track["scale"][k] for k in ["w", "h", "l"]] + scale = [track["scale"][k] for k in ["h", "w", "l"]] label = categories[AnnotationType.label].find(track["label"])[0] kf_occluded = False diff --git a/src/datumaro/plugins/data_formats/kitti_raw/exporter.py b/src/datumaro/plugins/data_formats/kitti_raw/exporter.py index 8e2f250d29..3d01b1d822 100644 --- a/src/datumaro/plugins/data_formats/kitti_raw/exporter.py +++ b/src/datumaro/plugins/data_formats/kitti_raw/exporter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 Intel Corporation +# Copyright (C) 2021-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -339,8 +339,8 @@ def _create_tracklets(self, subset): if not track: track = { "objectType": label, - "h": ann.scale[1], - "w": ann.scale[0], + "h": ann.scale[0], + "w": ann.scale[1], "l": ann.scale[2], "first_frame": frame_id, "poses": [], @@ -348,7 +348,7 @@ def _create_tracklets(self, subset): } tracks[track_id] = track else: - if [track["w"], track["h"], track["l"]] != ann.scale: + if [track["h"], track["w"], track["l"]] != ann.scale: # Tracks have fixed scale in the format raise DatasetExportError( "Item %s: mismatching track shapes, " diff --git a/src/datumaro/plugins/framework_converter.py b/src/datumaro/plugins/framework_converter.py index 556005e1b7..1aeb51138b 100644 --- a/src/datumaro/plugins/framework_converter.py +++ b/src/datumaro/plugins/framework_converter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2023-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -17,6 +17,7 @@ "detection": AnnotationType.bbox, "instance_segmentation": AnnotationType.polygon, "semantic_segmentation": AnnotationType.mask, + "tabular": [AnnotationType.label, AnnotationType.caption], } @@ -88,7 +89,10 @@ def _gen_item(self, idx: int): if ann.type == TASK_ANN_TYPE[self.task] ] label = mask_tools.merge_masks((mask, label_id) for mask, label_id in masks) - + elif self.task == "tabular": + label = [ + ann.as_dict() for ann in item.annotations if ann.type in TASK_ANN_TYPE[self.task] + ] return image, label @@ -103,15 +107,61 @@ def __init__( task: str, transform: Optional[Callable] = None, target_transform: Optional[Callable] = None, + target: Optional[str] = None, + tokenizer: Optional[tuple[Callable, Callable]] = None, + vocab: Optional[tuple[Callable, Callable]] = None, ): super().__init__(dataset=dataset, subset=subset, task=task) self.transform = transform self.target_transform = target_transform + if self.task == "tabular": + if not isinstance(target, dict): + raise ValueError( + "Target should be a dictionary with 'input' and 'output' keys." + ) + self.input_target = target.get("input") + self.output_target = target.get("output") + if not self.input_target: + raise ValueError( + "Please provide target column for tabular task which is used for input" + ) + + if not (tokenizer and vocab): + raise ValueError("Both tokenizer and vocab must be provided for tabular task") + self.tokenizer = tokenizer + self.vocab = vocab + def __getitem__(self, idx): image, label = self._gen_item(idx) + if self.task == "tabular": + try: + text = image[self.input_target] + except TypeError: + text = image()[self.input_target] + + if self.output_target: + src_tokenizer, tgt_tokenizer = self.tokenizer + src_vocab, tgt_vocab = self.vocab + src_tokens = src_tokenizer(text) + src_token_ids = src_vocab(src_tokens) + + label_text = label[0]["caption"].split(f"{self.output_target}:")[-1] + tgt_tokens = tgt_tokenizer(label_text) + tgt_token_ids = tgt_vocab(tgt_tokens) + + return torch.tensor(src_token_ids, dtype=torch.long), torch.tensor( + tgt_token_ids, dtype=torch.long + ) + else: + tokens = self.tokenizer(text) + token_ids = self.vocab(tokens) + return torch.tensor(token_ids, dtype=torch.long), torch.tensor( + label[0]["label"], dtype=torch.long + ) + if len(image.shape) == 2: image = np.expand_dims(image, axis=-1) diff --git a/src/datumaro/plugins/openvino_plugin/launcher.py b/src/datumaro/plugins/openvino_plugin/launcher.py index bdc924a949..9802ab0ca6 100644 --- a/src/datumaro/plugins/openvino_plugin/launcher.py +++ b/src/datumaro/plugins/openvino_plugin/launcher.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2021 Intel Corporation +# Copyright (C) 2019-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -92,6 +92,8 @@ class BuiltinOpenvinoModelInfo(OpenvinoModelInfo): downloadable_models = { "clip_text_ViT-B_32", "clip_visual_ViT-B_32", + "clip_visual_vit_l_14_336px_int8", + "clip_text_vit_l_14_336px_int8", "googlenet-v4-tf", } diff --git a/src/datumaro/plugins/openvino_plugin/samples/clip_text_vit_l_14_336px_int8_interp.py b/src/datumaro/plugins/openvino_plugin/samples/clip_text_vit_l_14_336px_int8_interp.py new file mode 100644 index 0000000000..3e7b6ad5a2 --- /dev/null +++ b/src/datumaro/plugins/openvino_plugin/samples/clip_text_vit_l_14_336px_int8_interp.py @@ -0,0 +1,30 @@ +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +from typing import List, Tuple + +from datumaro.components.abstracts import IModelInterpreter +from datumaro.components.abstracts.model_interpreter import LauncherInputType, ModelPred, PrepInfo +from datumaro.components.annotation import Annotation, AnnotationType, LabelCategories +from datumaro.components.dataset_base import DatasetItem +from datumaro.components.errors import DatumaroError +from datumaro.components.media import Image +from datumaro.plugins.openvino_plugin.samples.utils import gen_hash_key + + +class ClipTextViTL14ModelInterpreter(IModelInterpreter): + def preprocess(self, inp: DatasetItem) -> Tuple[LauncherInputType, PrepInfo]: + img = inp.media_as(Image).data + return img, None + + def postprocess(self, pred: ModelPred, info: PrepInfo) -> List[Annotation]: + feature_vector = pred.get("output") + if feature_vector is None: + raise DatumaroError('"output" key should exist in the model prediction.') + + return [gen_hash_key(feature_vector)] + + def get_categories(self): + label_categories = LabelCategories() + return {AnnotationType.label: label_categories} diff --git a/src/datumaro/plugins/openvino_plugin/samples/clip_visual_vit_l_14_336px_int8_interp.py b/src/datumaro/plugins/openvino_plugin/samples/clip_visual_vit_l_14_336px_int8_interp.py new file mode 100644 index 0000000000..320059357a --- /dev/null +++ b/src/datumaro/plugins/openvino_plugin/samples/clip_visual_vit_l_14_336px_int8_interp.py @@ -0,0 +1,52 @@ +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os.path as osp +from typing import List, Tuple + +import cv2 +import numpy as np + +from datumaro.components.abstracts import IModelInterpreter +from datumaro.components.abstracts.model_interpreter import LauncherInputType, ModelPred, PrepInfo +from datumaro.components.annotation import Annotation, AnnotationType, LabelCategories +from datumaro.components.dataset_base import DatasetItem +from datumaro.components.errors import DatumaroError +from datumaro.components.media import Image +from datumaro.plugins.openvino_plugin.samples.utils import gen_hash_key +from datumaro.util.samples import get_samples_path + + +class ClipViTL14ModelInterpreter(IModelInterpreter): + mean = (255 * np.array([0.485, 0.456, 0.406])).reshape(1, 1, 3) + std = (255 * np.array([0.229, 0.224, 0.225])).reshape(1, 1, 3) + + def preprocess(self, inp: DatasetItem) -> Tuple[LauncherInputType, PrepInfo]: + img = inp.media_as(Image).data + img = cv2.resize(img, (336, 336)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = (img - self.mean) / self.std + + if img.ndim == 3 and img.shape[2] in {3, 4}: + img = np.transpose(img, (2, 0, 1)) + return img, None + + def postprocess(self, pred: ModelPred, info: PrepInfo) -> List[Annotation]: + feature_vector = pred.get("output") + if feature_vector is None: + raise DatumaroError('"output" key should exist in the model prediction.') + + return [gen_hash_key(feature_vector)] + + def get_categories(self): + label_categories = LabelCategories() + openvino_plugin_samples_dir = get_samples_path() + imagenet_class_path = osp.join(openvino_plugin_samples_dir, "imagenet.class") + + with open(imagenet_class_path, "r", encoding="utf-8") as file: + labels = [line.strip() for line in file] + for label in labels: + label_categories.add(label) + + return {AnnotationType.label: label_categories} diff --git a/src/datumaro/plugins/specs.json b/src/datumaro/plugins/specs.json index 8891b79287..395ff510b0 100644 --- a/src/datumaro/plugins/specs.json +++ b/src/datumaro/plugins/specs.json @@ -799,6 +799,21 @@ ] } }, + { + "import_path": "datumaro.plugins.data_formats.kitti_3d.base.Kitti3dBase", + "plugin_name": "kitti3d", + "plugin_type": "DatasetBase" + }, + { + "import_path": "datumaro.plugins.data_formats.kitti_3d.importer.Kitti3dImporter", + "plugin_name": "kitti3d", + "plugin_type": "Importer", + "metadata": { + "file_extensions": [ + ".txt" + ] + } + }, { "import_path": "datumaro.plugins.data_formats.kitti_raw.base.KittiRawBase", "plugin_name": "kitti_raw", @@ -1855,13 +1870,13 @@ "plugin_type": "Transform" }, { - "import_path": "datumaro.plugins.transforms.Correct", - "plugin_name": "correct", + "import_path": "datumaro.plugins.transforms.Clean", + "plugin_name": "clean", "plugin_type": "Transform" }, { - "import_path": "datumaro.plugins.transforms.Clean", - "plugin_name": "clean", + "import_path": "datumaro.plugins.transforms.Correct", + "plugin_name": "correct", "plugin_type": "Transform" }, { @@ -1929,6 +1944,11 @@ "plugin_name": "remove_annotations", "plugin_type": "Transform" }, + { + "import_path": "datumaro.plugins.transforms.PseudoLabeling", + "plugin_name": "pseudo_labeling", + "plugin_type": "Transform" + }, { "import_path": "datumaro.plugins.transforms.RemoveAttributes", "plugin_name": "remove_attributes", diff --git a/src/datumaro/plugins/transforms.py b/src/datumaro/plugins/transforms.py index 6060b0ad3b..59062cd349 100644 --- a/src/datumaro/plugins/transforms.py +++ b/src/datumaro/plugins/transforms.py @@ -9,6 +9,7 @@ import os.path as osp import random import re +import string from collections import Counter, defaultdict from copy import deepcopy from enum import Enum, auto @@ -22,6 +23,8 @@ from pandas.api.types import CategoricalDtype import datumaro.util.mask_tools as mask_tools +from datumaro.components.algorithms.hash_key_inference.explorer import Explorer +from datumaro.components.algorithms.hash_key_inference.hashkey_util import calculate_hamming from datumaro.components.annotation import ( AnnotationType, Bbox, @@ -40,6 +43,7 @@ TabularCategories, ) from datumaro.components.cli_plugin import CliPlugin +from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DEFAULT_SUBSET_NAME, DatasetInfo, DatasetItem, IDataset from datumaro.components.errors import ( AnnotationTypeError, @@ -60,8 +64,8 @@ UndefinedAttribute, UndefinedLabel, ) -from datumaro.components.media import Image, TableRow -from datumaro.components.transformer import ItemTransform, Transform +from datumaro.components.media import Image, TableRow, VideoFrame +from datumaro.components.transformer import ItemTransform, TabularTransform, Transform from datumaro.util import NOTSET, filter_dict, parse_json_file, parse_str_enum_value, take_by from datumaro.util.annotation_util import find_group_leader, find_instances from datumaro.util.tabular_util import emoji_pattern @@ -592,12 +596,94 @@ def __iter__(self): class IdFromImageName(ItemTransform, CliPlugin): """ - Renames items in the dataset using image file name (without extension). + Renames items in the dataset based on the image file name, excluding the extension.|n + When 'ensure_unique' is enabled, a random suffix is appened to ensure each identifier is unique + in cases where the image name is not distinct. By default, the random suffix is three characters long, + but this can be adjusted with the 'suffix_length' parameter.|n + |n + Examples:|n + |n + |s|s- Renames items without duplication check:|n + + .. code-block:: + + |s|s|s|s%(prog)s|n + |n + |s|s- Renames items with duplication check:|n + + .. code-block:: + + |s|s|s|s%(prog)s --ensure_unique|n + |n + |s|s- Renames items with duplication check and alters the suffix length(default: 3):|n + + .. code-block:: + + |s|s|s|s%(prog)s --ensure_unique --suffix_length 2 """ + DEFAULT_RETRY = 1000 + SUFFIX_LETTERS = string.ascii_lowercase + string.digits + + @classmethod + def build_cmdline_parser(cls, **kwargs): + parser = super().build_cmdline_parser(**kwargs) + parser.add_argument( + "-u", + "--ensure_unique", + action="store_true", + help="Appends a random suffix to ensure each identifier is unique if the image name is duplicated.", + ) + parser.add_argument( + "-l", + "--suffix_length", + type=int, + default=3, + help="Alters the length of the random suffix if the 'ensure_unique' is enabled.", + ) + + return parser + + def __init__(self, extractor, ensure_unique: bool = False, suffix_length: int = 3): + super().__init__(extractor) + self._length = "parent" + self._ensure_unique = ensure_unique + self._names: set[str] = set() + self._suffix_length = suffix_length + if suffix_length <= 0: + raise ValueError( + f"The 'suffix_length' must be greater than 0. Received: {suffix_length}." + ) + self._max_retry = min( + self.DEFAULT_RETRY, pow(len(self.SUFFIX_LETTERS), self._suffix_length) + ) + + def _add_unique_suffix(self, name): + count = 0 + while name in self._names: + suffix = "".join( + random.choices(self.SUFFIX_LETTERS, k=self._suffix_length) # nosec B311 + ) + new_name = f"{name}__{suffix}" + if new_name not in self._names: + name = new_name + break + count += 1 + if count == self._max_retry: + raise Exception( + f"Too many duplicate names. Failed to generate a unique suffix after {self._max_retry} attempts." + ) + + self._names.add(name) + return name + def transform_item(self, item): if isinstance(item.media, Image) and hasattr(item.media, "path"): name = osp.splitext(osp.basename(item.media.path))[0] + if isinstance(item.media, VideoFrame): + name += f"_frame-{item.media.index}" + if self._ensure_unique: + name = self._add_unique_suffix(name) return self.wrap_item(item, id=name) else: log.debug("Can't change item id for item '%s': " "item has no path info" % item.id) @@ -1348,9 +1434,21 @@ def transform_item(self, item: DatasetItem): class Correct(Transform, CliPlugin): """ - Correct the dataset from a validation report. - A user can should feed into validation_reports.json from validator to correct the dataset. - This helps to refine the dataset by rejecting undefined labels, missing annotations, and outliers. + This class provides functionality to correct and refine a dataset based on a validation report.|n + It processes a validation report (typically in JSON format) to identify and rectify various |n + dataset issues, such as undefined labels, missing annotations, outliers, empty labels/captions,|n + and unnecessary characters in captions. The correction process includes:|n + |n + - Adding missing labels and attributes.|n + - Removing or adjusting annotations with invalid or anomalous values.|n + - Filling in missing labels and captions with appropriate values.|n + - Removing unnecessary characters from text-based annotations like captions.|n + - Handling outliers by capping values within specified bounds.|n + - Updating dataset categories and annotations according to the corrections.|n + |n + The class is designed to be used as part of a command-line interface (CLI) and can be |n + configured with different validation reports. It integrates with the dataset extraction |n + process, ensuring that corrections are applied consistently across the dataset.|n """ @classmethod @@ -1746,13 +1844,15 @@ def __iter__(self): class AstypeAnnotations(ItemTransform): """ - Enables the conversion of annotation types for the categories and individual items within a dataset.|n + Converts the types of annotations within a dataset based on a specified mapping.|n |n - Based on a specified mapping, it transforms the annotation types,|n - changing them to 'Label' if they are categorical, and to 'Caption' if they are of type string, float, or integer.|n + This transform changes annotations to 'Label' if they are categorical, and to 'Caption' + if they are of type string, float, or integer. This is particularly useful when working + with tabular data that needs to be converted into a format suitable for specific machine + learning tasks.|n |n Examples:|n - - Convert type of `title` annotation|n + - Converts the type of a `title` annotation:|n .. code-block:: @@ -1847,7 +1947,7 @@ def transform_item(self, item: DatasetItem): return self.wrap_item(item, annotations=annotations) -class Clean(ItemTransform): +class Clean(TabularTransform): """ A class used to refine the media items in a dataset.|n |n @@ -1866,8 +1966,10 @@ class Clean(ItemTransform): def __init__( self, extractor: IDataset, + batch_size: int = 1, + num_workers: int = 0, ): - super().__init__(extractor) + super().__init__(extractor, batch_size, num_workers) self._outlier_value = {} self._missing_value = {} @@ -1957,7 +2059,7 @@ def refine_tabular_media(self, item): or item.media.table.dtype(col) is int ] - df[str_cols] = df[str_cols].applymap(lambda x: self.remove_unnecessary_char(x)) + df[str_cols] = df[str_cols].map(lambda x: self.remove_unnecessary_char(x)) if not (self._outlier_value): self.check_outlier(media.table.data[float_cols + int_cols], float_cols + int_cols) @@ -2004,3 +2106,64 @@ def transform_item(self, item): refined_annotations.append(ann) return self.wrap_item(item, media=refined_media, annotations=refined_annotations) + + +class PseudoLabeling(ItemTransform): + """ + A class used to assign pseudo-labels to items in a dataset based on + their similarity to predefined labels.|n + |n + This class leverages hashing techniques to compute the similarity + between dataset items and a set of predefined labels.|n + It assigns the most similar label as a pseudo-label to each item. + This is particularly useful in semi-supervised + learning scenarios where some labels are missing or uncertain.|n + |n + Attributes:|n + - extractor : IDataset|n + The dataset extractor that provides access to dataset items and their annotations.|n + - labels : Optional[List[str]]|n + A list of label names to be used for pseudo-labeling. + If not provided, all available labels in the dataset will be used.|n + - explorer : Optional[Explorer]|n + An optional Explorer object used to compute hash keys for items and labels. + If not provided, a new Explorer will be created.|n + """ + + def __init__( + self, + extractor: IDataset, + labels: Optional[List[str]] = None, + explorer: Optional[Explorer] = None, + ): + super().__init__(extractor) + + self._categories = self._extractor.categories() + self._labels = labels + self._explorer = explorer + self._label_indices = self._categories[AnnotationType.label]._indices + + if not self._labels: + self._labels = list(self._label_indices.keys()) + if not self._explorer: + self._explorer = Explorer(Dataset.from_iterable(list(self._extractor))) + + label_hashkeys = [ + np.unpackbits(self._explorer._get_hash_key_from_text_query(label).hash_key, axis=-1) + for label in self._labels + ] + self._label_hashkeys = np.stack(label_hashkeys, axis=0) + + def categories(self): + return self._categories + + def transform_item(self, item: DatasetItem): + hashkey_ = np.unpackbits(self._explorer._get_hash_key_from_item_query(item).hash_key) + logits = calculate_hamming(hashkey_, self._label_hashkeys) + inverse_distances = 1.0 / (logits + 1e-6) + probs = inverse_distances / np.sum(inverse_distances) + ind = np.argsort(probs)[::-1] + + pseudo = np.array(self._labels)[ind][0] + pseudo_annotation = [Label(label=self._label_indices[pseudo])] + return self.wrap_item(item, annotations=pseudo_annotation) diff --git a/src/datumaro/version.py b/src/datumaro/version.py index b3de187d2e..7972985981 100644 --- a/src/datumaro/version.py +++ b/src/datumaro/version.py @@ -1 +1 @@ -__version__ = "1.9.0rc0" +__version__ = "1.10.0rc1" diff --git a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00003676.JPEG b/tests/assets/explore_dataset/bird/0.JPEG similarity index 100% rename from tests/assets/explore_dataset/bird/ILSVRC2012_val_00003676.JPEG rename to tests/assets/explore_dataset/bird/0.JPEG diff --git a/tests/assets/explore_dataset/bird/1.JPEG b/tests/assets/explore_dataset/bird/1.JPEG new file mode 100755 index 0000000000..8a0ed69866 Binary files /dev/null and b/tests/assets/explore_dataset/bird/1.JPEG differ diff --git a/tests/assets/explore_dataset/bird/2.JPEG b/tests/assets/explore_dataset/bird/2.JPEG new file mode 100755 index 0000000000..8a0ed69866 Binary files /dev/null and b/tests/assets/explore_dataset/bird/2.JPEG differ diff --git a/tests/assets/explore_dataset/bird/3.JPEG b/tests/assets/explore_dataset/bird/3.JPEG new file mode 100755 index 0000000000..8a0ed69866 Binary files /dev/null and b/tests/assets/explore_dataset/bird/3.JPEG differ diff --git a/tests/assets/explore_dataset/bird/4.JPEG b/tests/assets/explore_dataset/bird/4.JPEG new file mode 100755 index 0000000000..8a0ed69866 Binary files /dev/null and b/tests/assets/explore_dataset/bird/4.JPEG differ diff --git a/tests/assets/explore_dataset/bird/5.JPEG b/tests/assets/explore_dataset/bird/5.JPEG new file mode 100755 index 0000000000..8a0ed69866 Binary files /dev/null and b/tests/assets/explore_dataset/bird/5.JPEG differ diff --git a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00001563.JPEG b/tests/assets/explore_dataset/bird/ILSVRC2012_val_00001563.JPEG deleted file mode 100755 index 06fad85759..0000000000 Binary files a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00001563.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00019750.JPEG b/tests/assets/explore_dataset/bird/ILSVRC2012_val_00019750.JPEG deleted file mode 100755 index 1367daaab7..0000000000 Binary files a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00019750.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00033708.JPEG b/tests/assets/explore_dataset/bird/ILSVRC2012_val_00033708.JPEG deleted file mode 100755 index 3d52b00ff1..0000000000 Binary files a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00033708.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00044891.JPEG b/tests/assets/explore_dataset/bird/ILSVRC2012_val_00044891.JPEG deleted file mode 100755 index ef4e8e8863..0000000000 Binary files a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00044891.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00045503.JPEG b/tests/assets/explore_dataset/bird/ILSVRC2012_val_00045503.JPEG deleted file mode 100755 index 564b29da5b..0000000000 Binary files a/tests/assets/explore_dataset/bird/ILSVRC2012_val_00045503.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00024500.JPEG b/tests/assets/explore_dataset/cat/0.JPEG similarity index 100% rename from tests/assets/explore_dataset/cat/ILSVRC2012_val_00024500.JPEG rename to tests/assets/explore_dataset/cat/0.JPEG diff --git a/tests/assets/explore_dataset/cat/1.JPEG b/tests/assets/explore_dataset/cat/1.JPEG new file mode 100755 index 0000000000..1f0266df0e Binary files /dev/null and b/tests/assets/explore_dataset/cat/1.JPEG differ diff --git a/tests/assets/explore_dataset/cat/2.JPEG b/tests/assets/explore_dataset/cat/2.JPEG new file mode 100755 index 0000000000..1f0266df0e Binary files /dev/null and b/tests/assets/explore_dataset/cat/2.JPEG differ diff --git a/tests/assets/explore_dataset/cat/3.JPEG b/tests/assets/explore_dataset/cat/3.JPEG new file mode 100755 index 0000000000..1f0266df0e Binary files /dev/null and b/tests/assets/explore_dataset/cat/3.JPEG differ diff --git a/tests/assets/explore_dataset/cat/4.JPEG b/tests/assets/explore_dataset/cat/4.JPEG new file mode 100755 index 0000000000..1f0266df0e Binary files /dev/null and b/tests/assets/explore_dataset/cat/4.JPEG differ diff --git a/tests/assets/explore_dataset/cat/5.JPEG b/tests/assets/explore_dataset/cat/5.JPEG new file mode 100755 index 0000000000..1f0266df0e Binary files /dev/null and b/tests/assets/explore_dataset/cat/5.JPEG differ diff --git a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00004894.JPEG b/tests/assets/explore_dataset/cat/ILSVRC2012_val_00004894.JPEG deleted file mode 100755 index cdc827fdd1..0000000000 Binary files a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00004894.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00010218.JPEG b/tests/assets/explore_dataset/cat/ILSVRC2012_val_00010218.JPEG deleted file mode 100755 index b5b21d2a90..0000000000 Binary files a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00010218.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00015372.JPEG b/tests/assets/explore_dataset/cat/ILSVRC2012_val_00015372.JPEG deleted file mode 100755 index de06ec2003..0000000000 Binary files a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00015372.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00015898.JPEG b/tests/assets/explore_dataset/cat/ILSVRC2012_val_00015898.JPEG deleted file mode 100755 index 7b9949b2b7..0000000000 Binary files a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00015898.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00049996.JPEG b/tests/assets/explore_dataset/cat/ILSVRC2012_val_00049996.JPEG deleted file mode 100755 index acf01454d6..0000000000 Binary files a/tests/assets/explore_dataset/cat/ILSVRC2012_val_00049996.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00001698.JPEG b/tests/assets/explore_dataset/dog/0.JPEG similarity index 100% rename from tests/assets/explore_dataset/dog/ILSVRC2012_val_00001698.JPEG rename to tests/assets/explore_dataset/dog/0.JPEG diff --git a/tests/assets/explore_dataset/dog/1.JPEG b/tests/assets/explore_dataset/dog/1.JPEG new file mode 100755 index 0000000000..b370f35998 Binary files /dev/null and b/tests/assets/explore_dataset/dog/1.JPEG differ diff --git a/tests/assets/explore_dataset/dog/2.JPEG b/tests/assets/explore_dataset/dog/2.JPEG new file mode 100755 index 0000000000..b370f35998 Binary files /dev/null and b/tests/assets/explore_dataset/dog/2.JPEG differ diff --git a/tests/assets/explore_dataset/dog/3.JPEG b/tests/assets/explore_dataset/dog/3.JPEG new file mode 100755 index 0000000000..b370f35998 Binary files /dev/null and b/tests/assets/explore_dataset/dog/3.JPEG differ diff --git a/tests/assets/explore_dataset/dog/4.JPEG b/tests/assets/explore_dataset/dog/4.JPEG new file mode 100755 index 0000000000..b370f35998 Binary files /dev/null and b/tests/assets/explore_dataset/dog/4.JPEG differ diff --git a/tests/assets/explore_dataset/dog/5.JPEG b/tests/assets/explore_dataset/dog/5.JPEG new file mode 100755 index 0000000000..b370f35998 Binary files /dev/null and b/tests/assets/explore_dataset/dog/5.JPEG differ diff --git a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00002749.JPEG b/tests/assets/explore_dataset/dog/ILSVRC2012_val_00002749.JPEG deleted file mode 100755 index e638550f1b..0000000000 Binary files a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00002749.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00016303.JPEG b/tests/assets/explore_dataset/dog/ILSVRC2012_val_00016303.JPEG deleted file mode 100755 index a655729100..0000000000 Binary files a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00016303.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00021389.JPEG b/tests/assets/explore_dataset/dog/ILSVRC2012_val_00021389.JPEG deleted file mode 100755 index 6411284302..0000000000 Binary files a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00021389.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00036055.JPEG b/tests/assets/explore_dataset/dog/ILSVRC2012_val_00036055.JPEG deleted file mode 100755 index 618835bafb..0000000000 Binary files a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00036055.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00047918.JPEG b/tests/assets/explore_dataset/dog/ILSVRC2012_val_00047918.JPEG deleted file mode 100755 index 78f81dd202..0000000000 Binary files a/tests/assets/explore_dataset/dog/ILSVRC2012_val_00047918.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00016946.JPEG b/tests/assets/explore_dataset/monkey/0.JPEG similarity index 100% rename from tests/assets/explore_dataset/monkey/ILSVRC2012_val_00016946.JPEG rename to tests/assets/explore_dataset/monkey/0.JPEG diff --git a/tests/assets/explore_dataset/monkey/1.JPEG b/tests/assets/explore_dataset/monkey/1.JPEG new file mode 100755 index 0000000000..65dce62178 Binary files /dev/null and b/tests/assets/explore_dataset/monkey/1.JPEG differ diff --git a/tests/assets/explore_dataset/monkey/2.JPEG b/tests/assets/explore_dataset/monkey/2.JPEG new file mode 100755 index 0000000000..65dce62178 Binary files /dev/null and b/tests/assets/explore_dataset/monkey/2.JPEG differ diff --git a/tests/assets/explore_dataset/monkey/3.JPEG b/tests/assets/explore_dataset/monkey/3.JPEG new file mode 100755 index 0000000000..65dce62178 Binary files /dev/null and b/tests/assets/explore_dataset/monkey/3.JPEG differ diff --git a/tests/assets/explore_dataset/monkey/4.JPEG b/tests/assets/explore_dataset/monkey/4.JPEG new file mode 100755 index 0000000000..65dce62178 Binary files /dev/null and b/tests/assets/explore_dataset/monkey/4.JPEG differ diff --git a/tests/assets/explore_dataset/monkey/5.JPEG b/tests/assets/explore_dataset/monkey/5.JPEG new file mode 100755 index 0000000000..65dce62178 Binary files /dev/null and b/tests/assets/explore_dataset/monkey/5.JPEG differ diff --git a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00004458.JPEG b/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00004458.JPEG deleted file mode 100755 index da5af498ee..0000000000 Binary files a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00004458.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00021490.JPEG b/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00021490.JPEG deleted file mode 100755 index e4db6fee06..0000000000 Binary files a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00021490.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00021520.JPEG b/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00021520.JPEG deleted file mode 100755 index 748e0cb50f..0000000000 Binary files a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00021520.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00044586.JPEG b/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00044586.JPEG deleted file mode 100755 index 3cd8792bf1..0000000000 Binary files a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00044586.JPEG and /dev/null differ diff --git a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00047365.JPEG b/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00047365.JPEG deleted file mode 100755 index 7b37be01a4..0000000000 Binary files a/tests/assets/explore_dataset/monkey/ILSVRC2012_val_00047365.JPEG and /dev/null differ diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/ann.csv b/tests/assets/kaggle_dataset/image_csv_multi_label/ann.csv new file mode 100644 index 0000000000..57b6540a15 --- /dev/null +++ b/tests/assets/kaggle_dataset/image_csv_multi_label/ann.csv @@ -0,0 +1,7 @@ +image_name,dog,cat,person +1.jpg,1,0,0 +2.jpg,0,1,0 +3.jpg,0,0,1 +4.jpg,1,1,0 +5.jpg,1,0,1 +6.jpg,0,1,1 diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/ann_wo_ext.csv b/tests/assets/kaggle_dataset/image_csv_multi_label/ann_wo_ext.csv new file mode 100644 index 0000000000..dd01be80e0 --- /dev/null +++ b/tests/assets/kaggle_dataset/image_csv_multi_label/ann_wo_ext.csv @@ -0,0 +1,7 @@ +image_name,dog,cat,person +1,1,0,0 +2,0,1,0 +3,0,0,1 +4,1,1,0 +5,1,0,1 +6,0,1,1 diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/images/1.jpg b/tests/assets/kaggle_dataset/image_csv_multi_label/images/1.jpg new file mode 100644 index 0000000000..8689b95631 Binary files /dev/null and b/tests/assets/kaggle_dataset/image_csv_multi_label/images/1.jpg differ diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/images/2.jpg b/tests/assets/kaggle_dataset/image_csv_multi_label/images/2.jpg new file mode 100644 index 0000000000..8689b95631 Binary files /dev/null and b/tests/assets/kaggle_dataset/image_csv_multi_label/images/2.jpg differ diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/images/3.jpg b/tests/assets/kaggle_dataset/image_csv_multi_label/images/3.jpg new file mode 100644 index 0000000000..8689b95631 Binary files /dev/null and b/tests/assets/kaggle_dataset/image_csv_multi_label/images/3.jpg differ diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/images/4.jpg b/tests/assets/kaggle_dataset/image_csv_multi_label/images/4.jpg new file mode 100644 index 0000000000..8689b95631 Binary files /dev/null and b/tests/assets/kaggle_dataset/image_csv_multi_label/images/4.jpg differ diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/images/5.jpg b/tests/assets/kaggle_dataset/image_csv_multi_label/images/5.jpg new file mode 100644 index 0000000000..8689b95631 Binary files /dev/null and b/tests/assets/kaggle_dataset/image_csv_multi_label/images/5.jpg differ diff --git a/tests/assets/kaggle_dataset/image_csv_multi_label/images/6.jpg b/tests/assets/kaggle_dataset/image_csv_multi_label/images/6.jpg new file mode 100644 index 0000000000..8689b95631 Binary files /dev/null and b/tests/assets/kaggle_dataset/image_csv_multi_label/images/6.jpg differ diff --git a/tests/assets/kitti_dataset/kitti_3d/calib/000001.txt b/tests/assets/kitti_dataset/kitti_3d/calib/000001.txt new file mode 100644 index 0000000000..367f0b263a --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d/calib/000001.txt @@ -0,0 +1,7 @@ +P0: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 0.000000000000e+00 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P1: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 -3.875744000000e+02 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P2: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 4.485728000000e+01 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 2.163791000000e-01 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 2.745884000000e-03 +P3: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 -3.395242000000e+02 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 2.199936000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 2.729905000000e-03 +R0_rect: 9.999239000000e-01 9.837760000000e-03 -7.445048000000e-03 -9.869795000000e-03 9.999421000000e-01 -4.278459000000e-03 7.402527000000e-03 4.351614000000e-03 9.999631000000e-01 +Tr_velo_to_cam: 7.533745000000e-03 -9.999714000000e-01 -6.166020000000e-04 -4.069766000000e-03 1.480249000000e-02 7.280733000000e-04 -9.998902000000e-01 -7.631618000000e-02 9.998621000000e-01 7.523790000000e-03 1.480755000000e-02 -2.717806000000e-01 +Tr_imu_to_velo: 9.999976000000e-01 7.553071000000e-04 -2.035826000000e-03 -8.086759000000e-01 -7.854027000000e-04 9.998898000000e-01 -1.482298000000e-02 3.195559000000e-01 2.024406000000e-03 1.482454000000e-02 9.998881000000e-01 -7.997231000000e-01 \ No newline at end of file diff --git a/tests/assets/kitti_dataset/kitti_3d/image_2/000001.png b/tests/assets/kitti_dataset/kitti_3d/image_2/000001.png new file mode 100644 index 0000000000..e6f3cff877 Binary files /dev/null and b/tests/assets/kitti_dataset/kitti_3d/image_2/000001.png differ diff --git a/tests/assets/kitti_dataset/kitti_3d/label_2/000001.txt b/tests/assets/kitti_dataset/kitti_3d/label_2/000001.txt new file mode 100644 index 0000000000..a2fdc0fa6f --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d/label_2/000001.txt @@ -0,0 +1,3 @@ +Truck 0.00 0 -1.57 600 150 630 190 2.85 2.63 12.34 0.47 1.49 69.44 -1.56 +Car 0.00 3 -1.65 650 160 700 200 1.86 0.60 2.02 4.59 1.32 45.84 -1.55 +DontCare -1 -1 -10 500 170 590 190 -1 -1 -1 -1000 -1000 -1000 -10 diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/test/000002.txt b/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/test/000002.txt new file mode 100755 index 0000000000..2b8496d5be --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/test/000002.txt @@ -0,0 +1,7 @@ +P0: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 0.000000000000e+00 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P1: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 -3.875744000000e+02 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P2: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 4.485728000000e+01 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 2.163791000000e-01 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 2.745884000000e-03 +P3: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 -3.395242000000e+02 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 2.199936000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 2.729905000000e-03 +R0_rect: 9.999239000000e-01 9.837760000000e-03 -7.445048000000e-03 -9.869795000000e-03 9.999421000000e-01 -4.278459000000e-03 7.402527000000e-03 4.351614000000e-03 9.999631000000e-01 +Tr_velo_to_cam: 7.533745000000e-03 -9.999714000000e-01 -6.166020000000e-04 -4.069766000000e-03 1.480249000000e-02 7.280733000000e-04 -9.998902000000e-01 -7.631618000000e-02 9.998621000000e-01 7.523790000000e-03 1.480755000000e-02 -2.717806000000e-01 +Tr_imu_to_velo: 9.999976000000e-01 7.553071000000e-04 -2.035826000000e-03 -8.086759000000e-01 -7.854027000000e-04 9.998898000000e-01 -1.482298000000e-02 3.195559000000e-01 2.024406000000e-03 1.482454000000e-02 9.998881000000e-01 -7.997231000000e-01 diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/train/000000.txt b/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/train/000000.txt new file mode 100755 index 0000000000..108a6b1170 --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/train/000000.txt @@ -0,0 +1,7 @@ +P0: 7.070493000000e+02 0.000000000000e+00 6.040814000000e+02 0.000000000000e+00 0.000000000000e+00 7.070493000000e+02 1.805066000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P1: 7.070493000000e+02 0.000000000000e+00 6.040814000000e+02 -3.797842000000e+02 0.000000000000e+00 7.070493000000e+02 1.805066000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P2: 7.070493000000e+02 0.000000000000e+00 6.040814000000e+02 4.575831000000e+01 0.000000000000e+00 7.070493000000e+02 1.805066000000e+02 -3.454157000000e-01 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 4.981016000000e-03 +P3: 7.070493000000e+02 0.000000000000e+00 6.040814000000e+02 -3.341081000000e+02 0.000000000000e+00 7.070493000000e+02 1.805066000000e+02 2.330660000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 3.201153000000e-03 +R0_rect: 9.999128000000e-01 1.009263000000e-02 -8.511932000000e-03 -1.012729000000e-02 9.999406000000e-01 -4.037671000000e-03 8.470675000000e-03 4.123522000000e-03 9.999556000000e-01 +Tr_velo_to_cam: 6.927964000000e-03 -9.999722000000e-01 -2.757829000000e-03 -2.457729000000e-02 -1.162982000000e-03 2.749836000000e-03 -9.999955000000e-01 -6.127237000000e-02 9.999753000000e-01 6.931141000000e-03 -1.143899000000e-03 -3.321029000000e-01 +Tr_imu_to_velo: 9.999976000000e-01 7.553071000000e-04 -2.035826000000e-03 -8.086759000000e-01 -7.854027000000e-04 9.998898000000e-01 -1.482298000000e-02 3.195559000000e-01 2.024406000000e-03 1.482454000000e-02 9.998881000000e-01 -7.997231000000e-01 diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/val/000001.txt b/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/val/000001.txt new file mode 100755 index 0000000000..2b8496d5be --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d_with_subset/calib/val/000001.txt @@ -0,0 +1,7 @@ +P0: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 0.000000000000e+00 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P1: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 -3.875744000000e+02 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 0.000000000000e+00 +P2: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 4.485728000000e+01 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 2.163791000000e-01 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 2.745884000000e-03 +P3: 7.215377000000e+02 0.000000000000e+00 6.095593000000e+02 -3.395242000000e+02 0.000000000000e+00 7.215377000000e+02 1.728540000000e+02 2.199936000000e+00 0.000000000000e+00 0.000000000000e+00 1.000000000000e+00 2.729905000000e-03 +R0_rect: 9.999239000000e-01 9.837760000000e-03 -7.445048000000e-03 -9.869795000000e-03 9.999421000000e-01 -4.278459000000e-03 7.402527000000e-03 4.351614000000e-03 9.999631000000e-01 +Tr_velo_to_cam: 7.533745000000e-03 -9.999714000000e-01 -6.166020000000e-04 -4.069766000000e-03 1.480249000000e-02 7.280733000000e-04 -9.998902000000e-01 -7.631618000000e-02 9.998621000000e-01 7.523790000000e-03 1.480755000000e-02 -2.717806000000e-01 +Tr_imu_to_velo: 9.999976000000e-01 7.553071000000e-04 -2.035826000000e-03 -8.086759000000e-01 -7.854027000000e-04 9.998898000000e-01 -1.482298000000e-02 3.195559000000e-01 2.024406000000e-03 1.482454000000e-02 9.998881000000e-01 -7.997231000000e-01 diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/test/000002.png b/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/test/000002.png new file mode 100755 index 0000000000..e6f3cff877 Binary files /dev/null and b/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/test/000002.png differ diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/train/000000.png b/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/train/000000.png new file mode 100755 index 0000000000..e6f3cff877 Binary files /dev/null and b/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/train/000000.png differ diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/val/000001.png b/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/val/000001.png new file mode 100755 index 0000000000..e6f3cff877 Binary files /dev/null and b/tests/assets/kitti_dataset/kitti_3d_with_subset/image_2/val/000001.png differ diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/test/000002.txt b/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/test/000002.txt new file mode 100644 index 0000000000..3aca4b3e55 --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/test/000002.txt @@ -0,0 +1,2 @@ +Car 0.88 3 -0.69 0 190 400 380 1.60 1.57 3.23 -2.70 1.74 3.68 -1.29 +DontCare -1 -1 -10 800 160 825 185 -1 -1 -1 -1000 -1000 -1000 -10 diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/train/000000.txt b/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/train/000000.txt new file mode 100644 index 0000000000..9d035bc092 --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/train/000000.txt @@ -0,0 +1 @@ +Pedestrian 0.00 0 -0.20 700 150 800 300 1.89 0.48 1.20 1.84 1.47 8.41 0.01 diff --git a/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/val/000001.txt b/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/val/000001.txt new file mode 100644 index 0000000000..2bac65fc4a --- /dev/null +++ b/tests/assets/kitti_dataset/kitti_3d_with_subset/label_2/val/000001.txt @@ -0,0 +1,2 @@ +Pedestrian 0.00 0 1.94 330 180 360 240 1.87 0.96 0.65 -8.50 2.07 23.02 1.59 +DontCare -1 -1 -10 600 170 620 185 -1 -1 -1 -1000 -1000 -1000 -10 diff --git a/tests/integration/cli/test_kitti_raw_format.py b/tests/integration/cli/test_kitti_raw_format.py index 884cc02708..f7810bc981 100644 --- a/tests/integration/cli/test_kitti_raw_format.py +++ b/tests/integration/cli/test_kitti_raw_format.py @@ -33,13 +33,13 @@ def test_can_convert_to_kitti_raw(self): annotations=[ Cuboid3d( position=[1, 2, 3], - scale=[7.95, -3.62, -1.03], + scale=[-3.62, 7.95, -1.03], label=1, attributes={"occluded": False, "track_id": 1}, ), Cuboid3d( position=[1, 1, 0], - scale=[8.34, 23.01, -0.76], + scale=[23.01, 8.34, -0.76], label=0, attributes={"occluded": False, "track_id": 2}, ), @@ -65,7 +65,7 @@ def test_can_convert_to_kitti_raw(self): annotations=[ Cuboid3d( position=[0, 1, 0], - scale=[8.34, 23.01, -0.76], + scale=[23.01, 8.34, -0.76], rotation=[1, 1, 3], label=0, attributes={"occluded": True, "track_id": 2}, @@ -92,7 +92,7 @@ def test_can_convert_to_kitti_raw(self): annotations=[ Cuboid3d( position=[1, 2, 3], - scale=[-9.41, 13.54, 0.24], + scale=[13.54, -9.41, 0.24], label=1, attributes={"occluded": False, "track_id": 3}, ) diff --git a/tests/unit/components/test_transformer.py b/tests/unit/components/test_transformer.py index 197e6b317a..2055e01fe0 100644 --- a/tests/unit/components/test_transformer.py +++ b/tests/unit/components/test_transformer.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -from typing import List, Tuple +from typing import List, Optional, Tuple import pytest @@ -11,7 +11,7 @@ from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem from datumaro.components.launcher import Launcher -from datumaro.components.transformer import ModelTransform +from datumaro.components.transformer import ModelTransform, TabularTransform class MockLauncher(Launcher): @@ -64,3 +64,30 @@ def test_model_transform( assert item.annotations == [Annotation(id=0), Annotation(id=1)] else: assert item.annotations == [Annotation(id=1)] + + +class TabularTransformTest: + @pytest.fixture + def fxt_dataset(self): + return Dataset.from_iterable( + [DatasetItem(id=f"item_{i}", annotations=[Annotation(id=0)]) for i in range(10)] + ) + + @pytest.mark.parametrize("batch_size", [1, 10]) + @pytest.mark.parametrize("num_workers", [0, 2]) + def test_tabular_transform(self, fxt_dataset, batch_size, num_workers): + class MockTabularTransform(TabularTransform): + def transform_item(self, item: DatasetItem) -> Optional[DatasetItem]: + # Mock transformation logic + item.annotations.append(Annotation(id=1)) + return item + + transform = MockTabularTransform( + extractor=fxt_dataset, + batch_size=batch_size, + num_workers=num_workers, + ) + + for idx, item in enumerate(transform): + assert item.id == f"item_{idx}" + assert item.annotations == [Annotation(id=0), Annotation(id=1)] diff --git a/tests/unit/data_formats/conftest.py b/tests/unit/data_formats/conftest.py index c4c8c32f95..12351ee037 100644 --- a/tests/unit/data_formats/conftest.py +++ b/tests/unit/data_formats/conftest.py @@ -10,6 +10,8 @@ from datumaro import Dataset +from tests.utils.test_utils import TestDir + @pytest.fixture def fxt_dummy_dataset(): @@ -35,12 +37,12 @@ def fxt_export_kwargs(): @pytest.fixture def fxt_dataset_dir_with_subset_dirs(test_dir: str, request: pytest.FixtureRequest): fxt_dataset_dir = request.param + with TestDir(f"{test_dir}_with_subsets") as new_test_dir: + for subset in ["train", "val", "test"]: + dst = os.path.join(new_test_dir, subset) + shutil.copytree(fxt_dataset_dir, dst) - for subset in ["train", "val", "test"]: - dst = os.path.join(test_dir, subset) - shutil.copytree(fxt_dataset_dir, dst) - - yield test_dir + yield new_test_dir @pytest.fixture diff --git a/tests/unit/data_formats/datumaro/conftest.py b/tests/unit/data_formats/datumaro/conftest.py index e600ae957c..71fc8b1cd0 100644 --- a/tests/unit/data_formats/datumaro/conftest.py +++ b/tests/unit/data_formats/datumaro/conftest.py @@ -15,6 +15,7 @@ AnnotationType, Bbox, Caption, + Cuboid2D, Cuboid3d, Ellipse, Label, @@ -90,7 +91,7 @@ def fxt_test_datumaro_format_dataset(): }, ), Points( - [1, 2, 2, 0, 1, 1], + [1, 2, 0, 0, 1, 1], label=0, id=5, z_order=4, @@ -98,6 +99,7 @@ def fxt_test_datumaro_format_dataset(): "x": 1, "y": "2", }, + visibility=[1, 0, 2], ), Mask( label=3, @@ -122,6 +124,25 @@ def fxt_test_datumaro_format_dataset(): "y": "2", }, ), + Cuboid2D( + [ + (1, 1), + (3, 1), + (3, 3), + (1, 3), + (1.5, 1.5), + (3.5, 1.5), + (3.5, 3.5), + (1.5, 3.5), + ], + label=3, + id=5, + z_order=2, + attributes={ + "x": 1, + "y": "2", + }, + ), ], ), DatasetItem( @@ -201,6 +222,191 @@ def fxt_test_datumaro_format_dataset(): ) +@pytest.fixture +def fxt_test_datumaro_format_dataset_with_path_separator(): + label_categories = LabelCategories(attributes={"a", "b", "score"}) + for i in range(5): + label_categories.add("cat" + str(i), attributes={"x", "y"}) + + mask_categories = MaskCategories(generate_colormap(len(label_categories.items))) + + points_categories = PointsCategories() + for index, _ in enumerate(label_categories.items): + points_categories.add(index, ["cat1", "cat2"], joints=[[0, 1]]) + + sep = os.path.sep + return Dataset.from_iterable( + [ + DatasetItem( + id="100/0", + subset=f"my{sep}train", + media=Image.from_numpy(data=np.ones((10, 6, 3))), + annotations=[ + Caption("hello", id=1), + Caption("world", id=2, group=5), + Label( + 2, + id=3, + attributes={ + "x": 1, + "y": "2", + }, + ), + Bbox( + 1, + 2, + 3, + 4, + label=4, + id=4, + z_order=1, + attributes={ + "score": 1.0, + }, + ), + Bbox( + 5, + 6, + 7, + 8, + id=5, + group=5, + attributes={ + "a": 1.5, + "b": "text", + }, + ), + Points( + [1, 2, 2, 0, 1, 1], + label=0, + id=5, + z_order=4, + attributes={ + "x": 1, + "y": "2", + }, + ), + Mask( + label=3, + id=5, + z_order=2, + image=np.ones((2, 3)), + attributes={ + "x": 1, + "y": "2", + }, + ), + Ellipse( + 5, + 6, + 7, + 8, + label=3, + id=5, + z_order=2, + attributes={ + "x": 1, + "y": "2", + }, + ), + Cuboid2D( + [ + (1, 1), + (3, 1), + (3, 3), + (1, 3), + (1.5, 1.5), + (3.5, 1.5), + (3.5, 3.5), + (1.5, 3.5), + ], + label=3, + id=5, + z_order=2, + attributes={ + "x": 1, + "y": "2", + }, + ), + ], + ), + DatasetItem( + id=21, + media=Image.from_numpy(data=np.ones((10, 6, 3))), + subset="train", + annotations=[ + Caption("test"), + Label(2), + Bbox(1, 2, 3, 4, label=5, id=42, group=42), + ], + ), + DatasetItem( + id=2, + media=Image.from_numpy(data=np.ones((10, 6, 3))), + subset=f"my{sep}val", + annotations=[ + PolyLine([1, 2, 3, 4, 5, 6, 7, 8], id=11, z_order=1), + Polygon([1, 2, 3, 4, 5, 6, 7, 8], id=12, z_order=4), + ], + ), + DatasetItem( + id="1/1", + media=Image.from_numpy(data=np.ones((10, 6, 3))), + subset="test", + annotations=[ + Cuboid3d( + [1.0, 2.0, 3.0], + [2.0, 2.0, 4.0], + [1.0, 3.0, 4.0], + id=6, + label=0, + attributes={"occluded": True}, + group=6, + ) + ], + ), + DatasetItem( + id=42, + media=Image.from_numpy(data=np.ones((10, 6, 3))), + subset=f"my{sep}test", + attributes={"a1": 5, "a2": "42"}, + ), + DatasetItem( + id=42, + media=Image.from_numpy(data=np.ones((10, 6, 3))), + # id and group integer value can be higher than 32bits limits (COCO instances). + annotations=[ + Mask( + id=900100087038, group=900100087038, image=np.ones((2, 3), dtype=np.uint8) + ), + RleMask( + rle=mask_tools.encode(np.ones((2, 3), dtype=np.uint8, order="F")), + id=900100087038, + group=900100087038, + ), + ], + ), + DatasetItem( + id="1/b/c", + media=Image.from_file(path="1/b/c.qq", size=(2, 4)), + ), + ], + categories={ + AnnotationType.label: label_categories, + AnnotationType.mask: mask_categories, + AnnotationType.points: points_categories, + }, + infos={ + "string": "test", + "int": 0, + "float": 0.0, + "string_list": ["test0", "test1", "test2"], + "int_list": [0, 1, 2], + "float_list": [0.0, 0.1, 0.2], + }, + ) + + @pytest.fixture def fxt_test_datumaro_format_video_dataset(test_dir) -> Dataset: video_path = osp.join(test_dir, "video.avi") diff --git a/tests/unit/data_formats/datumaro/test_datumaro_format.py b/tests/unit/data_formats/datumaro/test_datumaro_format.py index 2492c072b6..bb03455b0d 100644 --- a/tests/unit/data_formats/datumaro/test_datumaro_format.py +++ b/tests/unit/data_formats/datumaro/test_datumaro_format.py @@ -14,6 +14,7 @@ from datumaro.components.dataset_base import DatasetItem from datumaro.components.environment import Environment +from datumaro.components.errors import PathSeparatorInSubsetNameError from datumaro.components.importer import DatasetImportError from datumaro.components.media import Image from datumaro.components.project import Dataset @@ -155,6 +156,31 @@ def test_can_save_and_load( stream=stream, ) + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + @pytest.mark.parametrize("require_media", [True, False]) + @pytest.mark.parametrize("stream", [True, False]) + def test_cannot_export_dataset_with_subset_containing_path_separators( + self, + fxt_test_datumaro_format_dataset_with_path_separator, + test_dir, + fxt_import_kwargs, + fxt_export_kwargs, + stream, + require_media, + helper_tc, + ): + with pytest.raises(PathSeparatorInSubsetNameError): + self._test_save_and_load( + helper_tc, + fxt_test_datumaro_format_dataset_with_path_separator, + partial(self.exporter.convert, save_media=True, stream=stream, **fxt_export_kwargs), + test_dir, + compare=compare_datasets, + require_media=require_media, + importer_args=fxt_import_kwargs, + stream=stream, + ) + @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_export_video_only_once( self, diff --git a/tests/unit/data_formats/test_common_semantic_segmentation_format.py b/tests/unit/data_formats/test_common_semantic_segmentation_format.py index b7297e4885..1d03e9ba01 100644 --- a/tests/unit/data_formats/test_common_semantic_segmentation_format.py +++ b/tests/unit/data_formats/test_common_semantic_segmentation_format.py @@ -2,8 +2,10 @@ # # SPDX-License-Identifier: MIT +import os +import shutil from collections import OrderedDict -from typing import Any, Dict, Optional +from typing import Any, Dict import numpy as np import pytest @@ -11,6 +13,7 @@ from datumaro.components.annotation import Mask from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem +from datumaro.components.errors import DatasetImportError from datumaro.components.media import Image from datumaro.plugins.data_formats.common_semantic_segmentation import ( CommonSemanticSegmentationImporter, @@ -143,6 +146,40 @@ def test_can_import( fxt_dataset_dir, fxt_expected_dataset, fxt_import_kwargs, request ) + @pytest.mark.parametrize( + ["fxt_dataset_dir", "fxt_expected_dataset", "fxt_import_kwargs"], + [ + (DUMMY_DATASET_DIR, "fxt_dataset", {}), + ( + DUMMY_NON_STANDARD_DATASET_DIR, + "fxt_non_standard_dataset", + {"image_prefix": "image_", "mask_prefix": "gt_"}, + ), + ], + indirect=["fxt_expected_dataset"], + ids=IDS, + ) + def test_cannot_import_nested( + self, + fxt_dataset_dir: str, + fxt_expected_dataset: Dataset, + fxt_import_kwargs: Dict[str, Any], + request: pytest.FixtureRequest, + test_dir: str, + ): + shutil.copytree(fxt_dataset_dir, test_dir, dirs_exist_ok=True) + subdir_name = "subdir" + subdir = os.path.join(test_dir, subdir_name) + os.makedirs(subdir) + for _file in os.listdir(test_dir): + if _file != subdir_name: + file_path = os.path.join(test_dir, _file) + shutil.move(file_path, subdir) + with pytest.raises(DatasetImportError) as exc_info: + super().test_can_import(test_dir, fxt_expected_dataset, fxt_import_kwargs, request) + assert exc_info.value.__cause__ is not None + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + class CommonSemanticSegmentationWithSubsetDirsImporterTest(TestDataFormatBase): IMPORTER = CommonSemanticSegmentationWithSubsetDirsImporter diff --git a/tests/unit/data_formats/test_kaggle.py b/tests/unit/data_formats/test_kaggle.py index 90071c71fc..262c741e74 100644 --- a/tests/unit/data_formats/test_kaggle.py +++ b/tests/unit/data_formats/test_kaggle.py @@ -20,6 +20,9 @@ from tests.utils.test_utils import compare_datasets DUMMY_DATASET_IMAGE_CSV_DIR = get_test_asset_path("kaggle_dataset", "image_csv") +DUMMY_DATASET_IMAGE_CSV_MULTI_LB_DIR = get_test_asset_path( + "kaggle_dataset", "image_csv_multi_label" +) DUMMY_DATASET_IMAGE_CSV_DET_DIR = get_test_asset_path("kaggle_dataset", "image_csv_det") DUMMY_DATASET_IMAGE_TXT_DIR = get_test_asset_path("kaggle_dataset", "image_txt") DUMMY_DATASET_IMAGE_TXT_DET_DIR = get_test_asset_path("kaggle_dataset", "image_txt_det") @@ -72,6 +75,51 @@ def fxt_img_dataset() -> Dataset: ) +@pytest.fixture +def fxt_img_multi_label_dataset() -> Dataset: + return Dataset.from_iterable( + [ + DatasetItem( + id="1", + subset="default", + media=Image.from_numpy(data=np.ones((5, 10, 3))), + annotations=[Label(label=0)], + ), + DatasetItem( + id="2", + subset="default", + media=Image.from_numpy(data=np.ones((5, 10, 3))), + annotations=[Label(label=1)], + ), + DatasetItem( + id="3", + subset="default", + media=Image.from_numpy(data=np.ones((5, 10, 3))), + annotations=[Label(label=2)], + ), + DatasetItem( + id="4", + subset="default", + media=Image.from_numpy(data=np.ones((5, 10, 3))), + annotations=[Label(label=0), Label(label=1)], + ), + DatasetItem( + id="5", + subset="default", + media=Image.from_numpy(data=np.ones((5, 10, 3))), + annotations=[Label(label=0), Label(label=2)], + ), + DatasetItem( + id="6", + subset="default", + media=Image.from_numpy(data=np.ones((5, 10, 3))), + annotations=[Label(label=1), Label(label=2)], + ), + ], + categories=["dog", "cat", "person"], + ) + + @pytest.fixture def fxt_img_det_dataset() -> Dataset: return Dataset.from_iterable( @@ -321,6 +369,8 @@ def fxt_coco_dataset() -> Dataset: IDS = [ "IMAGE_CSV", "IMAGE_CSV_WO_EXT", + "IMAGE_CSV_MULTI_LB", + "IMAGE_CSV_MULTI_LB_WO_EXT", "IMAGE_CSV_DET", "IMAGE_CSV_DET2", "IMAGE_CSV_DET3", @@ -372,6 +422,26 @@ def test_can_detect(self, fxt_dataset_dir: str): "columns": {"media": "image_name", "label": "label_name"}, }, ), + ( + DUMMY_DATASET_IMAGE_CSV_MULTI_LB_DIR, + "images", + "fxt_img_multi_label_dataset", + KaggleImageCsvBase, + { + "ann_file": osp.join(DUMMY_DATASET_IMAGE_CSV_MULTI_LB_DIR, "ann.csv"), + "columns": {"media": "image_name", "label": ["dog", "cat", "person"]}, + }, + ), + ( + DUMMY_DATASET_IMAGE_CSV_MULTI_LB_DIR, + "images", + "fxt_img_multi_label_dataset", + KaggleImageCsvBase, + { + "ann_file": osp.join(DUMMY_DATASET_IMAGE_CSV_MULTI_LB_DIR, "ann_wo_ext.csv"), + "columns": {"media": "image_name", "label": ["dog", "cat", "person"]}, + }, + ), ( DUMMY_DATASET_IMAGE_CSV_DET_DIR, "images", diff --git a/tests/unit/operations/test_statistics.py b/tests/unit/operations/test_statistics.py index bb92c53308..7f28be820a 100644 --- a/tests/unit/operations/test_statistics.py +++ b/tests/unit/operations/test_statistics.py @@ -10,7 +10,16 @@ import numpy as np import pytest -from datumaro.components.annotation import Bbox, Caption, Ellipse, Label, Mask, Points, RotatedBbox +from datumaro.components.annotation import ( + Bbox, + Caption, + Cuboid2D, + Ellipse, + Label, + Mask, + Points, + RotatedBbox, +) from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem from datumaro.components.errors import DatumaroError @@ -232,6 +241,25 @@ def test_stats(self): "tiny": True, }, ), + Cuboid2D( + [ + (1, 1), + (3, 1), + (3, 3), + (1, 3), + (1.5, 1.5), + (3.5, 1.5), + (3.5, 3.5), + (1.5, 3.5), + ], + label=3, + id=5, + z_order=2, + attributes={ + "x": 1, + "y": "2", + }, + ), ], ), DatasetItem(id=3), @@ -242,7 +270,7 @@ def test_stats(self): expected = { "images count": 4, - "annotations count": 12, + "annotations count": 13, "unannotated images count": 2, "unannotated images": ["3", "2.2"], "annotations by type": { @@ -277,33 +305,34 @@ def test_stats(self): "hash_key": {"count": 0}, "feature_vector": {"count": 0}, "tabular": {"count": 0}, + "cuboid_2d": {"count": 1}, "unknown": {"count": 0}, }, "annotations": { "labels": { - "count": 6, + "count": 7, "distribution": { - "label_0": [1, 1 / 6], + "label_0": [1, 1 / 7], "label_1": [0, 0.0], - "label_2": [3, 3 / 6], - "label_3": [2, 2 / 6], + "label_2": [3, 3 / 7], + "label_3": [3, 3 / 7], }, "attributes": { "x": { - "count": 2, # annotations with no label are skipped + "count": 3, # annotations with no label are skipped "values count": 2, "values present": ["1", "2"], "distribution": { - "1": [1, 1 / 2], - "2": [1, 1 / 2], + "1": [2, 2 / 3], + "2": [1, 1 / 3], }, }, "y": { - "count": 2, # annotations with no label are skipped + "count": 3, # annotations with no label are skipped "values count": 1, "values present": ["2"], "distribution": { - "2": [2, 2 / 2], + "2": [3, 3 / 3], }, }, # must not include "special" attributes like "occluded" @@ -403,6 +432,7 @@ def _get_stats_template(label_names: list): "feature_vector": {"count": 0}, "tabular": {"count": 0}, "rotated_bbox": {"count": 0}, + "cuboid_2d": {"count": 0}, "unknown": {"count": 0}, }, "annotations": { diff --git a/tests/unit/test_annotation.py b/tests/unit/test_annotation.py index ddcd718bf7..d1a6824e94 100644 --- a/tests/unit/test_annotation.py +++ b/tests/unit/test_annotation.py @@ -13,6 +13,7 @@ from datumaro.components.annotation import ( Annotations, + Cuboid2D, Ellipse, ExtractedMask, HashKey, @@ -45,7 +46,7 @@ def test_get_points(self, fxt_ellipses: List[Ellipse]): class HashKeyTest: @pytest.fixture def fxt_hashkeys_same(self): - hash_key = np.random.randint(0, 256, size=(64,), dtype=np.uint8) + hash_key = np.random.randint(0, 256, size=(96,), dtype=np.uint8) hashkey1 = HashKey(hash_key=hash_key) hashkey2 = HashKey(hash_key=hash_key) return hashkey1, hashkey2 @@ -53,8 +54,8 @@ def fxt_hashkeys_same(self): @pytest.fixture def fxt_hashkeys_diff(self): np.random.seed(3003) - hashkey1 = HashKey(hash_key=np.random.randint(0, 256, size=(64,), dtype=np.uint8)) - hashkey2 = HashKey(hash_key=np.random.randint(0, 256, size=(64,), dtype=np.uint8)) + hashkey1 = HashKey(hash_key=np.random.randint(0, 256, size=(96,), dtype=np.uint8)) + hashkey2 = HashKey(hash_key=np.random.randint(0, 256, size=(96,), dtype=np.uint8)) return hashkey1, hashkey2 @pytest.mark.parametrize( @@ -62,7 +63,7 @@ def fxt_hashkeys_diff(self): ) def test_compare_hashkey(self, fxt_hashkeys, expected, request): hashkey1, hashkey2 = request.getfixturevalue(fxt_hashkeys) - assert (expected, hashkey1 == hashkey2) + assert (hashkey1 == hashkey2) == expected class RotatedBboxTest: @@ -142,3 +143,66 @@ def test_get_semantic_seg_mask_binary_mask(self, fxt_index_mask, dtype): semantic_seg_mask = annotations.get_semantic_seg_mask(ignore_index=255, dtype=dtype) assert np.allclose(semantic_seg_mask, fxt_index_mask) + + +class Cuboid2DTest: + @pytest.fixture + def fxt_cuboid_2d(self): + return Cuboid2D( + [ + (684.172, 237.810), + (750.486, 237.673), + (803.791, 256.313), + (714.712, 256.542), + (684.035, 174.227), + (750.263, 174.203), + (803.399, 170.990), + (714.476, 171.015), + ], + y_3d=1.49, + ) + + @pytest.fixture + def fxt_kitti_data(self): + dimensions = np.array([1.49, 1.56, 4.34]) + location = np.array([2.51, 1.49, 14.75]) + rot_y = -1.59 + + return dimensions, location, rot_y + + @pytest.fixture + def fxt_P2(self): + return np.array( + [ + [7.215377000000e02, 0.000000000000e00, 6.095593000000e02, 4.485728000000e01], + [0.000000000000e00, 7.215377000000e02, 1.728540000000e02, 2.163791000000e-01], + [0.000000000000e00, 0.000000000000e00, 1.000000000000e00, 2.745884000000e-03], + ] + ) + + @pytest.fixture + def fxt_velo_to_cam(self): + return np.array( + [ + [7.533745000000e-03, -9.999714000000e-01, -6.166020000000e-04, -4.069766000000e-03], + [1.480249000000e-02, 7.280733000000e-04, -9.998902000000e-01, -7.631618000000e-02], + [9.998621000000e-01, 7.523790000000e-03, 1.480755000000e-02, -2.717806000000e-01], + ] + ) + + def test_create_from_3d(self, fxt_kitti_data, fxt_cuboid_2d, fxt_P2, fxt_velo_to_cam): + actual = Cuboid2D.from_3d( + dim=fxt_kitti_data[0], + location=fxt_kitti_data[1], + rotation_y=fxt_kitti_data[2], + P=fxt_P2, + Tr_velo_to_cam=fxt_velo_to_cam, + ) + + assert np.allclose(actual.points, fxt_cuboid_2d.points, atol=1e-3) + + def test_to_3d(self, fxt_kitti_data, fxt_cuboid_2d, fxt_P2): + P_inv = np.linalg.pinv(fxt_P2) + actual = fxt_cuboid_2d.to_3d(P_inv) + for act, exp in zip(actual, fxt_kitti_data): + assert np.allclose(act, exp, atol=2) diff --git a/tests/unit/test_environment.py b/tests/unit/test_environment.py index 11f08ae386..5fed7cd8ed 100644 --- a/tests/unit/test_environment.py +++ b/tests/unit/test_environment.py @@ -5,7 +5,8 @@ import pytest import datumaro.components.lazy_plugin -from datumaro.components.environment import Environment, PluginRegistry +from datumaro.components.environment import DEFAULT_ENVIRONMENT, Environment, PluginRegistry +from datumaro.components.exporter import Exporter real_find_spec = datumaro.components.lazy_plugin.find_spec @@ -77,3 +78,17 @@ def test_extra_deps_req(self, fxt_tf_failure_env): ) assert "tf_detection_api" not in loaded_plugin_names + + def test_merge_default_env(self): + merged_env = Environment.merge([DEFAULT_ENVIRONMENT, DEFAULT_ENVIRONMENT]) + assert merged_env is DEFAULT_ENVIRONMENT + + def test_merge_custom_env(self): + class TestPlugin(Exporter): + pass + + envs = [Environment(), Environment()] + envs[0].exporters.register("test_plugin", TestPlugin) + + merged = Environment.merge(envs) + assert "test_plugin" in merged.exporters diff --git a/tests/unit/test_explorer.py b/tests/unit/test_explorer.py index dc0ae61e0e..9cf78b5773 100644 --- a/tests/unit/test_explorer.py +++ b/tests/unit/test_explorer.py @@ -1,21 +1,17 @@ -import os.path as osp -from copy import deepcopy -from functools import partial from unittest import TestCase +from unittest.mock import patch import numpy as np from datumaro.components.algorithms.hash_key_inference.explorer import Explorer -from datumaro.components.annotation import AnnotationType, Caption, Label +from datumaro.components.annotation import AnnotationType, HashKey from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem from datumaro.components.errors import MediaTypeError -from datumaro.components.media import Image -from datumaro.plugins.data_formats.datumaro.exporter import DatumaroExporter +from datumaro.util.meta_file_util import load_hash_key from tests.requirements import Requirements, mark_requirement from tests.utils.assets import get_test_asset_path -from tests.utils.test_utils import TestDir class ExplorerTest(TestCase): @@ -171,3 +167,71 @@ def test_pointcloud_assert(self): with self.assertRaises(MediaTypeError) as capture: Explorer(imported_dataset) self.assertIn("PointCloud", str(capture.exception)) + + +class MetaFileTest(TestCase): + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_no_hashkey_dir(self): + """ + Test that the function returns the original dataset if the hashkey directory doesn't exist. + """ + dataset = [DatasetItem(id="000001", subset="test")] + with patch("os.path.isdir") as mock_isdir: + mock_isdir.return_value = False + result = load_hash_key("invalid_path", dataset) + self.assertEqual(result, dataset) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_no_hashkey_file(self): + """ + Test that the function returns the original dataset if the hashkey file doesn't exist. + """ + dataset = [DatasetItem(id="000001", subset="test")] + with patch("os.path.isdir") as mock_isdir, patch( + "datumaro.util.meta_file_util.has_hashkey_file" + ) as mock_has_hashkey_file: + mock_isdir.return_value = True + mock_has_hashkey_file.return_value = False + result = load_hash_key("hashkey_dir", dataset) + self.assertEqual(result, dataset) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_load_hash_key(self): + """ + Test that the function successfully parses the hashkey file and adds HashKey annotations to the dataset items. + """ + dataset = [ + DatasetItem(id="000001", subset="train", annotations=[]), + DatasetItem(id="000002", subset="val", annotations=[]), + ] + expected_hashkey1 = np.ones((96,), dtype=np.uint8) + expected_hashkey2 = np.zeros((96,), dtype=np.uint8) + hashkey_dict = { + "train/000001": expected_hashkey1.tolist(), + "val/000002": expected_hashkey2.tolist(), + } + + with patch("os.path.isdir") as mock_isdir, patch( + "datumaro.util.meta_file_util.has_hashkey_file" + ) as mock_has_hashkey_file, patch( + "datumaro.util.meta_file_util.parse_hashkey_file" + ) as mock_parse_hashkey_file: + mock_isdir.return_value = True + mock_has_hashkey_file.return_value = True + mock_parse_hashkey_file.return_value = hashkey_dict + + result = load_hash_key("hashkey_dir", dataset) + + self.assertEqual(len(result), len(dataset)) + self.assertEqual(result[0].id, dataset[0].id) + self.assertEqual(result[0].subset, dataset[0].subset) + + # Check if HashKey annotations are added + self.assertEqual(len(result[0].annotations), 1) + self.assertIsInstance(result[0].annotations[0], HashKey) + self.assertTrue(np.array_equal(result[0].annotations[0].hash_key, expected_hashkey1)) + + # Check if HashKey annotations are added for the second item as well + self.assertEqual(len(result[1].annotations), 1) + self.assertIsInstance(result[1].annotations[0], HashKey) + self.assertTrue(np.array_equal(result[1].annotations[0].hash_key, expected_hashkey2)) diff --git a/tests/unit/test_framework_converter.py b/tests/unit/test_framework_converter.py index 0933884293..83fd9a97c5 100644 --- a/tests/unit/test_framework_converter.py +++ b/tests/unit/test_framework_converter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2023-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -13,14 +13,16 @@ from datumaro.components.annotation import ( AnnotationType, Bbox, + Caption, Label, LabelCategories, Mask, Polygon, + Tabular, ) from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem -from datumaro.components.media import Image +from datumaro.components.media import Image, Table, TableRow from datumaro.plugins.framework_converter import ( TASK_ANN_TYPE, DmTfDataset, @@ -36,6 +38,8 @@ try: import torch + from torchtext.data.utils import get_tokenizer + from torchtext.vocab import build_vocab_from_iterator from torchvision import datasets, transforms except ImportError: TORCH_AVAILABLE = False @@ -142,6 +146,89 @@ def fxt_dataset(): ) +@pytest.fixture +def fxt_tabular_label_dataset(): + table = Table.from_list( + [ + { + "label": 1, + "text": "I rented I AM CURIOUS-YELLOW from my video store because of all the controversy that surrounded it when it was first released in 1967. I also heard that at first it was seized by U.S. customs if it ever tried to enter this country, therefore being a fan of films considered " + "controversial" + " I really had to see this for myself.

The plot is centered around a young Swedish drama student named Lena who wants to learn everything she can about life. In particular she wants to focus her attentions to making some sort of documentary on what the average Swede thought about certain political issues such as the Vietnam War and race issues in the United States. In between asking politicians and ordinary denizens of Stockholm about their opinions on politics, she has sex with her drama teacher, classmates, and married men.

What kills me about I AM CURIOUS-YELLOW is that 40 years ago, this was considered pornographic. Really, the sex and nudity scenes are few and far between, even then it's not shot like some cheaply made porno. While my countrymen mind find it shocking, in reality sex and nudity are a major staple in Swedish cinema. Even Ingmar Bergman, arguably their answer to good old boy John Ford, had sex scenes in his films.

I do commend the filmmakers for the fact that any sex shown in the film is shown for artistic purposes rather than just to shock people and make money to be shown in pornographic theaters in America. I AM CURIOUS-YELLOW is a good film for anyone wanting to study the meat and potatoes (no pun intended) of Swedish cinema. But really, this film doesn't have much of a plot.", + } + ] + ) + return Dataset.from_iterable( + [ + DatasetItem( + id=0, + subset="train", + media=TableRow(table=table, index=0), + annotations=[Label(id=0, attributes={}, group=0, object_id=-1, label=0)], + ) + ], + categories={ + AnnotationType.label: LabelCategories.from_iterable( + [("label:1", "label"), ("label:2", "label")] + ) + }, + media_type=TableRow, + ) + + +@pytest.fixture +def fxt_tabular_caption_dataset(): + table = Table.from_list( + [ + { + "source": "Zwei junge weiße Männer sind im Freien in der Nähe vieler Büsche.", + "target": "Two young, White males are outside near many bushes.", + } + ] + ) + return Dataset.from_iterable( + [ + DatasetItem( + id=0, + subset="train", + media=TableRow(table=table, index=0), + annotations=[ + Caption("target:Two young, White males are outside near many bushes.") + ], + ) + ], + categories={}, + media_type=TableRow, + ) + + +@pytest.fixture +def fxt_dummy_tokenizer(): + def dummy_tokenizer(text): + return text.split() + + return dummy_tokenizer + + +@pytest.fixture +def data_iter(): + return [(1, "This is a sample text"), (2, "Another sample text")] + + +@pytest.fixture +def fxt_dummy_vocab(fxt_dummy_tokenizer, data_iter): + vocab = build_vocab_from_iterator( + map(fxt_dummy_tokenizer, (text for _, text in data_iter)), specials=[""] + ) + vocab.set_default_index(vocab[""]) + return vocab + + +@pytest.fixture +def fxt_tabular_fixture(fxt_dummy_tokenizer, fxt_dummy_vocab): + return {"target": {"input": "text"}, "tokenizer": fxt_dummy_tokenizer, "vocab": fxt_dummy_vocab} + + @pytest.mark.new @mark_requirement(Requirements.DATUM_GENERAL_REQ) class FrameworkConverterFactoryTest(TestCase): @@ -173,38 +260,49 @@ def test_create_converter_tf_importerror(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) class MultiframeworkConverterTest: @pytest.mark.parametrize( - "fxt_subset,fxt_task", + "fxt_dataset_type,fxt_subset,fxt_task", [ ( + "fxt_dataset", "train", "classification", ), ( + "fxt_dataset", "val", "multilabel_classification", ), ( + "fxt_dataset", "train", "detection", ), ( + "fxt_dataset", "val", "instance_segmentation", ), ( + "fxt_dataset", "train", "semantic_segmentation", ), + ("fxt_tabular_label_dataset", "train", "tabular"), ], ) - def test_multi_framework_dataset(self, fxt_dataset: Dataset, fxt_subset: str, fxt_task: str): + def test_multi_framework_dataset( + self, fxt_dataset_type: str, fxt_subset: str, fxt_task: str, request + ): + dataset = request.getfixturevalue(fxt_dataset_type) dm_multi_framework_dataset = _MultiFrameworkDataset( - dataset=fxt_dataset, subset=fxt_subset, task=fxt_task + dataset=dataset, subset=fxt_subset, task=fxt_task ) for idx in range(len(dm_multi_framework_dataset)): image, label = dm_multi_framework_dataset._gen_item(idx) - assert isinstance(image, np.ndarray) + if fxt_task == "tabular": + image = image() + assert isinstance(image, (np.ndarray, dict)) if fxt_task == "classification": assert isinstance(label, int) elif fxt_task == "multilabel_classification": @@ -213,6 +311,8 @@ def test_multi_framework_dataset(self, fxt_dataset: Dataset, fxt_subset: str, fx assert isinstance(label, list) if fxt_task == "semantic_segmentation": assert isinstance(label, np.ndarray) + elif fxt_task == "tabular": + assert isinstance(label, list) @pytest.mark.skipif(not TORCH_AVAILABLE, reason="PyTorch is not installed") @pytest.mark.parametrize( @@ -261,7 +361,6 @@ def test_can_convert_torch_framework( fxt_subset: str, fxt_task: str, fxt_convert_kwargs: Dict[str, Any], - request: pytest.FixtureRequest, ): multi_framework_dataset = FrameworkConverter(fxt_dataset, subset=fxt_subset, task=fxt_task) @@ -294,7 +393,12 @@ def test_can_convert_torch_framework( if ann.type == TASK_ANN_TYPE[fxt_task] ] label = np.sum(masks, axis=0, dtype=np.uint8) - + elif fxt_task == "tabular": + label = [ + ann.as_dict() + for ann in exp_item.annotations + if ann.type in TASK_ANN_TYPE[fxt_task] + ] if fxt_convert_kwargs.get("transform", None): actual = dm_torch_item[0].permute(1, 2, 0).mul(255.0).to(torch.uint8).numpy() assert np.array_equal(image, actual) @@ -374,6 +478,130 @@ def test_can_convert_torch_framework_detection(self): assert torch_ann["bbox"] == [x1, y1, x2 - x1, y2 - y1] assert torch_ann["iscrowd"] == dm_ann["attributes"]["is_crowd"] + @pytest.mark.skipif(not TORCH_AVAILABLE, reason="PyTorch is not installed") + def test_can_convert_torch_framework_tabular_label(self, fxt_tabular_label_dataset): + class IMDBDataset(Dataset): + def __init__(self, data_iter, vocab, transform=None): + self.data = list(data_iter) + self.vocab = vocab + self.transform = transform + self.tokenizer = get_tokenizer("basic_english") + + def __len__(self): + return len(self.data) + + def __getitem__(self, idx): + label, text = self.data[idx] + token_ids = [self.vocab[token] for token in self.tokenizer(text)] + + if self.transform: + token_ids = self.transform(token_ids) + + return torch.tensor(token_ids, dtype=torch.long), torch.tensor( + label, dtype=torch.long + ) + + # Prepare data and tokenizer + # First item of IMDB + first_item = ( + 1, + "I rented I AM CURIOUS-YELLOW from my video store because of all the controversy that surrounded it when it was first released in 1967. I also heard that at first it was seized by U.S. customs if it ever tried to enter this country, therefore being a fan of films considered \"controversial\" I really had to see this for myself.

The plot is centered around a young Swedish drama student named Lena who wants to learn everything she can about life. In particular she wants to focus her attentions to making some sort of documentary on what the average Swede thought about certain political issues such as the Vietnam War and race issues in the United States. In between asking politicians and ordinary denizens of Stockholm about their opinions on politics, she has sex with her drama teacher, classmates, and married men.

What kills me about I AM CURIOUS-YELLOW is that 40 years ago, this was considered pornographic. Really, the sex and nudity scenes are few and far between, even then it's not shot like some cheaply made porno. While my countrymen mind find it shocking, in reality sex and nudity are a major staple in Swedish cinema. Even Ingmar Bergman, arguably their answer to good old boy John Ford, had sex scenes in his films.

I do commend the filmmakers for the fact that any sex shown in the film is shown for artistic purposes rather than just to shock people and make money to be shown in pornographic theaters in America. I AM CURIOUS-YELLOW is a good film for anyone wanting to study the meat and potatoes (no pun intended) of Swedish cinema. But really, this film doesn't have much of a plot.", + ) + tokenizer = get_tokenizer("basic_english") + + # Build vocabulary + vocab = build_vocab_from_iterator([tokenizer(first_item[1])], specials=[""]) + vocab.set_default_index(vocab[""]) + + # Create torch dataset + torch_dataset = IMDBDataset(iter([first_item]), vocab) + + # Convert to dm_torch_dataset + dm_dataset = fxt_tabular_label_dataset + multi_framework_dataset = FrameworkConverter(dm_dataset, subset="train", task="tabular") + dm_torch_dataset = multi_framework_dataset.to_framework( + framework="torch", target={"input": "text"}, tokenizer=tokenizer, vocab=vocab + ) + + # Verify equality of items in torch_dataset and dm_torch_dataset + label_indices = dm_dataset.categories().get(AnnotationType.label)._indices + torch_item = torch_dataset[0] + dm_item = dm_torch_dataset[0] + assert torch.equal(torch_item[0], dm_item[0]), "Token IDs do not match" + + # Extract and compare labels + torch_item_label = str(torch_item[1].item()) + dm_item_label = list(label_indices.keys())[list(label_indices.values()).index(0)].split( + ":" + )[-1] + assert torch_item_label == dm_item_label, "Labels do not match" + + @pytest.mark.skipif(not TORCH_AVAILABLE, reason="PyTorch is not installed") + def test_can_convert_torch_framework_tabular_caption(self, fxt_tabular_caption_dataset): + class Multi30kDataset(Dataset): + def __init__(self, dataset, src_tokenizer, tgt_tokenizer, src_vocab, tgt_vocab): + self.dataset = list(dataset) + self.src_tokenizer = src_tokenizer + self.tgt_tokenizer = tgt_tokenizer + self.src_vocab = src_vocab + self.tgt_vocab = tgt_vocab + + def __len__(self): + return len(self.dataset) + + def _data_process(self, text, tokenizer, vocab): + tokens = tokenizer(text) + token_ids = [vocab[token] for token in tokens] + return torch.tensor(token_ids, dtype=torch.long) + + def __getitem__(self, idx): + src, tgt = self.dataset[idx] + src_tensor = self._data_process(src, self.src_tokenizer, self.src_vocab) + tgt_tensor = self._data_process(tgt, self.tgt_tokenizer, self.tgt_vocab) + return src_tensor, tgt_tensor + + # Prepare data and tokenizer + # First item of Multi30k + first_item = ( + "Zwei junge weiße Männer sind im Freien in der Nähe vieler Büsche.", + "Two young, White males are outside near many bushes.", + ) + + dummy_tokenizer = str.split + + def build_single_vocab(item, tokenizer, specials): + tokens = tokenizer(item) + vocab = build_vocab_from_iterator([tokens], specials=specials) + vocab.set_default_index(vocab[""]) + return vocab + + # Build vocabularies + specials = ["", "", "", ""] + src_vocab = build_single_vocab(first_item[0], dummy_tokenizer, specials) + tgt_vocab = build_single_vocab(first_item[1], dummy_tokenizer, specials) + + # Create torch dataset + torch_dataset = Multi30kDataset( + iter([first_item]), dummy_tokenizer, dummy_tokenizer, src_vocab, tgt_vocab + ) + + # Convert to dm_torch_dataset + dm_dataset = fxt_tabular_caption_dataset + multi_framework_dataset = FrameworkConverter(dm_dataset, subset="train", task="tabular") + dm_torch_dataset = multi_framework_dataset.to_framework( + framework="torch", + target={"input": "source", "output": "target"}, + tokenizer=(dummy_tokenizer, dummy_tokenizer), + vocab=(src_vocab, tgt_vocab), + ) + + # Verify equality of items in torch_dataset and dm_torch_dataset + torch_item = torch_dataset[0] + dm_item = dm_torch_dataset[0] + + assert torch.equal(torch_item[0], dm_item[0]), "Token IDs for de do not match" + assert torch.equal(torch_item[1], dm_item[1]), "Token IDs for en do not match" + @pytest.mark.skipif(not TF_AVAILABLE, reason="Tensorflow is not installed") @pytest.mark.parametrize( "fxt_subset,fxt_task,fxt_convert_kwargs", diff --git a/tests/unit/test_hashkey.py b/tests/unit/test_hashkey.py index 13b60222d0..da360a8e64 100644 --- a/tests/unit/test_hashkey.py +++ b/tests/unit/test_hashkey.py @@ -46,7 +46,7 @@ def fxt_dataset_dir_with_hash_key(test_dir, fxt_data_format): test_asset_dir = test_asset_dir_map[fxt_data_format] dataset = Dataset.import_from(test_asset_dir, format=fxt_data_format) for item in dataset: - hash_key = HashKey(hash_key=np.random.randint(0, 256, size=(64,), dtype=np.uint8)) + hash_key = HashKey(hash_key=np.random.randint(0, 256, size=(96,), dtype=np.uint8)) item.annotations += [hash_key] if fxt_data_format == "wider_face": diff --git a/tests/unit/test_imagenet_format.py b/tests/unit/test_imagenet_format.py index 6e3ca2abec..5bd8a1c9bb 100644 --- a/tests/unit/test_imagenet_format.py +++ b/tests/unit/test_imagenet_format.py @@ -7,6 +7,7 @@ import pytest from datumaro.components.annotation import AnnotationType, Label, LabelCategories +from datumaro.components.contexts.importer import ImportErrorPolicy from datumaro.components.dataset import Dataset, StreamDataset from datumaro.components.dataset_base import DatasetItem from datumaro.components.environment import Environment @@ -181,6 +182,12 @@ class ImagenetImporterTest: IMPORTER_NAME = ImagenetImporter.NAME def _create_expected_dataset(self): + label_categories = LabelCategories.from_iterable( + ("label_0", "label_1", f"{Path('label_1', 'label_1_1')}") + ) + label_categories[-1].parent = "label_1" + label_categories.add_label_group(name="label_1", labels=["label_1_1"], group_type=0) + return Dataset.from_iterable( [ DatasetItem( @@ -203,18 +210,16 @@ def _create_expected_dataset(self): annotations=[Label(1)], ), ], - categories={ - AnnotationType.label: LabelCategories.from_iterable( - ("label_0", "label_1", f"{Path('label_1', 'label_1_1')}") - ), - }, + categories={AnnotationType.label: label_categories}, ) @mark_requirement(Requirements.DATUM_GENERAL_REQ) @pytest.mark.parametrize("dataset_cls, is_stream", [(Dataset, False), (StreamDataset, True)]) def test_can_import(self, dataset_cls, is_stream, helper_tc): expected_dataset = self._create_expected_dataset() - dataset = dataset_cls.import_from(self.DUMMY_DATASET_DIR, self.IMPORTER_NAME) + dataset = dataset_cls.import_from( + self.DUMMY_DATASET_DIR, self.IMPORTER_NAME, error_policy=ImportErrorPolicy() + ) assert dataset.is_stream == is_stream compare_datasets(helper_tc, expected_dataset, dataset, require_media=True) @@ -240,7 +245,9 @@ class ImagenetWithSubsetDirsImporterTest(ImagenetImporterTest): @mark_requirement(Requirements.DATUM_GENERAL_REQ) @pytest.mark.parametrize("dataset_cls, is_stream", [(Dataset, False), (StreamDataset, True)]) def test_can_import(self, dataset_cls, is_stream, helper_tc): - dataset = dataset_cls.import_from(self.DUMMY_DATASET_DIR, self.IMPORTER_NAME) + dataset = dataset_cls.import_from( + self.DUMMY_DATASET_DIR, self.IMPORTER_NAME, error_policy=ImportErrorPolicy() + ) assert dataset.is_stream == is_stream for subset_name, subset in dataset.subsets().items(): diff --git a/tests/unit/test_kitti_3d_format.py b/tests/unit/test_kitti_3d_format.py new file mode 100644 index 0000000000..3dbadb2507 --- /dev/null +++ b/tests/unit/test_kitti_3d_format.py @@ -0,0 +1,292 @@ +import os.path as osp +from unittest import TestCase + +import numpy as np + +from datumaro.components.annotation import AnnotationType, Bbox, LabelCategories +from datumaro.components.dataset_base import DatasetItem +from datumaro.components.environment import Environment +from datumaro.components.media import Image, PointCloud +from datumaro.components.project import Dataset +from datumaro.plugins.data_formats.kitti_3d.importer import Kitti3dImporter + +from tests.requirements import Requirements, mark_requirement +from tests.utils.assets import get_test_asset_path +from tests.utils.test_utils import compare_datasets_3d + +DUMMY_DATASET_DIR = get_test_asset_path("kitti_dataset", "kitti_3d") +DUMMY_SUBSET_DATASET_DIR = get_test_asset_path("kitti_dataset", "kitti_3d_with_subset") + + +class Kitti3DImporterTest(TestCase): + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_can_detect(self): + detected_formats = Environment().detect_dataset(DUMMY_DATASET_DIR) + self.assertEqual([Kitti3dImporter.NAME], detected_formats) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_can_load(self): + """ + Description: + Ensure that the dataset can be loaded correctly from the KITTI3D format. + + Expected results: + The loaded dataset should have the same number of data items as the expected dataset. + The data items in the loaded dataset should have the same attributes and values as the expected data items. + The point clouds and images associated with the data items should be loaded correctly. + + Steps: + 1. Prepare an expected dataset with known data items, point clouds, images, and attributes. + 2. Load the dataset from the KITTI3D format. + 3. Compare the loaded dataset with the expected dataset. + """ + + image1 = Image.from_file(path=osp.join(DUMMY_DATASET_DIR, "image_2", "000001.png")) + + expected_label_cat = LabelCategories( + attributes={"occluded", "truncated", "alpha", "dimensions", "location", "rotation_y"} + ) + expected_label_list = [ + "DontCare", + "Car", + "Pedestrian", + "Van", + "Truck", + "Cyclist", + "Sitter", + "Train", + "Motorcycle", + "Bus", + "Misc", + ] + for label in expected_label_list: + expected_label_cat.add(label) + expected_dataset = Dataset.from_iterable( + [ + DatasetItem( + id="000001", + annotations=[ + Bbox( + 600, # x1 + 150, # y1 + 30, # x2-x1 + 40, # y2-y1 + label=4, + id=0, + attributes={ + "truncated": 0.0, + "occluded": 0, + "alpha": -1.57, + "dimensions": [2.85, 2.63, 12.34], + "location": [0.47, 1.49, 69.44], + "rotation_y": -1.56, + }, + ), + Bbox( + 650, # x1 + 160, # y1 + 50, # x2-x1 + 40, # y2-y1 + label=1, + id=1, + attributes={ + "truncated": 0.0, + "occluded": 3, + "alpha": -1.65, + "dimensions": [1.86, 0.6, 2.02], + "location": [4.59, 1.32, 45.84], + "rotation_y": -1.55, + }, + ), + Bbox( + 500, # x1 + 170, # y1 + 90, # x2-x1 + 20, # y2-y1 + label=0, + id=2, + attributes={ + "truncated": -1.0, + "occluded": -1, + "alpha": -10.0, + "dimensions": [-1.0, -1.0, -1.0], + "location": [-1000.0, -1000.0, -1000.0], + "rotation_y": -10.0, + }, + ), + ], + media=image1, + attributes={"calib_path": osp.join(DUMMY_DATASET_DIR, "calib", "000001.txt")}, + ), + ], + categories={AnnotationType.label: expected_label_cat}, + ) + + parsed_dataset = Dataset.import_from(DUMMY_DATASET_DIR, "kitti3d") + + compare_datasets_3d(self, expected_dataset, parsed_dataset, require_point_cloud=True) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_can_load_with_subset(self): + """ + Description: + Ensure that the dataset can be loaded correctly from the KITTI3D format with a specified subset of data items. + + Expected results: + The loaded dataset should contain only the specified subset of data items from the original dataset. + The data items in the loaded dataset should have the same attributes and values as the expected data items. + + Steps: + 1. Prepare an expected dataset with a subset of data items from the original dataset. + 2. Load the dataset from the KITTI3D format, specifying the subset of data items to load. + 3. Compare the loaded dataset with the expected dataset. + """ + expected_label_cat = LabelCategories( + attributes={"occluded", "truncated", "alpha", "dimensions", "location", "rotation_y"} + ) + expected_label_list = [ + "DontCare", + "Car", + "Pedestrian", + "Van", + "Truck", + "Cyclist", + "Sitter", + "Train", + "Motorcycle", + "Bus", + "Misc", + ] + for label in expected_label_list: + expected_label_cat.add(label) + expected_dataset = Dataset.from_iterable( + [ + DatasetItem( + id="000000", + subset="train", + annotations=[ + Bbox( + 700, # x1 + 150, # y1 + 100, # x2-x1 + 150, # y2-y1 + label=2, + id=0, + attributes={ + "truncated": 0.0, + "occluded": 0, + "alpha": -0.2, + "dimensions": [1.89, 0.48, 1.20], + "location": [1.84, 1.47, 8.41], + "rotation_y": 0.01, + }, + ), + ], + media=Image.from_file( + path=osp.join(DUMMY_SUBSET_DATASET_DIR, "image_2", "train", "000000.png") + ), + attributes={ + "calib_path": osp.join( + DUMMY_SUBSET_DATASET_DIR, "calib", "train", "000000.txt" + ) + }, + ), + DatasetItem( + id="000001", + subset="val", + annotations=[ + Bbox( + 330, # x1 + 180, # y1 + 30, # x2-x1 + 60, # y2-y1 + label=2, + id=0, + attributes={ + "truncated": 0.0, + "occluded": 0, + "alpha": 1.94, + "dimensions": [1.87, 0.96, 0.65], + "location": [-8.50, 2.07, 23.02], + "rotation_y": 1.59, + }, + ), + Bbox( + 600, # x1 + 170, # y1 + 20, # x2-x1 + 15, # y2-y1 + label=0, + id=1, + attributes={ + "truncated": -1, + "occluded": -1, + "alpha": -10, + "dimensions": [-1, -1, -1], + "location": [-1000, -1000, -1000], + "rotation_y": -10, + }, + ), + ], + media=Image.from_file( + path=osp.join(DUMMY_SUBSET_DATASET_DIR, "image_2", "val", "000001.png") + ), + attributes={ + "calib_path": osp.join( + DUMMY_SUBSET_DATASET_DIR, "calib", "val", "000001.txt" + ) + }, + ), + DatasetItem( + id="000002", + subset="test", + annotations=[ + Bbox( + 0, # x1 + 190, # y1 + 400, # x2-x1 + 190, # y2-y1 + label=1, + id=0, + attributes={ + "truncated": 0.88, + "occluded": 3, + "alpha": -0.69, + "dimensions": [1.60, 1.57, 3.23], + "location": [-2.70, 1.74, 3.68], + "rotation_y": -1.29, + }, + ), + Bbox( + 800, # x1 + 160, # y1 + 25, # x2-x1 + 25, # y2-y1 + label=0, + id=1, + attributes={ + "truncated": -1, + "occluded": -1, + "alpha": -10, + "dimensions": [-1, -1, -1], + "location": [-1000, -1000, -1000], + "rotation_y": -10, + }, + ), + ], + media=Image.from_file( + path=osp.join(DUMMY_SUBSET_DATASET_DIR, "image_2", "test", "000002.png") + ), + attributes={ + "calib_path": osp.join( + DUMMY_SUBSET_DATASET_DIR, "calib", "test", "000002.txt" + ) + }, + ), + ], + categories={AnnotationType.label: expected_label_cat}, + ) + + parsed_dataset = Dataset.import_from(DUMMY_SUBSET_DATASET_DIR, "kitti3d") + + compare_datasets_3d(self, expected_dataset, parsed_dataset, require_point_cloud=True) diff --git a/tests/unit/test_kitti_raw_format.py b/tests/unit/test_kitti_raw_format.py index e8ab776b75..498e99b20f 100644 --- a/tests/unit/test_kitti_raw_format.py +++ b/tests/unit/test_kitti_raw_format.py @@ -52,13 +52,13 @@ def test_can_load(self): annotations=[ Cuboid3d( position=[1, 2, 3], - scale=[7.95, -3.62, -1.03], + scale=[-3.62, 7.95, -1.03], label=1, attributes={"occluded": False, "track_id": 1}, ), Cuboid3d( position=[1, 1, 0], - scale=[8.34, 23.01, -0.76], + scale=[23.01, 8.34, -0.76], label=0, attributes={"occluded": False, "track_id": 2}, ), @@ -71,7 +71,7 @@ def test_can_load(self): annotations=[ Cuboid3d( position=[0, 1, 0], - scale=[8.34, 23.01, -0.76], + scale=[23.01, 8.34, -0.76], rotation=[1, 1, 3], label=0, attributes={"occluded": True, "track_id": 2}, @@ -85,7 +85,7 @@ def test_can_load(self): annotations=[ Cuboid3d( position=[1, 2, 3], - scale=[-9.41, 13.54, 0.24], + scale=[13.54, -9.41, 0.24], label=1, attributes={"occluded": False, "track_id": 3}, ) @@ -161,7 +161,7 @@ def test_can_save_and_load(self): Cuboid3d(position=[1.4, 2.1, 1.4], label=1, attributes={"track_id": 2}), Cuboid3d( position=[11.4, -0.1, 4.2], - scale=[2, 1, 2], + scale=[1, 2, 2], label=0, attributes={"track_id": 3}, ), @@ -172,7 +172,7 @@ def test_can_save_and_load(self): annotations=[ Cuboid3d( position=[0.4, -1, 2.24], - scale=[2, 1, 2], + scale=[1, 2, 2], label=0, attributes={"track_id": 3}, ), @@ -185,7 +185,7 @@ def test_can_save_and_load(self): annotations=[ Cuboid3d( position=[0.4, -1, 3.24], - scale=[2, 1, 2], + scale=[1, 2, 2], label=0, attributes={"track_id": 3}, ), @@ -244,7 +244,7 @@ def test_can_save_and_load(self): ), Cuboid3d( position=[11.4, -0.1, 4.2], - scale=[2, 1, 2], + scale=[1, 2, 2], label=0, attributes={"occluded": False, "track_id": 3}, ), @@ -256,7 +256,7 @@ def test_can_save_and_load(self): annotations=[ Cuboid3d( position=[0.4, -1, 2.24], - scale=[2, 1, 2], + scale=[1, 2, 2], label=0, attributes={"occluded": False, "track_id": 3}, ), @@ -271,7 +271,7 @@ def test_can_save_and_load(self): annotations=[ Cuboid3d( position=[0.4, -1, 3.24], - scale=[2, 1, 2], + scale=[1, 2, 2], label=0, attributes={"occluded": False, "track_id": 3}, ), diff --git a/tests/unit/test_transforms.py b/tests/unit/test_transforms.py index 24db8a76e6..ebd7e58664 100644 --- a/tests/unit/test_transforms.py +++ b/tests/unit/test_transforms.py @@ -2,9 +2,11 @@ import argparse import logging as log +import os import os.path as osp import random from unittest import TestCase +from unittest.mock import MagicMock, patch import numpy as np import pandas as pd @@ -14,6 +16,7 @@ import datumaro.plugins.transforms as transforms import datumaro.util.mask_tools as mask_tools +from datumaro.components.algorithms.hash_key_inference.explorer import Explorer from datumaro.components.annotation import ( AnnotationType, Bbox, @@ -31,10 +34,10 @@ Tabular, TabularCategories, ) -from datumaro.components.dataset import Dataset +from datumaro.components.dataset import Dataset, eager_mode from datumaro.components.dataset_base import DatasetItem from datumaro.components.errors import AnnotationTypeError -from datumaro.components.media import Image, Table, TableRow +from datumaro.components.media import Image, Table, TableRow, Video, VideoFrame from ..requirements import Requirements, mark_bug, mark_requirement @@ -418,26 +421,6 @@ def test_shapes_to_boxes(self): actual = transforms.ShapesToBoxes(source_dataset) compare_datasets(self, target_dataset, actual) - @mark_requirement(Requirements.DATUM_GENERAL_REQ) - def test_id_from_image(self): - source_dataset = Dataset.from_iterable( - [ - DatasetItem(id=1, media=Image.from_file(path="path.jpg")), - DatasetItem(id=2), - DatasetItem(id=3, media=Image.from_numpy(data=np.ones([5, 5, 3]))), - ] - ) - target_dataset = Dataset.from_iterable( - [ - DatasetItem(id="path", media=Image.from_file(path="path.jpg")), - DatasetItem(id=2), - DatasetItem(id=3, media=Image.from_numpy(data=np.ones([5, 5, 3]))), - ] - ) - - actual = transforms.IdFromImageName(source_dataset) - compare_datasets(self, target_dataset, actual) - @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_boxes_to_masks(self): source_dataset = Dataset.from_iterable( @@ -1225,6 +1208,84 @@ def test_annotation_reindex(self, fxt_dataset: Dataset, reindex_each_item: bool) ) +class IdFromImageNameTest: + @pytest.fixture + def fxt_dataset(self, n_labels=3, n_anns=5, n_items=7) -> Dataset: + video = Video("video.mp4") + video._frame_size = MagicMock(return_value=(32, 32)) + video.get_frame_data = MagicMock(return_value=np.ndarray((32, 32, 3), dtype=np.uint8)) + return Dataset.from_iterable( + [ + DatasetItem(id=1, media=Image.from_file(path="path1.jpg")), + DatasetItem(id=2, media=Image.from_file(path="path1.jpg")), + DatasetItem(id=3, media=Image.from_file(path="path1.jpg")), + DatasetItem(id=4, media=VideoFrame(video, index=30)), + DatasetItem(id=5, media=VideoFrame(video, index=30)), + DatasetItem(id=6, media=VideoFrame(video, index=60)), + DatasetItem(id=7), + DatasetItem(id=8, media=Image.from_numpy(data=np.ones([5, 5, 3]))), + ] + ) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + @pytest.mark.parametrize("ensure_unique", [True, False]) + def test_id_from_image(self, fxt_dataset, ensure_unique): + source_dataset: Dataset = fxt_dataset + actual_dataset = transforms.IdFromImageName(source_dataset, ensure_unique=ensure_unique) + + unique_names: set[str] = set() + for src, actual in zip(source_dataset, actual_dataset): + if not isinstance(src.media, Image) or not hasattr(src.media, "path"): + src == actual + else: + if isinstance(src.media, VideoFrame): + expected_id = f"video_frame-{src.media.index}" + else: + expected_id = os.path.splitext(src.media.path)[0] + if ensure_unique: + assert actual.id.startswith(expected_id) + assert actual.wrap(id=src.id) == src + assert actual.id not in unique_names + unique_names.add(actual.id) + else: + assert src.wrap(id=expected_id) == actual + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_id_from_image_wrong_suffix_length(self, fxt_dataset): + with pytest.raises(ValueError) as e: + transforms.IdFromImageName(fxt_dataset, ensure_unique=True, suffix_length=0) + assert str(e.value).startswith("The 'suffix_length' must be greater than 0.") + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_id_from_image_too_many_duplication(self, fxt_dataset): + with patch("datumaro.plugins.transforms.IdFromImageName.DEFAULT_RETRY", 1), patch( + "datumaro.plugins.transforms.IdFromImageName.SUFFIX_LETTERS", "a" + ), pytest.raises(Exception) as e: + with eager_mode(): + fxt_dataset.transform( + "id_from_image_name", + ensure_unique=True, + suffix_length=1, + ) + assert str(e.value).startswith("Too many duplicate names.") + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + @pytest.mark.parametrize( + "args,ensure_unique,suffix_length", + [ + ([], False, 3), + (["--ensure_unique", "--suffix_length", "2"], True, 2), + ], + ids=["default", "ensure_unique"], + ) + def test_parser(self, args, ensure_unique, suffix_length): + parser = transforms.IdFromImageName.build_cmdline_parser() + args = parser.parse_args(args) + + assert hasattr(args, "ensure_unique") and args.ensure_unique == ensure_unique + assert hasattr(args, "suffix_length") and args.suffix_length == suffix_length + + class AstypeAnnotationsTest(TestCase): def setUp(self): self.table = Table.from_list( @@ -1673,3 +1734,53 @@ def test_transform_clean_after_astype_ann(self): result_item = result.__getitem__(i) self.assertEqual(expected_item.annotations, result_item.annotations) self.assertEqual(expected_item.media, result_item.media) + + +class PseudoLabelingTest(TestCase): + def setUp(self): + self.data_path = get_test_asset_path("explore_dataset") + self.categories = ["bird", "cat", "dog", "monkey"] + self.source = Dataset.from_iterable( + [ + DatasetItem( + id=0, + media=Image.from_file(path=os.path.join(self.data_path, "dog", "0.JPEG")), + ), + DatasetItem( + id=1, + media=Image.from_file(path=os.path.join(self.data_path, "cat", "0.JPEG")), + ), + ], + categories=self.categories, + ) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_transform_pseudolabeling_with_labels(self): + dataset = self.source + labels = self.categories + explorer = Explorer(dataset) + result = dataset.transform("pseudo_labeling", labels=labels, explorer=explorer) + + label_indices = dataset.categories()[AnnotationType.label]._indices + for item, expected in zip(result, ["dog", "cat"]): + self.assertEqual(item.annotations[0].label, label_indices[expected]) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_transform_pseudolabeling_without_labels(self): + dataset = self.source + explorer = Explorer(dataset) + result = dataset.transform("pseudo_labeling", explorer=explorer) + + label_indices = dataset.categories()[AnnotationType.label]._indices + for item, expected in zip(result, ["dog", "cat"]): + self.assertEqual(item.annotations[0].label, label_indices[expected]) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_transform_pseudolabeling_without_explorer(self): + dataset = self.source + labels = self.categories + result = dataset.transform("pseudo_labeling", labels=labels) + + label_indices = dataset.categories()[AnnotationType.label]._indices + for item, expected in zip(result, ["dog", "cat"]): + self.assertEqual(item.annotations[0].label, label_indices[expected]) diff --git a/tests/unit/test_visualizer.py b/tests/unit/test_visualizer.py index 676cbfb966..6d0d1c15a9 100644 --- a/tests/unit/test_visualizer.py +++ b/tests/unit/test_visualizer.py @@ -474,7 +474,7 @@ def setUpClass(cls): super().setUpClass() for item in cls.dataset: - item.annotations.append(HashKey(np.ones(64).astype(np.uint8))) + item.annotations.append(HashKey(np.ones(96).astype(np.uint8))) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_vis_one_sample(self): diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index f9a2e72d59..3a25c1d158 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -108,6 +108,17 @@ def compare_categories(test, expected, actual): sorted(expected[AnnotationType.label].items, key=lambda t: t.name), sorted(actual[AnnotationType.label].items, key=lambda t: t.name), ) + if expected[AnnotationType.label].label_groups: + assert len(expected[AnnotationType.label].label_groups) == len( + actual[AnnotationType.label].label_groups + ) + for expected_group, actual_group in zip( + expected[AnnotationType.label].label_groups, + actual[AnnotationType.label].label_groups, + ): + test.assertEqual(set(expected_group.labels), set(actual_group.labels)) + test.assertEqual(expected_group.group_type, actual_group.group_type) + if AnnotationType.mask in expected: test.assertEqual( expected[AnnotationType.mask].colormap,