diff --git a/.dockerignore b/.dockerignore index 3c6b6ab02e..6c2f2b9b77 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,17 +8,21 @@ coco storage.googleapis.com data/samples/* -**/results*.txt +**/results*.csv *.jpg # Neural Network weights ----------------------------------------------------------------------------------------------- -**/*.weights **/*.pt **/*.pth **/*.onnx **/*.mlmodel **/*.torchscript - +**/*.torchscript.pt +**/*.tflite +**/*.h5 +**/*.pb +*_saved_model/ +*_web_model/ # Below Copied From .gitignore ----------------------------------------------------------------------------------------- # Below Copied From .gitignore ----------------------------------------------------------------------------------------- diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 3f7d83a407..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: "πŸ› Bug report" -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -Before submitting a bug report, please be aware that your issue **must be reproducible** with all of the following, otherwise it is non-actionable, and we can not help you: - - **Current repo**: run `git fetch && git status -uno` to check and `git pull` to update repo - - **Common dataset**: coco.yaml or coco128.yaml - - **Common environment**: Colab, Google Cloud, or Docker image. See https://github.com/ultralytics/yolov3#environments - -If this is a custom dataset/training question you **must include** your `train*.jpg`, `test*.jpg` and `results.png` figures, or we can not help you. You can generate these with `utils.plot_results()`. - - -## πŸ› Bug -A clear and concise description of what the bug is. - - -## To Reproduce (REQUIRED) - -Input: -``` -import torch - -a = torch.tensor([5]) -c = a / 0 -``` - -Output: -``` -Traceback (most recent call last): - File "/Users/glennjocher/opt/anaconda3/envs/env1/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3331, in run_code - exec(code_obj, self.user_global_ns, self.user_ns) - File "", line 5, in - c = a / 0 -RuntimeError: ZeroDivisionError -``` - - -## Expected behavior -A clear and concise description of what you expected to happen. - - -## Environment -If applicable, add screenshots to help explain your problem. - - - OS: [e.g. Ubuntu] - - GPU [e.g. 2080 Ti] - - -## Additional context -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000000..affe6aae2b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,85 @@ +name: πŸ› Bug Report +# title: " " +description: Problems with YOLOv3 +labels: [bug, triage] +body: + - type: markdown + attributes: + value: | + Thank you for submitting a YOLOv3 πŸ› Bug Report! + + - type: checkboxes + attributes: + label: Search before asking + description: > + Please search the [issues](https://github.com/ultralytics/yolov3/issues) to see if a similar bug report already exists. + options: + - label: > + I have searched the YOLOv3 [issues](https://github.com/ultralytics/yolov3/issues) and found no similar bug report. + required: true + + - type: dropdown + attributes: + label: YOLOv3 Component + description: | + Please select the part of YOLOv3 where you found the bug. + multiple: true + options: + - "Training" + - "Validation" + - "Detection" + - "Export" + - "PyTorch Hub" + - "Multi-GPU" + - "Evolution" + - "Integrations" + - "Other" + validations: + required: false + + - type: textarea + attributes: + label: Bug + description: Provide console output with error messages and/or screenshots of the bug. + placeholder: | + πŸ’‘ ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response. + validations: + required: true + + - type: textarea + attributes: + label: Environment + description: Please specify the software and hardware you used to produce the bug. + placeholder: | + - YOLO: YOLOv3 πŸš€ v6.0-67-g60e42e1 torch 1.9.0+cu111 CUDA:0 (A100-SXM4-40GB, 40536MiB) + - OS: Ubuntu 20.04 + - Python: 3.9.0 + validations: + required: false + + - type: textarea + attributes: + label: Minimal Reproducible Example + description: > + When asking a question, people will be better able to provide help if you provide code that they can easily understand and use to **reproduce** the problem. + This is referred to by community members as creating a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + placeholder: | + ``` + # Code to reproduce your issue here + ``` + validations: + required: false + + - type: textarea + attributes: + label: Additional + description: Anything else you would like to share? + + - type: checkboxes + attributes: + label: Are you willing to submit a PR? + description: > + (Optional) We encourage you to submit a [Pull Request](https://github.com/ultralytics/yolov3/pulls) (PR) to help improve YOLOv3 for everyone, especially if you have a good understanding of how to implement a fix or feature. + See the YOLOv3 [Contributing Guide](https://github.com/ultralytics/yolov3/blob/master/CONTRIBUTING.md) to get started. + options: + - label: Yes I'd like to help by submitting a PR! diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..02be0529fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Slack + url: https://join.slack.com/t/ultralytics/shared_invite/zt-w29ei8bp-jczz7QYUmDtgo6r6KcMIAg + about: Ask on Ultralytics Slack Forum + - name: Stack Overflow + url: https://stackoverflow.com/search?q=YOLOv3 + about: Ask on Stack Overflow with 'YOLOv3' tag diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 87db3eacbf..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: "πŸš€ Feature request" -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -## πŸš€ Feature - - -## Motivation - - - -## Pitch - - - -## Alternatives - - - -## Additional context - - diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000000..53cf234475 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,50 @@ +name: πŸš€ Feature Request +description: Suggest a YOLOv3 idea +# title: " " +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Thank you for submitting a YOLOv3 πŸš€ Feature Request! + + - type: checkboxes + attributes: + label: Search before asking + description: > + Please search the [issues](https://github.com/ultralytics/yolov3/issues) to see if a similar feature request already exists. + options: + - label: > + I have searched the YOLOv3 [issues](https://github.com/ultralytics/yolov3/issues) and found no similar feature requests. + required: true + + - type: textarea + attributes: + label: Description + description: A short description of your feature. + placeholder: | + What new feature would you like to see in YOLOv3? + validations: + required: true + + - type: textarea + attributes: + label: Use case + description: | + Describe the use case of your feature request. It will help us understand and prioritize the feature request. + placeholder: | + How would this feature be used, and who would use it? + + - type: textarea + attributes: + label: Additional + description: Anything else you would like to share? + + - type: checkboxes + attributes: + label: Are you willing to submit a PR? + description: > + (Optional) We encourage you to submit a [Pull Request](https://github.com/ultralytics/yolov3/pulls) (PR) to help improve YOLOv3 for everyone, especially if you have a good understanding of how to implement a fix or feature. + See the YOLOv3 [Contributing Guide](https://github.com/ultralytics/yolov3/blob/master/CONTRIBUTING.md) to get started. + options: + - label: Yes I'd like to help by submitting a PR! diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 2c22aea70a..0000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: "❓Question" -about: Ask a general question -title: '' -labels: question -assignees: '' - ---- - -## ❔Question - - -## Additional context diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000000..decb214859 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,33 @@ +name: ❓ Question +description: Ask a YOLOv3 question +# title: " " +labels: [question] +body: + - type: markdown + attributes: + value: | + Thank you for asking a YOLOv3 ❓ Question! + + - type: checkboxes + attributes: + label: Search before asking + description: > + Please search the [issues](https://github.com/ultralytics/yolov3/issues) and [discussions](https://github.com/ultralytics/yolov3/discussions) to see if a similar question already exists. + options: + - label: > + I have searched the YOLOv3 [issues](https://github.com/ultralytics/yolov3/issues) and [discussions](https://github.com/ultralytics/yolov3/discussions) and found no similar questions. + required: true + + - type: textarea + attributes: + label: Question + description: What is your question? + placeholder: | + πŸ’‘ ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response. + validations: + required: true + + - type: textarea + attributes: + label: Additional + description: Anything else you would like to share? diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9910689197..c1b3d5d514 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,23 @@ version: 2 updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: weekly - time: "04:00" - open-pull-requests-limit: 10 - reviewers: - - glenn-jocher - labels: - - dependencies + - package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + time: "04:00" + open-pull-requests-limit: 10 + reviewers: + - glenn-jocher + labels: + - dependencies + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "04:00" + open-pull-requests-limit: 5 + reviewers: + - glenn-jocher + labels: + - dependencies diff --git a/.github/workflows/ci-testing.yml b/.github/workflows/ci-testing.yml index 77ac2c3f35..e777173374 100644 --- a/.github/workflows/ci-testing.yml +++ b/.github/workflows/ci-testing.yml @@ -1,6 +1,8 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license + name: CI CPU testing -on: # https://help.github.com/en/actions/reference/events-that-trigger-workflows +on: # https://help.github.com/en/actions/reference/events-that-trigger-workflows push: branches: [ master ] pull_request: @@ -16,9 +18,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.8] - model: ['yolov3-tiny'] # models to test + os: [ ubuntu-latest, macos-latest, windows-latest ] + python-version: [ 3.9 ] + model: [ 'yolov3-tiny' ] # models to test # Timeout: https://stackoverflow.com/a/59076067/4521646 timeout-minutes: 50 @@ -37,23 +39,27 @@ jobs: python -c "from pip._internal.locations import USER_CACHE_DIR; print('::set-output name=dir::' + USER_CACHE_DIR)" - name: Cache pip - uses: actions/cache@v1 + uses: actions/cache@v2.1.6 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('requirements.txt') }} restore-keys: | ${{ runner.os }}-${{ matrix.python-version }}-pip- + # Known Keras 2.7.0 issue: https://github.com/ultralytics/yolov5/pull/5486 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -qr requirements.txt -f https://download.pytorch.org/whl/cpu/torch_stable.html - pip install -q onnx + pip install -q onnx tensorflow-cpu keras==2.6.0 # wandb # extras python --version pip --version pip list shell: bash + # - name: W&B login + # run: wandb login 345011b3fb26dc8337fd9b20e53857c1d403f2aa + - name: Download data run: | # curl -L -o tmp.zip https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128.zip @@ -63,18 +69,26 @@ jobs: - name: Tests workflow run: | # export PYTHONPATH="$PWD" # to run '$ python *.py' files in subdirectories - di=cpu # inference devices # define device + di=cpu # device - # train - python train.py --img 128 --batch 16 --weights weights/${{ matrix.model }}.pt --cfg models/${{ matrix.model }}.yaml --epochs 1 --device $di - # detect - python detect.py --weights weights/${{ matrix.model }}.pt --device $di + # Train + python train.py --img 64 --batch 32 --weights ${{ matrix.model }}.pt --cfg ${{ matrix.model }}.yaml --epochs 1 --device $di + # Val + python val.py --img 64 --batch 32 --weights ${{ matrix.model }}.pt --device $di + python val.py --img 64 --batch 32 --weights runs/train/exp/weights/last.pt --device $di + # Detect + python detect.py --weights ${{ matrix.model }}.pt --device $di python detect.py --weights runs/train/exp/weights/last.pt --device $di - # test - python test.py --img 128 --batch 16 --weights weights/${{ matrix.model }}.pt --device $di - python test.py --img 128 --batch 16 --weights runs/train/exp/weights/last.pt --device $di - python hubconf.py # hub - python models/yolo.py --cfg models/${{ matrix.model }}.yaml # inspect - python models/export.py --img 128 --batch 1 --weights weights/${{ matrix.model }}.pt # export + # Export + python models/yolo.py --cfg ${{ matrix.model }}.yaml # build PyTorch model + # python models/tf.py --weights ${{ matrix.model }}.pt # build TensorFlow model (YOLOv3 not supported) + python export.py --img 64 --batch 1 --weights runs/train/exp/weights/last.pt --include torchscript onnx # export + # Python + python - <=1.7`. To install run: + [**Python>=3.6.0**](https://www.python.org/) with all [requirements.txt](https://github.com/ultralytics/yolov3/blob/master/requirements.txt) installed including [**PyTorch>=1.7**](https://pytorch.org/get-started/locally/). To get started: ```bash + $ git clone https://github.com/ultralytics/yolov3 + $ cd yolov3 $ pip install -r requirements.txt ``` ## Environments - + YOLOv3 may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled): - + - **Google Colab and Kaggle** notebooks with free GPU: Open In Colab Open In Kaggle - **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/GCP-Quickstart) - **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/AWS-Quickstart) - **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/Docker-Quickstart) Docker Pulls - - + + ## Status - - ![CI CPU testing](https://github.com/ultralytics/yolov3/workflows/CI%20CPU%20testing/badge.svg) - - If this badge is green, all [YOLOv3 GitHub Actions](https://github.com/ultralytics/yolov3/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv3 training ([train.py](https://github.com/ultralytics/yolov3/blob/master/train.py)), testing ([test.py](https://github.com/ultralytics/yolov3/blob/master/test.py)), inference ([detect.py](https://github.com/ultralytics/yolov3/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov3/blob/master/models/export.py)) on MacOS, Windows, and Ubuntu every 24 hours and on every commit. - + + CI CPU testing + + If this badge is green, all [YOLOv3 GitHub Actions](https://github.com/ultralytics/yolov3/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv3 training ([train.py](https://github.com/ultralytics/yolov3/blob/master/train.py)), validation ([val.py](https://github.com/ultralytics/yolov3/blob/master/val.py)), inference ([detect.py](https://github.com/ultralytics/yolov3/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov3/blob/master/export.py)) on MacOS, Windows, and Ubuntu every 24 hours and on every commit. diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index e86c57744b..a4db1efb29 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -1,10 +1,9 @@ -name: Automatic Rebase # https://github.com/marketplace/actions/automatic-rebase +name: Automatic Rebase on: issue_comment: types: [created] - jobs: rebase: name: Rebase @@ -14,8 +13,9 @@ jobs: - name: Checkout the latest code uses: actions/checkout@v2 with: - fetch-depth: 0 + token: ${{ secrets.ACTIONS_TOKEN }} + fetch-depth: 0 # otherwise, you will fail to push refs to dest repo - name: Automatic Rebase - uses: cirrus-actions/rebase@1.3.1 + uses: cirrus-actions/rebase@1.5 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e9adff9899..330184e879 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,3 +1,5 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license + name: Close stale issues on: schedule: @@ -7,19 +9,19 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: | πŸ‘‹ Hello, this issue has been automatically marked as stale because it has not had recent activity. Please note it will be closed if no further activity occurs. - Access additional [YOLOv3](https://ultralytics.com/yolov5) πŸš€ resources: + Access additional [YOLOv3](https://ultralytics.com/yolov3) πŸš€ resources: - **Wiki** – https://github.com/ultralytics/yolov3/wiki - **Tutorials** – https://github.com/ultralytics/yolov3#tutorials - **Docs** – https://docs.ultralytics.com Access additional [Ultralytics](https://ultralytics.com) ⚑ resources: - - **Ultralytics HUB** – https://ultralytics.com/pricing + - **Ultralytics HUB** – https://ultralytics.com/hub - **Vision API** – https://ultralytics.com/yolov5 - **About Us** – https://ultralytics.com/about - **Join Our Team** – https://ultralytics.com/work @@ -29,7 +31,7 @@ jobs: Thank you for your contributions to YOLOv3 πŸš€ and Vision AI ⭐! - stale-pr-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions YOLOv3 πŸš€ and Vision AI ⭐.' + stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions YOLOv3 πŸš€ and Vision AI ⭐.' days-before-stale: 30 days-before-close: 5 exempt-issue-labels: 'documentation,tutorial' diff --git a/.gitignore b/.gitignore index 91ce33fb93..5f8cab5500 100755 --- a/.gitignore +++ b/.gitignore @@ -19,26 +19,19 @@ *.avi *.data *.json - *.cfg +!setup.cfg !cfg/yolov3*.cfg storage.googleapis.com runs/* data/* +!data/hyps/* !data/images/zidane.jpg !data/images/bus.jpg -!data/coco.names -!data/coco_paper.names -!data/coco.data -!data/coco_*.data -!data/coco_*.txt -!data/trainvalno5k.shapes !data/*.sh -pycocotools/* -results*.txt -gcp_test*.sh +results*.csv # Datasets ------------------------------------------------------------------------------------------------------------- coco/ @@ -53,9 +46,14 @@ VOC/ # Neural Network weights ----------------------------------------------------------------------------------------------- *.weights *.pt +*.pb *.onnx *.mlmodel *.torchscript +*.tflite +*.h5 +*_saved_model/ +*_web_model/ darknet53.conv.74 yolov3-tiny.conv.15 @@ -84,7 +82,7 @@ sdist/ var/ wheels/ *.egg-info/ -wandb/ +/wandb/ .installed.cfg *.egg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..48e752f448 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,66 @@ +# Define hooks for code formations +# Will be applied on any updated commit files if a user has installed and linked commit hook + +default_language_version: + python: python3.8 + +# Define bot property if installed via https://github.com/marketplace/pre-commit-ci +ci: + autofix_prs: true + autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions' + autoupdate_schedule: quarterly + # submodules: true + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-case-conflict + - id: check-yaml + - id: check-toml + - id: pretty-format-json + - id: check-docstring-first + + - repo: https://github.com/asottile/pyupgrade + rev: v2.23.1 + hooks: + - id: pyupgrade + args: [--py36-plus] + name: Upgrade code + + - repo: https://github.com/PyCQA/isort + rev: 5.9.3 + hooks: + - id: isort + name: Sort imports + + # TODO + #- repo: https://github.com/pre-commit/mirrors-yapf + # rev: v0.31.0 + # hooks: + # - id: yapf + # name: formatting + + # TODO + #- repo: https://github.com/executablebooks/mdformat + # rev: 0.7.7 + # hooks: + # - id: mdformat + # additional_dependencies: + # - mdformat-gfm + # - mdformat-black + # - mdformat_frontmatter + + # TODO + #- repo: https://github.com/asottile/yesqa + # rev: v1.2.3 + # hooks: + # - id: yesqa + + - repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + name: PEP8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..0ef52f638d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,94 @@ +## Contributing to YOLOv3 πŸš€ + +We love your input! We want to make contributing to YOLOv3 as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing a new feature +- Becoming a maintainer + +YOLOv3 works so well due to our combined community effort, and for every small improvement you contribute you will be +helping push the frontiers of what's possible in AI πŸ˜ƒ! + +## Submitting a Pull Request (PR) πŸ› οΈ + +Submitting a PR is easy! This example shows how to submit a PR for updating `requirements.txt` in 4 steps: + +### 1. Select File to Update + +Select `requirements.txt` to update by clicking on it in GitHub. +

PR_step1

+ +### 2. Click 'Edit this file' + +Button is in top-right corner. +

PR_step2

+ +### 3. Make Changes + +Change `matplotlib` version from `3.2.2` to `3.3`. +

PR_step3

+ +### 4. Preview Changes and Submit PR + +Click on the **Preview changes** tab to verify your updates. At the bottom of the screen select 'Create a **new branch** +for this commit', assign your branch a descriptive name such as `fix/matplotlib_version` and click the green **Propose +changes** button. All done, your PR is now submitted to YOLOv3 for review and approval πŸ˜ƒ! +

PR_step4

+ +### PR recommendations + +To allow your work to be integrated as seamlessly as possible, we advise you to: + +- βœ… Verify your PR is **up-to-date with upstream/master.** If your PR is behind upstream/master an + automatic [GitHub actions](https://github.com/ultralytics/yolov3/blob/master/.github/workflows/rebase.yml) rebase may + be attempted by including the /rebase command in a comment body, or by running the following code, replacing 'feature' + with the name of your local branch: + + ```bash + git remote add upstream https://github.com/ultralytics/yolov3.git + git fetch upstream + git checkout feature # <----- replace 'feature' with local branch name + git merge upstream/master + git push -u origin -f + ``` + +- βœ… Verify all Continuous Integration (CI) **checks are passing**. +- βœ… Reduce changes to the absolute **minimum** required for your bug fix or feature addition. _"It is not daily increase + but daily decrease, hack away the unessential. The closer to the source, the less wastage there is."_ β€” Bruce Lee + +## Submitting a Bug Report πŸ› + +If you spot a problem with YOLOv3 please submit a Bug Report! + +For us to start investigating a possible problem we need to be able to reproduce it ourselves first. We've created a few +short guidelines below to help users provide what we need in order to get started. + +When asking a question, people will be better able to provide help if you provide **code** that they can easily +understand and use to **reproduce** the problem. This is referred to by community members as creating +a [minimum reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). Your code that reproduces +the problem should be: + +* βœ… **Minimal** – Use as little code as possible that still produces the same problem +* βœ… **Complete** – Provide **all** parts someone else needs to reproduce your problem in the question itself +* βœ… **Reproducible** – Test the code you're about to provide to make sure it reproduces the problem + +In addition to the above requirements, for [Ultralytics](https://ultralytics.com/) to provide assistance your code +should be: + +* βœ… **Current** – Verify that your code is up-to-date with current + GitHub [master](https://github.com/ultralytics/yolov3/tree/master), and if necessary `git pull` or `git clone` a new + copy to ensure your problem has not already been resolved by previous commits. +* βœ… **Unmodified** – Your problem must be reproducible without any modifications to the codebase in this + repository. [Ultralytics](https://ultralytics.com/) does not provide support for custom code ⚠️. + +If you believe your problem meets all of the above criteria, please close this issue and raise a new one using the πŸ› ** +Bug Report** [template](https://github.com/ultralytics/yolov3/issues/new/choose) and providing +a [minimum reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) to help us better +understand and diagnose your problem. + +## License + +By contributing, you agree that your contributions will be licensed under +the [GPL-3.0 license](https://choosealicense.com/licenses/gpl-3.0/) diff --git a/Dockerfile b/Dockerfile index e058b45b07..78b97305ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license + # Start FROM Nvidia PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch -FROM nvcr.io/nvidia/pytorch:21.03-py3 +FROM nvcr.io/nvidia/pytorch:21.10-py3 # Install linux packages RUN apt update && apt install -y zip htop screen libgl1-mesa-glx @@ -11,5 +13,41 @@ WORKDIR /usr/src/app # Copy contents COPY . /usr/src/app +# Downloads to user config dir +ADD https://ultralytics.com/assets/Arial.ttf /root/.config/Ultralytics/ + # Set environment variables -ENV HOME=/usr/src/app +# ENV HOME=/usr/src/app + + +# Usage Examples ------------------------------------------------------------------------------------------------------- + +# Build and Push +# t=ultralytics/yolov3:latest && sudo docker build -t $t . && sudo docker push $t + +# Pull and Run +# t=ultralytics/yolov3:latest && sudo docker pull $t && sudo docker run -it --ipc=host --gpus all $t + +# Pull and Run with local directory access +# t=ultralytics/yolov3:latest && sudo docker pull $t && sudo docker run -it --ipc=host --gpus all -v "$(pwd)"/datasets:/usr/src/datasets $t + +# Kill all +# sudo docker kill $(sudo docker ps -q) + +# Kill all image-based +# sudo docker kill $(sudo docker ps -qa --filter ancestor=ultralytics/yolov3:latest) + +# Bash into running container +# sudo docker exec -it 5a9b5863d93d bash + +# Bash into stopped container +# id=$(sudo docker ps -qa) && sudo docker start $id && sudo docker exec -it $id bash + +# Clean up +# docker system prune -a --volumes + +# Update Ubuntu drivers +# https://www.maketecheasier.com/install-nvidia-drivers-ubuntu/ + +# DDP test +# python -m torch.distributed.run --nproc_per_node 2 --master_port 1 train.py --epochs 3 diff --git a/LICENSE b/LICENSE index 9e419e0421..92b370f0e0 100644 --- a/LICENSE +++ b/LICENSE @@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. \ No newline at end of file +. diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 04012af4d6..1d961c3c7a --- a/README.md +++ b/README.md @@ -1,160 +1,273 @@ - - -  +
+

+ + +

+
+
+ CI CPU testing + YOLOv3 Citation + Docker Pulls +
+ Open In Colab + Open In Kaggle + Join Forum +
+
+ + +
+

+YOLOv3 πŸš€ is a family of object detection architectures and models pretrained on the COCO dataset, and represents Ultralytics + open-source research into future vision AI methods, incorporating lessons learned and best practices evolved over thousands of hours of research and development. +

+ + + +
+ +##
Documentation
+ +See the [YOLOv3 Docs](https://docs.ultralytics.com) for full documentation on training, testing and deployment. + +##
Quick Start Examples
+ +
+Install + +[**Python>=3.6.0**](https://www.python.org/) is required with all +[requirements.txt](https://github.com/ultralytics/yolov3/blob/master/requirements.txt) installed including +[**PyTorch>=1.7**](https://pytorch.org/get-started/locally/): + -CI CPU testing - -This repository represents Ultralytics open-source research into future object detection methods, and incorporates lessons learned and best practices evolved over thousands of hours of training and evolution on anonymized client datasets. **All code and models are under active development, and are subject to modification or deletion without notice.** Use at your own risk. - -

-
- YOLOv5-P5 640 Figure (click to expand) - -

-
-
- Figure Notes (click to expand) - - * GPU Speed measures end-to-end time per image averaged over 5000 COCO val2017 images using a V100 GPU with batch size 32, and includes image preprocessing, PyTorch FP16 inference, postprocessing and NMS. - * EfficientDet data from [google/automl](https://github.com/google/automl) at batch size 8. - * **Reproduce** by `python test.py --task study --data coco.yaml --iou 0.7 --weights yolov3.pt yolov3-spp.pt yolov3-tiny.pt yolov5l.pt` -
- - -## Branch Notice - -The [ultralytics/yolov3](https://github.com/ultralytics/yolov3) repository is now divided into two branches: -* [Master branch](https://github.com/ultralytics/yolov3/tree/master): Forward-compatible with all [YOLOv5](https://github.com/ultralytics/yolov5) models and methods (**recommended** βœ…). ```bash -$ git clone https://github.com/ultralytics/yolov3 # master branch (default) -``` -* [Archive branch](https://github.com/ultralytics/yolov3/tree/archive): Backwards-compatible with original [darknet](https://pjreddie.com/darknet/) *.cfg models (**no longer maintained** ⚠️). -```bash -$ git clone https://github.com/ultralytics/yolov3 -b archive # archive branch +$ git clone https://github.com/ultralytics/yolov3 +$ cd yolov3 +$ pip install -r requirements.txt ``` -## Pretrained Checkpoints - -[assets3]: https://github.com/ultralytics/yolov3/releases -[assets5]: https://github.com/ultralytics/yolov5/releases - -Model |size
(pixels) |mAPval
0.5:0.95 |mAPtest
0.5:0.95 |mAPval
0.5 |Speed
V100 (ms) | |params
(M) |FLOPS
640 (B) ---- |--- |--- |--- |--- |--- |---|--- |--- -[YOLOv3-tiny][assets3] |640 |17.6 |17.6 |34.8 |**1.2** | |8.8 |13.2 -[YOLOv3][assets3] |640 |43.3 |43.3 |63.0 |4.1 | |61.9 |156.3 -[YOLOv3-SPP][assets3] |640 |44.3 |44.3 |64.6 |4.1 | |63.0 |157.1 -| | | | | | || | -[YOLOv5l][assets5] |640 |**48.2** |**48.2** |**66.9** |3.7 | |47.0 |115.4 - - -
- Table Notes (click to expand) - - * APtest denotes COCO [test-dev2017](http://cocodataset.org/#upload) server results, all other AP results denote val2017 accuracy. - * AP values are for single-model single-scale unless otherwise noted. **Reproduce mAP** by `python test.py --data coco.yaml --img 640 --conf 0.001 --iou 0.65` - * SpeedGPU averaged over 5000 COCO val2017 images using a GCP [n1-standard-16](https://cloud.google.com/compute/docs/machine-types#n1_standard_machine_types) V100 instance, and includes FP16 inference, postprocessing and NMS. **Reproduce speed** by `python test.py --data coco.yaml --img 640 --conf 0.25 --iou 0.45` - * All checkpoints are trained to 300 epochs with default settings and hyperparameters (no autoaugmentation).
+
+Inference -## Requirements +Inference with YOLOv3 and [PyTorch Hub](https://github.com/ultralytics/yolov5/issues/36). Models automatically download +from the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases). -Python 3.8 or later with all [requirements.txt](https://github.com/ultralytics/yolov3/blob/master/requirements.txt) dependencies installed, including `torch>=1.7`. To install run: -```bash -$ pip install -r requirements.txt -``` +```python +import torch +# Model +model = torch.hub.load('ultralytics/yolov3', 'yolov3') # or yolov3-spp, yolov3-tiny, custom -## Tutorials +# Images +img = 'https://ultralytics.com/images/zidane.jpg' # or file, Path, PIL, OpenCV, numpy, list -* [Train Custom Data](https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data)  πŸš€ RECOMMENDED -* [Tips for Best Training Results](https://github.com/ultralytics/yolov5/wiki/Tips-for-Best-Training-Results)  ☘️ RECOMMENDED -* [Weights & Biases Logging](https://github.com/ultralytics/yolov5/issues/1289)  🌟 NEW -* [Supervisely Ecosystem](https://github.com/ultralytics/yolov5/issues/2518)  🌟 NEW -* [Multi-GPU Training](https://github.com/ultralytics/yolov5/issues/475) -* [PyTorch Hub](https://github.com/ultralytics/yolov5/issues/36)  ⭐ NEW -* [TorchScript, ONNX, CoreML Export](https://github.com/ultralytics/yolov5/issues/251) πŸš€ -* [Test-Time Augmentation (TTA)](https://github.com/ultralytics/yolov5/issues/303) -* [Model Ensembling](https://github.com/ultralytics/yolov5/issues/318) -* [Model Pruning/Sparsity](https://github.com/ultralytics/yolov5/issues/304) -* [Hyperparameter Evolution](https://github.com/ultralytics/yolov5/issues/607) -* [Transfer Learning with Frozen Layers](https://github.com/ultralytics/yolov5/issues/1314)  ⭐ NEW -* [TensorRT Deployment](https://github.com/wang-xinyu/tensorrtx) +# Inference +results = model(img) +# Results +results.print() # or .show(), .save(), .crop(), .pandas(), etc. +``` -## Environments +
-YOLOv3 may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled): -- **Google Colab and Kaggle** notebooks with free GPU: Open In Colab Open In Kaggle -- **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/GCP-Quickstart) -- **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/AWS-Quickstart) -- **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/Docker-Quickstart) Docker Pulls +
+Inference with detect.py -## Inference +`detect.py` runs inference on a variety of sources, downloading models automatically from +the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases) and saving results to `runs/detect`. -`detect.py` runs inference on a variety of sources, downloading models automatically from the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases) and saving results to `runs/detect`. ```bash $ python detect.py --source 0 # webcam - file.jpg # image - file.mp4 # video + img.jpg # image + vid.mp4 # video path/ # directory path/*.jpg # glob - 'https://youtu.be/NUsoVlDFqZg' # YouTube video + 'https://youtu.be/Zgi9g1ksQHc' # YouTube 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream ``` -To run inference on example images in `data/images`: -```bash -$ python detect.py --source data/images --weights yolov3.pt --conf 0.25 -``` - - -### PyTorch Hub - -To run **batched inference** with YOLOv3 and [PyTorch Hub](https://github.com/ultralytics/yolov5/issues/36): -```python -import torch +
-# Model -model = torch.hub.load('ultralytics/yolov3', 'yolov3') # or 'yolov3_spp', 'yolov3_tiny' +
+Training -# Image -img = 'https://ultralytics.com/images/zidane.jpg' -# Inference -results = model(img) -results.print() # or .show(), .save() -``` + -## Training +
-Run commands below to reproduce results on [COCO](https://github.com/ultralytics/yolov3/blob/master/data/scripts/get_coco.sh) dataset (dataset auto-downloads on first use). Training times for YOLOv3/YOLOv3-SPP/YOLOv3-tiny are 6/6/2 days on a single V100 (multi-GPU times faster). Use the largest `--batch-size` your GPU allows (batch sizes shown for 16 GB devices). -```bash -$ python train.py --data coco.yaml --cfg yolov3.yaml --weights '' --batch-size 24 - yolov3-spp.yaml 24 - yolov3-tiny.yaml 64 -``` - +
+Tutorials +* [Train Custom Data](https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data)  πŸš€ RECOMMENDED +* [Tips for Best Training Results](https://github.com/ultralytics/yolov3/wiki/Tips-for-Best-Training-Results)  ☘️ + RECOMMENDED +* [Weights & Biases Logging](https://github.com/ultralytics/yolov5/issues/1289)  🌟 NEW +* [Roboflow for Datasets, Labeling, and Active Learning](https://github.com/ultralytics/yolov5/issues/4975)  🌟 NEW +* [Multi-GPU Training](https://github.com/ultralytics/yolov5/issues/475) +* [PyTorch Hub](https://github.com/ultralytics/yolov5/issues/36)  ⭐ NEW +* [TorchScript, ONNX, CoreML Export](https://github.com/ultralytics/yolov5/issues/251) πŸš€ +* [Test-Time Augmentation (TTA)](https://github.com/ultralytics/yolov5/issues/303) +* [Model Ensembling](https://github.com/ultralytics/yolov5/issues/318) +* [Model Pruning/Sparsity](https://github.com/ultralytics/yolov5/issues/304) +* [Hyperparameter Evolution](https://github.com/ultralytics/yolov5/issues/607) +* [Transfer Learning with Frozen Layers](https://github.com/ultralytics/yolov5/issues/1314)  ⭐ NEW +* [TensorRT Deployment](https://github.com/wang-xinyu/tensorrtx) -## Citation +
-[![DOI](https://zenodo.org/badge/146165888.svg)](https://zenodo.org/badge/latestdoi/146165888) +##
Environments
+ +Get started in seconds with our verified environments. Click each icon below for details. + + + +##
Integrations
+ + + +|Weights and Biases|Roboflow ⭐ NEW| +|:-:|:-:| +|Automatically track and visualize all your YOLOv3 training runs in the cloud with [Weights & Biases](https://wandb.ai/site?utm_campaign=repo_yolo_readme)|Label and export your custom datasets directly to YOLOv3 for training with [Roboflow](https://roboflow.com/?ref=ultralytics) | + + +##
Why YOLOv5
+ +

+
+ YOLOv3-P5 640 Figure (click to expand) +

+
+
+ Figure Notes (click to expand) -## About Us +* **COCO AP val** denotes mAP@0.5:0.95 metric measured on the 5000-image [COCO val2017](http://cocodataset.org) dataset over various inference sizes from 256 to 1536. +* **GPU Speed** measures average inference time per image on [COCO val2017](http://cocodataset.org) dataset using a [AWS p3.2xlarge](https://aws.amazon.com/ec2/instance-types/p3/) V100 instance at batch-size 32. +* **EfficientDet** data from [google/automl](https://github.com/google/automl) at batch size 8. +* **Reproduce** by `python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n6.pt yolov5s6.pt yolov5m6.pt yolov5l6.pt yolov5x6.pt` +
-Ultralytics is a U.S.-based particle physics and AI startup with over 6 years of expertise supporting government, academic and business clients. We offer a wide range of vision AI services, spanning from simple expert advice up to delivery of fully customized, end-to-end production solutions, including: -- **Cloud-based AI** systems operating on **hundreds of HD video streams in realtime.** -- **Edge AI** integrated into custom iOS and Android apps for realtime **30 FPS video inference.** -- **Custom data training**, hyperparameter evolution, and model exportation to any destination. +### Pretrained Checkpoints + +[assets]: https://github.com/ultralytics/yolov5/releases +[TTA]: https://github.com/ultralytics/yolov5/issues/303 + +|Model |size
(pixels) |mAPval
0.5:0.95 |mAPval
0.5 |Speed
CPU b1
(ms) |Speed
V100 b1
(ms) |Speed
V100 b32
(ms) |params
(M) |FLOPs
@640 (B) +|--- |--- |--- |--- |--- |--- |--- |--- |--- +|[YOLOv5n][assets] |640 |28.4 |46.0 |**45** |**6.3**|**0.6**|**1.9**|**4.5** +|[YOLOv5s][assets] |640 |37.2 |56.0 |98 |6.4 |0.9 |7.2 |16.5 +|[YOLOv5m][assets] |640 |45.2 |63.9 |224 |8.2 |1.7 |21.2 |49.0 +|[YOLOv5l][assets] |640 |48.8 |67.2 |430 |10.1 |2.7 |46.5 |109.1 +|[YOLOv5x][assets] |640 |50.7 |68.9 |766 |12.1 |4.8 |86.7 |205.7 +| | | | | | | | | +|[YOLOv5n6][assets] |1280 |34.0 |50.7 |153 |8.1 |2.1 |3.2 |4.6 +|[YOLOv5s6][assets] |1280 |44.5 |63.0 |385 |8.2 |3.6 |16.8 |12.6 +|[YOLOv5m6][assets] |1280 |51.0 |69.0 |887 |11.1 |6.8 |35.7 |50.0 +|[YOLOv5l6][assets] |1280 |53.6 |71.6 |1784 |15.8 |10.5 |76.8 |111.4 +|[YOLOv5x6][assets]
+ [TTA][TTA]|1280
1536 |54.7
**55.4** |**72.4**
72.3 |3136
- |26.2
- |19.4
- |140.7
- |209.8
- -For business inquiries and professional support requests please visit us at https://ultralytics.com. +
+ Table Notes (click to expand) +* All checkpoints are trained to 300 epochs with default settings and hyperparameters. +* **mAPval** values are for single-model single-scale on [COCO val2017](http://cocodataset.org) dataset.
Reproduce by `python val.py --data coco.yaml --img 640 --conf 0.001 --iou 0.65` +* **Speed** averaged over COCO val images using a [AWS p3.2xlarge](https://aws.amazon.com/ec2/instance-types/p3/) instance. NMS times (~1 ms/img) not included.
Reproduce by `python val.py --data coco.yaml --img 640 --conf 0.25 --iou 0.45` +* **TTA** [Test Time Augmentation](https://github.com/ultralytics/yolov5/issues/303) includes reflection and scale augmentations.
Reproduce by `python val.py --data coco.yaml --img 1536 --iou 0.7 --augment` -## Contact +
-**Issues should be raised directly in the repository.** For business inquiries or professional support requests please visit https://ultralytics.com or email Glenn Jocher at glenn.jocher@ultralytics.com. +##
Contribute
+ +We love your input! We want to make contributing to YOLOv3 as easy and transparent as possible. Please see our [Contributing Guide](CONTRIBUTING.md) to get started, and fill out the [YOLOv3 Survey](https://ultralytics.com/survey?utm_source=github&utm_medium=social&utm_campaign=Survey) to send us feedback on your experiences. Thank you to all our contributors! + + + + +##
Contact
+ +For YOLOv3 bugs and feature requests please visit [GitHub Issues](https://github.com/ultralytics/yolov3/issues). For business inquiries or +professional support requests please visit [https://ultralytics.com/contact](https://ultralytics.com/contact). + +
+ + diff --git a/data/Argoverse.yaml b/data/Argoverse.yaml new file mode 100644 index 0000000000..9be3ae79a5 --- /dev/null +++ b/data/Argoverse.yaml @@ -0,0 +1,67 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +# Argoverse-HD dataset (ring-front-center camera) http://www.cs.cmu.edu/~mengtial/proj/streaming/ +# Example usage: python train.py --data Argoverse.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── Argoverse ← downloads here + + +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/Argoverse # dataset root dir +train: Argoverse-1.1/images/train/ # train images (relative to 'path') 39384 images +val: Argoverse-1.1/images/val/ # val images (relative to 'path') 15062 images +test: Argoverse-1.1/images/test/ # test images (optional) https://eval.ai/web/challenges/challenge-page/800/overview + +# Classes +nc: 8 # number of classes +names: ['person', 'bicycle', 'car', 'motorcycle', 'bus', 'truck', 'traffic_light', 'stop_sign'] # class names + + +# Download script/URL (optional) --------------------------------------------------------------------------------------- +download: | + import json + + from tqdm import tqdm + from utils.general import download, Path + + + def argoverse2yolo(set): + labels = {} + a = json.load(open(set, "rb")) + for annot in tqdm(a['annotations'], desc=f"Converting {set} to YOLOv3 format..."): + img_id = annot['image_id'] + img_name = a['images'][img_id]['name'] + img_label_name = img_name[:-3] + "txt" + + cls = annot['category_id'] # instance class id + x_center, y_center, width, height = annot['bbox'] + x_center = (x_center + width / 2) / 1920.0 # offset and scale + y_center = (y_center + height / 2) / 1200.0 # offset and scale + width /= 1920.0 # scale + height /= 1200.0 # scale + + img_dir = set.parents[2] / 'Argoverse-1.1' / 'labels' / a['seq_dirs'][a['images'][annot['image_id']]['sid']] + if not img_dir.exists(): + img_dir.mkdir(parents=True, exist_ok=True) + + k = str(img_dir / img_label_name) + if k not in labels: + labels[k] = [] + labels[k].append(f"{cls} {x_center} {y_center} {width} {height}\n") + + for k in labels: + with open(k, "w") as f: + f.writelines(labels[k]) + + + # Download + dir = Path('../datasets/Argoverse') # dataset root dir + urls = ['https://argoverse-hd.s3.us-east-2.amazonaws.com/Argoverse-HD-Full.zip'] + download(urls, dir=dir, delete=False) + + # Convert + annotations_dir = 'Argoverse-HD/annotations/' + (dir / 'Argoverse-1.1' / 'tracking').rename(dir / 'Argoverse-1.1' / 'images') # rename 'tracking' to 'images' + for d in "train.json", "val.json": + argoverse2yolo(dir / annotations_dir / d) # convert VisDrone annotations to YOLO labels diff --git a/data/GlobalWheat2020.yaml b/data/GlobalWheat2020.yaml index ca2a49e2c9..10a2d3fca8 100644 --- a/data/GlobalWheat2020.yaml +++ b/data/GlobalWheat2020.yaml @@ -1,43 +1,41 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license # Global Wheat 2020 dataset http://www.global-wheat.com/ -# Train command: python train.py --data GlobalWheat2020.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /datasets/GlobalWheat2020 -# /yolov3 - - -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: # 3422 images - - ../datasets/GlobalWheat2020/images/arvalis_1 - - ../datasets/GlobalWheat2020/images/arvalis_2 - - ../datasets/GlobalWheat2020/images/arvalis_3 - - ../datasets/GlobalWheat2020/images/ethz_1 - - ../datasets/GlobalWheat2020/images/rres_1 - - ../datasets/GlobalWheat2020/images/inrae_1 - - ../datasets/GlobalWheat2020/images/usask_1 - -val: # 748 images (WARNING: train set contains ethz_1) - - ../datasets/GlobalWheat2020/images/ethz_1 - -test: # 1276 images - - ../datasets/GlobalWheat2020/images/utokyo_1 - - ../datasets/GlobalWheat2020/images/utokyo_2 - - ../datasets/GlobalWheat2020/images/nau_1 - - ../datasets/GlobalWheat2020/images/uq_1 - -# number of classes -nc: 1 - -# class names -names: [ 'wheat_head' ] - - -# download command/URL (optional) -------------------------------------------------------------------------------------- +# Example usage: python train.py --data GlobalWheat2020.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── GlobalWheat2020 ← downloads here + + +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/GlobalWheat2020 # dataset root dir +train: # train images (relative to 'path') 3422 images + - images/arvalis_1 + - images/arvalis_2 + - images/arvalis_3 + - images/ethz_1 + - images/rres_1 + - images/inrae_1 + - images/usask_1 +val: # val images (relative to 'path') 748 images (WARNING: train set contains ethz_1) + - images/ethz_1 +test: # test images (optional) 1276 images + - images/utokyo_1 + - images/utokyo_2 + - images/nau_1 + - images/uq_1 + +# Classes +nc: 1 # number of classes +names: ['wheat_head'] # class names + + +# Download script/URL (optional) --------------------------------------------------------------------------------------- download: | from utils.general import download, Path # Download - dir = Path('../datasets/GlobalWheat2020') # dataset directory + dir = Path(yaml['path']) # dataset root dir urls = ['https://zenodo.org/record/4298502/files/global-wheat-codalab-official.zip', 'https://github.com/ultralytics/yolov5/releases/download/v1.0/GlobalWheat2020_labels.zip'] download(urls, dir=dir) diff --git a/data/SKU-110K.yaml b/data/SKU-110K.yaml index f4aea8b55e..183d3637b4 100644 --- a/data/SKU-110K.yaml +++ b/data/SKU-110K.yaml @@ -1,39 +1,39 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license # SKU-110K retail items dataset https://github.com/eg4000/SKU110K_CVPR19 -# Train command: python train.py --data SKU-110K.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /datasets/SKU-110K -# /yolov3 +# Example usage: python train.py --data SKU-110K.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── SKU-110K ← downloads here -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: ../datasets/SKU-110K/train.txt # 8219 images -val: ../datasets/SKU-110K/val.txt # 588 images -test: ../datasets/SKU-110K/test.txt # 2936 images +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/SKU-110K # dataset root dir +train: train.txt # train images (relative to 'path') 8219 images +val: val.txt # val images (relative to 'path') 588 images +test: test.txt # test images (optional) 2936 images -# number of classes -nc: 1 +# Classes +nc: 1 # number of classes +names: ['object'] # class names -# class names -names: [ 'object' ] - -# download command/URL (optional) -------------------------------------------------------------------------------------- +# Download script/URL (optional) --------------------------------------------------------------------------------------- download: | import shutil from tqdm import tqdm from utils.general import np, pd, Path, download, xyxy2xywh # Download - datasets = Path('../datasets') # download directory + dir = Path(yaml['path']) # dataset root dir + parent = Path(dir.parent) # download dir urls = ['http://trax-geometry.s3.amazonaws.com/cvpr_challenge/SKU110K_fixed.tar.gz'] - download(urls, dir=datasets, delete=False) + download(urls, dir=parent, delete=False) # Rename directories - dir = (datasets / 'SKU-110K') if dir.exists(): shutil.rmtree(dir) - (datasets / 'SKU110K_fixed').rename(dir) # rename dir + (parent / 'SKU110K_fixed').rename(dir) # rename dir (dir / 'labels').mkdir(parents=True, exist_ok=True) # create labels dir # Convert labels diff --git a/data/VisDrone.yaml b/data/VisDrone.yaml index 59f1cd5105..945f05b40f 100644 --- a/data/VisDrone.yaml +++ b/data/VisDrone.yaml @@ -1,24 +1,24 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license # VisDrone2019-DET dataset https://github.com/VisDrone/VisDrone-Dataset -# Train command: python train.py --data VisDrone.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /VisDrone -# /yolov3 +# Example usage: python train.py --data VisDrone.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── VisDrone ← downloads here -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: ../VisDrone/VisDrone2019-DET-train/images # 6471 images -val: ../VisDrone/VisDrone2019-DET-val/images # 548 images -test: ../VisDrone/VisDrone2019-DET-test-dev/images # 1610 images +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/VisDrone # dataset root dir +train: VisDrone2019-DET-train/images # train images (relative to 'path') 6471 images +val: VisDrone2019-DET-val/images # val images (relative to 'path') 548 images +test: VisDrone2019-DET-test-dev/images # test images (optional) 1610 images -# number of classes -nc: 10 +# Classes +nc: 10 # number of classes +names: ['pedestrian', 'people', 'bicycle', 'car', 'van', 'truck', 'tricycle', 'awning-tricycle', 'bus', 'motor'] -# class names -names: [ 'pedestrian', 'people', 'bicycle', 'car', 'van', 'truck', 'tricycle', 'awning-tricycle', 'bus', 'motor' ] - -# download command/URL (optional) -------------------------------------------------------------------------------------- +# Download script/URL (optional) --------------------------------------------------------------------------------------- download: | from utils.general import download, os, Path @@ -49,7 +49,7 @@ download: | # Download - dir = Path('../VisDrone') # dataset directory + dir = Path(yaml['path']) # dataset root dir urls = ['https://github.com/ultralytics/yolov5/releases/download/v1.0/VisDrone2019-DET-train.zip', 'https://github.com/ultralytics/yolov5/releases/download/v1.0/VisDrone2019-DET-val.zip', 'https://github.com/ultralytics/yolov5/releases/download/v1.0/VisDrone2019-DET-test-dev.zip', diff --git a/data/argoverse_hd.yaml b/data/argoverse_hd.yaml deleted file mode 100644 index 29d49b8807..0000000000 --- a/data/argoverse_hd.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Argoverse-HD dataset (ring-front-center camera) http://www.cs.cmu.edu/~mengtial/proj/streaming/ -# Train command: python train.py --data argoverse_hd.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /argoverse -# /yolov3 - - -# download command/URL (optional) -download: bash data/scripts/get_argoverse_hd.sh - -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: ../argoverse/Argoverse-1.1/images/train/ # 39384 images -val: ../argoverse/Argoverse-1.1/images/val/ # 15062 iamges -test: ../argoverse/Argoverse-1.1/images/test/ # Submit to: https://eval.ai/web/challenges/challenge-page/800/overview - -# number of classes -nc: 8 - -# class names -names: [ 'person', 'bicycle', 'car', 'motorcycle', 'bus', 'truck', 'traffic_light', 'stop_sign' ] diff --git a/data/coco.yaml b/data/coco.yaml index 8714e6a33f..1d89a3a033 100644 --- a/data/coco.yaml +++ b/data/coco.yaml @@ -1,35 +1,44 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license # COCO 2017 dataset http://cocodataset.org -# Train command: python train.py --data coco.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /coco -# /yolov3 +# Example usage: python train.py --data coco.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── coco ← downloads here -# download command/URL (optional) -download: bash data/scripts/get_coco.sh +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/coco # dataset root dir +train: train2017.txt # train images (relative to 'path') 118287 images +val: val2017.txt # train images (relative to 'path') 5000 images +test: test-dev2017.txt # 20288 of 40670 images, submit to https://competitions.codalab.org/competitions/20794 -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: ../coco/train2017.txt # 118287 images -val: ../coco/val2017.txt # 5000 images -test: ../coco/test-dev2017.txt # 20288 of 40670 images, submit to https://competitions.codalab.org/competitions/20794 +# Classes +nc: 80 # number of classes +names: ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', + 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', + 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', + 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', + 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', + 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', + 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', + 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', + 'hair drier', 'toothbrush'] # class names -# number of classes -nc: 80 -# class names -names: [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', - 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', - 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', - 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', - 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', - 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', - 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', - 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', - 'hair drier', 'toothbrush' ] +# Download script/URL (optional) +download: | + from utils.general import download, Path -# Print classes -# with open('data/coco.yaml') as f: -# d = yaml.safe_load(f) # dict -# for i, x in enumerate(d['names']): -# print(i, x) + # Download labels + segments = False # segment or box labels + dir = Path(yaml['path']) # dataset root dir + url = 'https://github.com/ultralytics/yolov5/releases/download/v1.0/' + urls = [url + ('coco2017labels-segments.zip' if segments else 'coco2017labels.zip')] # labels + download(urls, dir=dir.parent) + + # Download data + urls = ['http://images.cocodataset.org/zips/train2017.zip', # 19G, 118k images + 'http://images.cocodataset.org/zips/val2017.zip', # 1G, 5k images + 'http://images.cocodataset.org/zips/test2017.zip'] # 7G, 41k images (optional) + download(urls, dir=dir / 'images', threads=3) diff --git a/data/coco128.yaml b/data/coco128.yaml index ef1f6c62fb..19e8d8001e 100644 --- a/data/coco128.yaml +++ b/data/coco128.yaml @@ -1,28 +1,30 @@ -# COCO 2017 dataset http://cocodataset.org - first 128 training images -# Train command: python train.py --data coco128.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /coco128 -# /yolov3 +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +# COCO128 dataset https://www.kaggle.com/ultralytics/coco128 (first 128 images from COCO train2017) +# Example usage: python train.py --data coco128.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── coco128 ← downloads here -# download command/URL (optional) -download: https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128.zip +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/coco128 # dataset root dir +train: images/train2017 # train images (relative to 'path') 128 images +val: images/train2017 # val images (relative to 'path') 128 images +test: # test images (optional) -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: ../coco128/images/train2017/ # 128 images -val: ../coco128/images/train2017/ # 128 images +# Classes +nc: 80 # number of classes +names: ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', + 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', + 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', + 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', + 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', + 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', + 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', + 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', + 'hair drier', 'toothbrush'] # class names -# number of classes -nc: 80 -# class names -names: [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', - 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', - 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', - 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', - 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', - 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', - 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', - 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', - 'hair drier', 'toothbrush' ] +# Download script/URL (optional) +download: https://ultralytics.com/assets/coco128.zip diff --git a/data/hyp.finetune.yaml b/data/hyp.finetune.yaml deleted file mode 100644 index 1b84cff95c..0000000000 --- a/data/hyp.finetune.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# Hyperparameters for VOC finetuning -# python train.py --batch 64 --weights yolov5m.pt --data voc.yaml --img 512 --epochs 50 -# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials - - -# Hyperparameter Evolution Results -# Generations: 306 -# P R mAP.5 mAP.5:.95 box obj cls -# Metrics: 0.6 0.936 0.896 0.684 0.0115 0.00805 0.00146 - -lr0: 0.0032 -lrf: 0.12 -momentum: 0.843 -weight_decay: 0.00036 -warmup_epochs: 2.0 -warmup_momentum: 0.5 -warmup_bias_lr: 0.05 -box: 0.0296 -cls: 0.243 -cls_pw: 0.631 -obj: 0.301 -obj_pw: 0.911 -iou_t: 0.2 -anchor_t: 2.91 -# anchors: 3.63 -fl_gamma: 0.0 -hsv_h: 0.0138 -hsv_s: 0.664 -hsv_v: 0.464 -degrees: 0.373 -translate: 0.245 -scale: 0.898 -shear: 0.602 -perspective: 0.0 -flipud: 0.00856 -fliplr: 0.5 -mosaic: 1.0 -mixup: 0.243 diff --git a/data/hyp.finetune_objects365.yaml b/data/hyp.finetune_objects365.yaml deleted file mode 100644 index 2b104ef2d9..0000000000 --- a/data/hyp.finetune_objects365.yaml +++ /dev/null @@ -1,28 +0,0 @@ -lr0: 0.00258 -lrf: 0.17 -momentum: 0.779 -weight_decay: 0.00058 -warmup_epochs: 1.33 -warmup_momentum: 0.86 -warmup_bias_lr: 0.0711 -box: 0.0539 -cls: 0.299 -cls_pw: 0.825 -obj: 0.632 -obj_pw: 1.0 -iou_t: 0.2 -anchor_t: 3.44 -anchors: 3.2 -fl_gamma: 0.0 -hsv_h: 0.0188 -hsv_s: 0.704 -hsv_v: 0.36 -degrees: 0.0 -translate: 0.0902 -scale: 0.491 -shear: 0.0 -perspective: 0.0 -flipud: 0.0 -fliplr: 0.5 -mosaic: 1.0 -mixup: 0.0 diff --git a/data/hyps/hyp.scratch-high.yaml b/data/hyps/hyp.scratch-high.yaml new file mode 100644 index 0000000000..07ad9fc295 --- /dev/null +++ b/data/hyps/hyp.scratch-high.yaml @@ -0,0 +1,34 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +# Hyperparameters for high-augmentation COCO training from scratch +# python train.py --batch 32 --cfg yolov5m6.yaml --weights '' --data coco.yaml --img 1280 --epochs 300 +# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials + +lr0: 0.01 # initial learning rate (SGD=1E-2, Adam=1E-3) +lrf: 0.2 # final OneCycleLR learning rate (lr0 * lrf) +momentum: 0.937 # SGD momentum/Adam beta1 +weight_decay: 0.0005 # optimizer weight decay 5e-4 +warmup_epochs: 3.0 # warmup epochs (fractions ok) +warmup_momentum: 0.8 # warmup initial momentum +warmup_bias_lr: 0.1 # warmup initial bias lr +box: 0.05 # box loss gain +cls: 0.3 # cls loss gain +cls_pw: 1.0 # cls BCELoss positive_weight +obj: 0.7 # obj loss gain (scale with pixels) +obj_pw: 1.0 # obj BCELoss positive_weight +iou_t: 0.20 # IoU training threshold +anchor_t: 4.0 # anchor-multiple threshold +# anchors: 3 # anchors per output layer (0 to ignore) +fl_gamma: 0.0 # focal loss gamma (efficientDet default gamma=1.5) +hsv_h: 0.015 # image HSV-Hue augmentation (fraction) +hsv_s: 0.7 # image HSV-Saturation augmentation (fraction) +hsv_v: 0.4 # image HSV-Value augmentation (fraction) +degrees: 0.0 # image rotation (+/- deg) +translate: 0.1 # image translation (+/- fraction) +scale: 0.9 # image scale (+/- gain) +shear: 0.0 # image shear (+/- deg) +perspective: 0.0 # image perspective (+/- fraction), range 0-0.001 +flipud: 0.0 # image flip up-down (probability) +fliplr: 0.5 # image flip left-right (probability) +mosaic: 1.0 # image mosaic (probability) +mixup: 0.1 # image mixup (probability) +copy_paste: 0.1 # segment copy-paste (probability) diff --git a/data/hyps/hyp.scratch-low.yaml b/data/hyps/hyp.scratch-low.yaml new file mode 100644 index 0000000000..3f9849c9fa --- /dev/null +++ b/data/hyps/hyp.scratch-low.yaml @@ -0,0 +1,34 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +# Hyperparameters for low-augmentation COCO training from scratch +# python train.py --batch 64 --cfg yolov5n6.yaml --weights '' --data coco.yaml --img 640 --epochs 300 --linear +# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials + +lr0: 0.01 # initial learning rate (SGD=1E-2, Adam=1E-3) +lrf: 0.01 # final OneCycleLR learning rate (lr0 * lrf) +momentum: 0.937 # SGD momentum/Adam beta1 +weight_decay: 0.0005 # optimizer weight decay 5e-4 +warmup_epochs: 3.0 # warmup epochs (fractions ok) +warmup_momentum: 0.8 # warmup initial momentum +warmup_bias_lr: 0.1 # warmup initial bias lr +box: 0.05 # box loss gain +cls: 0.5 # cls loss gain +cls_pw: 1.0 # cls BCELoss positive_weight +obj: 1.0 # obj loss gain (scale with pixels) +obj_pw: 1.0 # obj BCELoss positive_weight +iou_t: 0.20 # IoU training threshold +anchor_t: 4.0 # anchor-multiple threshold +# anchors: 3 # anchors per output layer (0 to ignore) +fl_gamma: 0.0 # focal loss gamma (efficientDet default gamma=1.5) +hsv_h: 0.015 # image HSV-Hue augmentation (fraction) +hsv_s: 0.7 # image HSV-Saturation augmentation (fraction) +hsv_v: 0.4 # image HSV-Value augmentation (fraction) +degrees: 0.0 # image rotation (+/- deg) +translate: 0.1 # image translation (+/- fraction) +scale: 0.5 # image scale (+/- gain) +shear: 0.0 # image shear (+/- deg) +perspective: 0.0 # image perspective (+/- fraction), range 0-0.001 +flipud: 0.0 # image flip up-down (probability) +fliplr: 0.5 # image flip left-right (probability) +mosaic: 1.0 # image mosaic (probability) +mixup: 0.0 # image mixup (probability) +copy_paste: 0.0 # segment copy-paste (probability) diff --git a/data/hyps/hyp.scratch-med.yaml b/data/hyps/hyp.scratch-med.yaml new file mode 100644 index 0000000000..d1f480bcb2 --- /dev/null +++ b/data/hyps/hyp.scratch-med.yaml @@ -0,0 +1,34 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +# Hyperparameters for medium-augmentation COCO training from scratch +# python train.py --batch 32 --cfg yolov5m6.yaml --weights '' --data coco.yaml --img 1280 --epochs 300 +# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials + +lr0: 0.01 # initial learning rate (SGD=1E-2, Adam=1E-3) +lrf: 0.1 # final OneCycleLR learning rate (lr0 * lrf) +momentum: 0.937 # SGD momentum/Adam beta1 +weight_decay: 0.0005 # optimizer weight decay 5e-4 +warmup_epochs: 3.0 # warmup epochs (fractions ok) +warmup_momentum: 0.8 # warmup initial momentum +warmup_bias_lr: 0.1 # warmup initial bias lr +box: 0.05 # box loss gain +cls: 0.3 # cls loss gain +cls_pw: 1.0 # cls BCELoss positive_weight +obj: 0.7 # obj loss gain (scale with pixels) +obj_pw: 1.0 # obj BCELoss positive_weight +iou_t: 0.20 # IoU training threshold +anchor_t: 4.0 # anchor-multiple threshold +# anchors: 3 # anchors per output layer (0 to ignore) +fl_gamma: 0.0 # focal loss gamma (efficientDet default gamma=1.5) +hsv_h: 0.015 # image HSV-Hue augmentation (fraction) +hsv_s: 0.7 # image HSV-Saturation augmentation (fraction) +hsv_v: 0.4 # image HSV-Value augmentation (fraction) +degrees: 0.0 # image rotation (+/- deg) +translate: 0.1 # image translation (+/- fraction) +scale: 0.9 # image scale (+/- gain) +shear: 0.0 # image shear (+/- deg) +perspective: 0.0 # image perspective (+/- fraction), range 0-0.001 +flipud: 0.0 # image flip up-down (probability) +fliplr: 0.5 # image flip left-right (probability) +mosaic: 1.0 # image mosaic (probability) +mixup: 0.1 # image mixup (probability) +copy_paste: 0.0 # segment copy-paste (probability) diff --git a/data/hyp.scratch.yaml b/data/hyps/hyp.scratch.yaml similarity index 90% rename from data/hyp.scratch.yaml rename to data/hyps/hyp.scratch.yaml index 44f26b6658..31f6d142e2 100644 --- a/data/hyp.scratch.yaml +++ b/data/hyps/hyp.scratch.yaml @@ -1,10 +1,10 @@ +# YOLOv5 πŸš€ by Ultralytics, GPL-3.0 license # Hyperparameters for COCO training from scratch # python train.py --batch 40 --cfg yolov5m.yaml --weights '' --data coco.yaml --img 640 --epochs 300 # See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials - lr0: 0.01 # initial learning rate (SGD=1E-2, Adam=1E-3) -lrf: 0.2 # final OneCycleLR learning rate (lr0 * lrf) +lrf: 0.1 # final OneCycleLR learning rate (lr0 * lrf) momentum: 0.937 # SGD momentum/Adam beta1 weight_decay: 0.0005 # optimizer weight decay 5e-4 warmup_epochs: 3.0 # warmup epochs (fractions ok) @@ -31,3 +31,4 @@ flipud: 0.0 # image flip up-down (probability) fliplr: 0.5 # image flip left-right (probability) mosaic: 1.0 # image mosaic (probability) mixup: 0.0 # image mixup (probability) +copy_paste: 0.0 # segment copy-paste (probability) diff --git a/data/objects365.yaml b/data/objects365.yaml index 1cac32f4e2..3472f0f6b9 100644 --- a/data/objects365.yaml +++ b/data/objects365.yaml @@ -1,102 +1,112 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license # Objects365 dataset https://www.objects365.org/ -# Train command: python train.py --data objects365.yaml -# Default dataset location is next to YOLOv3: -# /parent_folder -# /datasets/objects365 -# /yolov3 +# Example usage: python train.py --data Objects365.yaml +# parent +# β”œβ”€β”€ yolov3 +# └── datasets +# └── Objects365 ← downloads here -# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/] -train: ../datasets/objects365/images/train # 1742289 images -val: ../datasets/objects365/images/val # 5570 images -# number of classes -nc: 365 +# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] +path: ../datasets/Objects365 # dataset root dir +train: images/train # train images (relative to 'path') 1742289 images +val: images/val # val images (relative to 'path') 80000 images +test: # test images (optional) -# class names -names: [ 'Person', 'Sneakers', 'Chair', 'Other Shoes', 'Hat', 'Car', 'Lamp', 'Glasses', 'Bottle', 'Desk', 'Cup', - 'Street Lights', 'Cabinet/shelf', 'Handbag/Satchel', 'Bracelet', 'Plate', 'Picture/Frame', 'Helmet', 'Book', - 'Gloves', 'Storage box', 'Boat', 'Leather Shoes', 'Flower', 'Bench', 'Potted Plant', 'Bowl/Basin', 'Flag', - 'Pillow', 'Boots', 'Vase', 'Microphone', 'Necklace', 'Ring', 'SUV', 'Wine Glass', 'Belt', 'Monitor/TV', - 'Backpack', 'Umbrella', 'Traffic Light', 'Speaker', 'Watch', 'Tie', 'Trash bin Can', 'Slippers', 'Bicycle', - 'Stool', 'Barrel/bucket', 'Van', 'Couch', 'Sandals', 'Basket', 'Drum', 'Pen/Pencil', 'Bus', 'Wild Bird', - 'High Heels', 'Motorcycle', 'Guitar', 'Carpet', 'Cell Phone', 'Bread', 'Camera', 'Canned', 'Truck', - 'Traffic cone', 'Cymbal', 'Lifesaver', 'Towel', 'Stuffed Toy', 'Candle', 'Sailboat', 'Laptop', 'Awning', - 'Bed', 'Faucet', 'Tent', 'Horse', 'Mirror', 'Power outlet', 'Sink', 'Apple', 'Air Conditioner', 'Knife', - 'Hockey Stick', 'Paddle', 'Pickup Truck', 'Fork', 'Traffic Sign', 'Balloon', 'Tripod', 'Dog', 'Spoon', 'Clock', - 'Pot', 'Cow', 'Cake', 'Dinning Table', 'Sheep', 'Hanger', 'Blackboard/Whiteboard', 'Napkin', 'Other Fish', - 'Orange/Tangerine', 'Toiletry', 'Keyboard', 'Tomato', 'Lantern', 'Machinery Vehicle', 'Fan', - 'Green Vegetables', 'Banana', 'Baseball Glove', 'Airplane', 'Mouse', 'Train', 'Pumpkin', 'Soccer', 'Skiboard', - 'Luggage', 'Nightstand', 'Tea pot', 'Telephone', 'Trolley', 'Head Phone', 'Sports Car', 'Stop Sign', - 'Dessert', 'Scooter', 'Stroller', 'Crane', 'Remote', 'Refrigerator', 'Oven', 'Lemon', 'Duck', 'Baseball Bat', - 'Surveillance Camera', 'Cat', 'Jug', 'Broccoli', 'Piano', 'Pizza', 'Elephant', 'Skateboard', 'Surfboard', - 'Gun', 'Skating and Skiing shoes', 'Gas stove', 'Donut', 'Bow Tie', 'Carrot', 'Toilet', 'Kite', 'Strawberry', - 'Other Balls', 'Shovel', 'Pepper', 'Computer Box', 'Toilet Paper', 'Cleaning Products', 'Chopsticks', - 'Microwave', 'Pigeon', 'Baseball', 'Cutting/chopping Board', 'Coffee Table', 'Side Table', 'Scissors', - 'Marker', 'Pie', 'Ladder', 'Snowboard', 'Cookies', 'Radiator', 'Fire Hydrant', 'Basketball', 'Zebra', 'Grape', - 'Giraffe', 'Potato', 'Sausage', 'Tricycle', 'Violin', 'Egg', 'Fire Extinguisher', 'Candy', 'Fire Truck', - 'Billiards', 'Converter', 'Bathtub', 'Wheelchair', 'Golf Club', 'Briefcase', 'Cucumber', 'Cigar/Cigarette', - 'Paint Brush', 'Pear', 'Heavy Truck', 'Hamburger', 'Extractor', 'Extension Cord', 'Tong', 'Tennis Racket', - 'Folder', 'American Football', 'earphone', 'Mask', 'Kettle', 'Tennis', 'Ship', 'Swing', 'Coffee Machine', - 'Slide', 'Carriage', 'Onion', 'Green beans', 'Projector', 'Frisbee', 'Washing Machine/Drying Machine', - 'Chicken', 'Printer', 'Watermelon', 'Saxophone', 'Tissue', 'Toothbrush', 'Ice cream', 'Hot-air balloon', - 'Cello', 'French Fries', 'Scale', 'Trophy', 'Cabbage', 'Hot dog', 'Blender', 'Peach', 'Rice', 'Wallet/Purse', - 'Volleyball', 'Deer', 'Goose', 'Tape', 'Tablet', 'Cosmetics', 'Trumpet', 'Pineapple', 'Golf Ball', - 'Ambulance', 'Parking meter', 'Mango', 'Key', 'Hurdle', 'Fishing Rod', 'Medal', 'Flute', 'Brush', 'Penguin', - 'Megaphone', 'Corn', 'Lettuce', 'Garlic', 'Swan', 'Helicopter', 'Green Onion', 'Sandwich', 'Nuts', - 'Speed Limit Sign', 'Induction Cooker', 'Broom', 'Trombone', 'Plum', 'Rickshaw', 'Goldfish', 'Kiwi fruit', - 'Router/modem', 'Poker Card', 'Toaster', 'Shrimp', 'Sushi', 'Cheese', 'Notepaper', 'Cherry', 'Pliers', 'CD', - 'Pasta', 'Hammer', 'Cue', 'Avocado', 'Hamimelon', 'Flask', 'Mushroom', 'Screwdriver', 'Soap', 'Recorder', - 'Bear', 'Eggplant', 'Board Eraser', 'Coconut', 'Tape Measure/Ruler', 'Pig', 'Showerhead', 'Globe', 'Chips', - 'Steak', 'Crosswalk Sign', 'Stapler', 'Camel', 'Formula 1', 'Pomegranate', 'Dishwasher', 'Crab', - 'Hoverboard', 'Meat ball', 'Rice Cooker', 'Tuba', 'Calculator', 'Papaya', 'Antelope', 'Parrot', 'Seal', - 'Butterfly', 'Dumbbell', 'Donkey', 'Lion', 'Urinal', 'Dolphin', 'Electric Drill', 'Hair Dryer', 'Egg tart', - 'Jellyfish', 'Treadmill', 'Lighter', 'Grapefruit', 'Game board', 'Mop', 'Radish', 'Baozi', 'Target', 'French', - 'Spring Rolls', 'Monkey', 'Rabbit', 'Pencil Case', 'Yak', 'Red Cabbage', 'Binoculars', 'Asparagus', 'Barbell', - 'Scallop', 'Noddles', 'Comb', 'Dumpling', 'Oyster', 'Table Tennis paddle', 'Cosmetics Brush/Eyeliner Pencil', - 'Chainsaw', 'Eraser', 'Lobster', 'Durian', 'Okra', 'Lipstick', 'Cosmetics Mirror', 'Curling', 'Table Tennis' ] +# Classes +nc: 365 # number of classes +names: ['Person', 'Sneakers', 'Chair', 'Other Shoes', 'Hat', 'Car', 'Lamp', 'Glasses', 'Bottle', 'Desk', 'Cup', + 'Street Lights', 'Cabinet/shelf', 'Handbag/Satchel', 'Bracelet', 'Plate', 'Picture/Frame', 'Helmet', 'Book', + 'Gloves', 'Storage box', 'Boat', 'Leather Shoes', 'Flower', 'Bench', 'Potted Plant', 'Bowl/Basin', 'Flag', + 'Pillow', 'Boots', 'Vase', 'Microphone', 'Necklace', 'Ring', 'SUV', 'Wine Glass', 'Belt', 'Monitor/TV', + 'Backpack', 'Umbrella', 'Traffic Light', 'Speaker', 'Watch', 'Tie', 'Trash bin Can', 'Slippers', 'Bicycle', + 'Stool', 'Barrel/bucket', 'Van', 'Couch', 'Sandals', 'Basket', 'Drum', 'Pen/Pencil', 'Bus', 'Wild Bird', + 'High Heels', 'Motorcycle', 'Guitar', 'Carpet', 'Cell Phone', 'Bread', 'Camera', 'Canned', 'Truck', + 'Traffic cone', 'Cymbal', 'Lifesaver', 'Towel', 'Stuffed Toy', 'Candle', 'Sailboat', 'Laptop', 'Awning', + 'Bed', 'Faucet', 'Tent', 'Horse', 'Mirror', 'Power outlet', 'Sink', 'Apple', 'Air Conditioner', 'Knife', + 'Hockey Stick', 'Paddle', 'Pickup Truck', 'Fork', 'Traffic Sign', 'Balloon', 'Tripod', 'Dog', 'Spoon', 'Clock', + 'Pot', 'Cow', 'Cake', 'Dinning Table', 'Sheep', 'Hanger', 'Blackboard/Whiteboard', 'Napkin', 'Other Fish', + 'Orange/Tangerine', 'Toiletry', 'Keyboard', 'Tomato', 'Lantern', 'Machinery Vehicle', 'Fan', + 'Green Vegetables', 'Banana', 'Baseball Glove', 'Airplane', 'Mouse', 'Train', 'Pumpkin', 'Soccer', 'Skiboard', + 'Luggage', 'Nightstand', 'Tea pot', 'Telephone', 'Trolley', 'Head Phone', 'Sports Car', 'Stop Sign', + 'Dessert', 'Scooter', 'Stroller', 'Crane', 'Remote', 'Refrigerator', 'Oven', 'Lemon', 'Duck', 'Baseball Bat', + 'Surveillance Camera', 'Cat', 'Jug', 'Broccoli', 'Piano', 'Pizza', 'Elephant', 'Skateboard', 'Surfboard', + 'Gun', 'Skating and Skiing shoes', 'Gas stove', 'Donut', 'Bow Tie', 'Carrot', 'Toilet', 'Kite', 'Strawberry', + 'Other Balls', 'Shovel', 'Pepper', 'Computer Box', 'Toilet Paper', 'Cleaning Products', 'Chopsticks', + 'Microwave', 'Pigeon', 'Baseball', 'Cutting/chopping Board', 'Coffee Table', 'Side Table', 'Scissors', + 'Marker', 'Pie', 'Ladder', 'Snowboard', 'Cookies', 'Radiator', 'Fire Hydrant', 'Basketball', 'Zebra', 'Grape', + 'Giraffe', 'Potato', 'Sausage', 'Tricycle', 'Violin', 'Egg', 'Fire Extinguisher', 'Candy', 'Fire Truck', + 'Billiards', 'Converter', 'Bathtub', 'Wheelchair', 'Golf Club', 'Briefcase', 'Cucumber', 'Cigar/Cigarette', + 'Paint Brush', 'Pear', 'Heavy Truck', 'Hamburger', 'Extractor', 'Extension Cord', 'Tong', 'Tennis Racket', + 'Folder', 'American Football', 'earphone', 'Mask', 'Kettle', 'Tennis', 'Ship', 'Swing', 'Coffee Machine', + 'Slide', 'Carriage', 'Onion', 'Green beans', 'Projector', 'Frisbee', 'Washing Machine/Drying Machine', + 'Chicken', 'Printer', 'Watermelon', 'Saxophone', 'Tissue', 'Toothbrush', 'Ice cream', 'Hot-air balloon', + 'Cello', 'French Fries', 'Scale', 'Trophy', 'Cabbage', 'Hot dog', 'Blender', 'Peach', 'Rice', 'Wallet/Purse', + 'Volleyball', 'Deer', 'Goose', 'Tape', 'Tablet', 'Cosmetics', 'Trumpet', 'Pineapple', 'Golf Ball', + 'Ambulance', 'Parking meter', 'Mango', 'Key', 'Hurdle', 'Fishing Rod', 'Medal', 'Flute', 'Brush', 'Penguin', + 'Megaphone', 'Corn', 'Lettuce', 'Garlic', 'Swan', 'Helicopter', 'Green Onion', 'Sandwich', 'Nuts', + 'Speed Limit Sign', 'Induction Cooker', 'Broom', 'Trombone', 'Plum', 'Rickshaw', 'Goldfish', 'Kiwi fruit', + 'Router/modem', 'Poker Card', 'Toaster', 'Shrimp', 'Sushi', 'Cheese', 'Notepaper', 'Cherry', 'Pliers', 'CD', + 'Pasta', 'Hammer', 'Cue', 'Avocado', 'Hamimelon', 'Flask', 'Mushroom', 'Screwdriver', 'Soap', 'Recorder', + 'Bear', 'Eggplant', 'Board Eraser', 'Coconut', 'Tape Measure/Ruler', 'Pig', 'Showerhead', 'Globe', 'Chips', + 'Steak', 'Crosswalk Sign', 'Stapler', 'Camel', 'Formula 1', 'Pomegranate', 'Dishwasher', 'Crab', + 'Hoverboard', 'Meat ball', 'Rice Cooker', 'Tuba', 'Calculator', 'Papaya', 'Antelope', 'Parrot', 'Seal', + 'Butterfly', 'Dumbbell', 'Donkey', 'Lion', 'Urinal', 'Dolphin', 'Electric Drill', 'Hair Dryer', 'Egg tart', + 'Jellyfish', 'Treadmill', 'Lighter', 'Grapefruit', 'Game board', 'Mop', 'Radish', 'Baozi', 'Target', 'French', + 'Spring Rolls', 'Monkey', 'Rabbit', 'Pencil Case', 'Yak', 'Red Cabbage', 'Binoculars', 'Asparagus', 'Barbell', + 'Scallop', 'Noddles', 'Comb', 'Dumpling', 'Oyster', 'Table Tennis paddle', 'Cosmetics Brush/Eyeliner Pencil', + 'Chainsaw', 'Eraser', 'Lobster', 'Durian', 'Okra', 'Lipstick', 'Cosmetics Mirror', 'Curling', 'Table Tennis'] -# download command/URL (optional) -------------------------------------------------------------------------------------- +# Download script/URL (optional) --------------------------------------------------------------------------------------- download: | from pycocotools.coco import COCO from tqdm import tqdm - from utils.general import download, Path + from utils.general import Path, download, np, xyxy2xywhn # Make Directories - dir = Path('../datasets/objects365') # dataset directory + dir = Path(yaml['path']) # dataset root dir for p in 'images', 'labels': (dir / p).mkdir(parents=True, exist_ok=True) for q in 'train', 'val': (dir / p / q).mkdir(parents=True, exist_ok=True) - # Download - url = "https://dorc.ks3-cn-beijing.ksyun.com/data-set/2020Objects365%E6%95%B0%E6%8D%AE%E9%9B%86/train/" - download([url + 'zhiyuan_objv2_train.tar.gz'], dir=dir, delete=False) # annotations json - download([url + f for f in [f'patch{i}.tar.gz' for i in range(51)]], dir=dir / 'images' / 'train', - curl=True, delete=False, threads=8) + # Train, Val Splits + for split, patches in [('train', 50 + 1), ('val', 43 + 1)]: + print(f"Processing {split} in {patches} patches ...") + images, labels = dir / 'images' / split, dir / 'labels' / split - # Move - train = dir / 'images' / 'train' - for f in tqdm(train.rglob('*.jpg'), desc=f'Moving images'): - f.rename(train / f.name) # move to /images/train + # Download + url = f"https://dorc.ks3-cn-beijing.ksyun.com/data-set/2020Objects365%E6%95%B0%E6%8D%AE%E9%9B%86/{split}/" + if split == 'train': + download([f'{url}zhiyuan_objv2_{split}.tar.gz'], dir=dir, delete=False) # annotations json + download([f'{url}patch{i}.tar.gz' for i in range(patches)], dir=images, curl=True, delete=False, threads=8) + elif split == 'val': + download([f'{url}zhiyuan_objv2_{split}.json'], dir=dir, delete=False) # annotations json + download([f'{url}images/v1/patch{i}.tar.gz' for i in range(15 + 1)], dir=images, curl=True, delete=False, threads=8) + download([f'{url}images/v2/patch{i}.tar.gz' for i in range(16, patches)], dir=images, curl=True, delete=False, threads=8) - # Labels - coco = COCO(dir / 'zhiyuan_objv2_train.json') - names = [x["name"] for x in coco.loadCats(coco.getCatIds())] - for cid, cat in enumerate(names): - catIds = coco.getCatIds(catNms=[cat]) - imgIds = coco.getImgIds(catIds=catIds) - for im in tqdm(coco.loadImgs(imgIds), desc=f'Class {cid + 1}/{len(names)} {cat}'): - width, height = im["width"], im["height"] - path = Path(im["file_name"]) # image filename - try: - with open(dir / 'labels' / 'train' / path.with_suffix('.txt').name, 'a') as file: - annIds = coco.getAnnIds(imgIds=im["id"], catIds=catIds, iscrowd=None) - for a in coco.loadAnns(annIds): - x, y, w, h = a['bbox'] # bounding box in xywh (xy top-left corner) - x, y = x + w / 2, y + h / 2 # xy to center - file.write(f"{cid} {x / width:.5f} {y / height:.5f} {w / width:.5f} {h / height:.5f}\n") + # Move + for f in tqdm(images.rglob('*.jpg'), desc=f'Moving {split} images'): + f.rename(images / f.name) # move to /images/{split} - except Exception as e: - print(e) + # Labels + coco = COCO(dir / f'zhiyuan_objv2_{split}.json') + names = [x["name"] for x in coco.loadCats(coco.getCatIds())] + for cid, cat in enumerate(names): + catIds = coco.getCatIds(catNms=[cat]) + imgIds = coco.getImgIds(catIds=catIds) + for im in tqdm(coco.loadImgs(imgIds), desc=f'Class {cid + 1}/{len(names)} {cat}'): + width, height = im["width"], im["height"] + path = Path(im["file_name"]) # image filename + try: + with open(labels / path.with_suffix('.txt').name, 'a') as file: + annIds = coco.getAnnIds(imgIds=im["id"], catIds=catIds, iscrowd=None) + for a in coco.loadAnns(annIds): + x, y, w, h = a['bbox'] # bounding box in xywh (xy top-left corner) + xyxy = np.array([x, y, x + w, y + h])[None] # pixels(1,4) + x, y, w, h = xyxy2xywhn(xyxy, w=width, h=height, clip=True)[0] # normalized and clipped + file.write(f"{cid} {x:.5f} {y:.5f} {w:.5f} {h:.5f}\n") + except Exception as e: + print(e) diff --git a/data/scripts/download_weights.sh b/data/scripts/download_weights.sh new file mode 100755 index 0000000000..50aec183ab --- /dev/null +++ b/data/scripts/download_weights.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +# Download latest models from https://github.com/ultralytics/yolov3/releases +# Example usage: bash path/to/download_weights.sh +# parent +# └── yolov3 +# β”œβ”€β”€ yolov3.pt ← downloads here +# β”œβ”€β”€ yolov3-spp.pt +# └── ... + +python - <train.txt -cat 2007_train.txt 2007_val.txt 2007_test.txt 2012_train.txt 2012_val.txt >train.all.txt - -mkdir ../VOC ../VOC/images ../VOC/images/train ../VOC/images/val -mkdir ../VOC/labels ../VOC/labels/train ../VOC/labels/val - -python3 - "$@" <= cls >= 0, f'incorrect class index {cls}' + + # Write YOLO label + if id not in shapes: + shapes[id] = Image.open(file).size + box = xyxy2xywhn(box[None].astype(np.float), w=shapes[id][0], h=shapes[id][1], clip=True) + with open((labels / id).with_suffix('.txt'), 'a') as f: + f.write(f"{cls} {' '.join(f'{x:.6f}' for x in box[0])}\n") # write label.txt + except Exception as e: + print(f'WARNING: skipping one label for {file}: {e}') + + + # Download manually from https://challenge.xviewdataset.org + dir = Path(yaml['path']) # dataset root dir + # urls = ['https://d307kc0mrhucc3.cloudfront.net/train_labels.zip', # train labels + # 'https://d307kc0mrhucc3.cloudfront.net/train_images.zip', # 15G, 847 train images + # 'https://d307kc0mrhucc3.cloudfront.net/val_images.zip'] # 5G, 282 val images (no labels) + # download(urls, dir=dir, delete=False) + + # Convert labels + convert_labels(dir / 'xView_train.geojson') + + # Move images + images = Path(dir / 'images') + images.mkdir(parents=True, exist_ok=True) + Path(dir / 'train_images').rename(dir / 'images' / 'train') + Path(dir / 'val_images').rename(dir / 'images' / 'val') + + # Split + autosplit(dir / 'images' / 'train') diff --git a/detect.py b/detect.py index c2bec17e60..1ae4c3c402 100644 --- a/detect.py +++ b/detect.py @@ -1,98 +1,147 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Run inference on images, videos, directories, streams, etc. + +Usage: + $ python path/to/detect.py --weights yolov3.pt --source 0 # webcam + img.jpg # image + vid.mp4 # video + path/ # directory + path/*.jpg # glob + 'https://youtu.be/Zgi9g1ksQHc' # YouTube + 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream +""" + import argparse -import time +import os +import sys from pathlib import Path import cv2 import torch import torch.backends.cudnn as cudnn -from models.experimental import attempt_load -from utils.datasets import LoadStreams, LoadImages -from utils.general import check_img_size, check_requirements, check_imshow, non_max_suppression, apply_classifier, \ - scale_coords, xyxy2xywh, strip_optimizer, set_logging, increment_path, save_one_box -from utils.plots import colors, plot_one_box -from utils.torch_utils import select_device, load_classifier, time_synchronized +FILE = Path(__file__).resolve() +ROOT = FILE.parents[0] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH +ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative + +from models.common import DetectMultiBackend +from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams +from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr, + increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh) +from utils.plots import Annotator, colors, save_one_box +from utils.torch_utils import select_device, time_sync @torch.no_grad() -def detect(opt): - source, weights, view_img, save_txt, imgsz = opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size - save_img = not opt.nosave and not source.endswith('.txt') # save inference images - webcam = source.isnumeric() or source.endswith('.txt') or source.lower().startswith( - ('rtsp://', 'rtmp://', 'http://', 'https://')) +def run(weights=ROOT / 'yolov3.pt', # model.pt path(s) + source=ROOT / 'data/images', # file/dir/URL/glob, 0 for webcam + imgsz=640, # inference size (pixels) + conf_thres=0.25, # confidence threshold + iou_thres=0.45, # NMS IOU threshold + max_det=1000, # maximum detections per image + device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu + view_img=False, # show results + save_txt=False, # save results to *.txt + save_conf=False, # save confidences in --save-txt labels + save_crop=False, # save cropped prediction boxes + nosave=False, # do not save images/videos + classes=None, # filter by class: --class 0, or --class 0 2 3 + agnostic_nms=False, # class-agnostic NMS + augment=False, # augmented inference + visualize=False, # visualize features + update=False, # update all models + project=ROOT / 'runs/detect', # save results to project/name + name='exp', # save results to project/name + exist_ok=False, # existing project/name ok, do not increment + line_thickness=3, # bounding box thickness (pixels) + hide_labels=False, # hide labels + hide_conf=False, # hide confidences + half=False, # use FP16 half-precision inference + dnn=False, # use OpenCV DNN for ONNX inference + ): + source = str(source) + save_img = not nosave and not source.endswith('.txt') # save inference images + is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS) + is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://')) + webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file) + if is_url and is_file: + source = check_file(source) # download # Directories - save_dir = increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok) # increment run + save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir - # Initialize - set_logging() - device = select_device(opt.device) - half = device.type != 'cpu' # half precision only supported on CUDA - # Load model - model = attempt_load(weights, map_location=device) # load FP32 model - stride = int(model.stride.max()) # model stride - imgsz = check_img_size(imgsz, s=stride) # check img_size - names = model.module.names if hasattr(model, 'module') else model.names # get class names - if half: - model.half() # to FP16 - - # Second-stage classifier - classify = False - if classify: - modelc = load_classifier(name='resnet101', n=2) # initialize - modelc.load_state_dict(torch.load('weights/resnet101.pt', map_location=device)['model']).to(device).eval() - - # Set Dataloader - vid_path, vid_writer = None, None + device = select_device(device) + model = DetectMultiBackend(weights, device=device, dnn=dnn) + stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx + imgsz = check_img_size(imgsz, s=stride) # check image size + + # Half + half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA + if pt: + model.model.half() if half else model.model.float() + + # Dataloader if webcam: view_img = check_imshow() cudnn.benchmark = True # set True to speed up constant image size inference - dataset = LoadStreams(source, img_size=imgsz, stride=stride) + dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit) + bs = len(dataset) # batch_size else: - dataset = LoadImages(source, img_size=imgsz, stride=stride) + dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit) + bs = 1 # batch_size + vid_path, vid_writer = [None] * bs, [None] * bs # Run inference - if device.type != 'cpu': - model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once - t0 = time.time() - for path, img, im0s, vid_cap in dataset: - img = torch.from_numpy(img).to(device) - img = img.half() if half else img.float() # uint8 to fp16/32 - img /= 255.0 # 0 - 255 to 0.0 - 1.0 - if img.ndimension() == 3: - img = img.unsqueeze(0) + if pt and device.type != 'cpu': + model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.model.parameters()))) # warmup + dt, seen = [0.0, 0.0, 0.0], 0 + for path, im, im0s, vid_cap, s in dataset: + t1 = time_sync() + im = torch.from_numpy(im).to(device) + im = im.half() if half else im.float() # uint8 to fp16/32 + im /= 255 # 0 - 255 to 0.0 - 1.0 + if len(im.shape) == 3: + im = im[None] # expand for batch dim + t2 = time_sync() + dt[0] += t2 - t1 # Inference - t1 = time_synchronized() - pred = model(img, augment=opt.augment)[0] + visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False + pred = model(im, augment=augment, visualize=visualize) + t3 = time_sync() + dt[1] += t3 - t2 - # Apply NMS - pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, opt.classes, opt.agnostic_nms, - max_det=opt.max_det) - t2 = time_synchronized() + # NMS + pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det) + dt[2] += time_sync() - t3 - # Apply Classifier - if classify: - pred = apply_classifier(pred, modelc, img, im0s) + # Second-stage classifier (optional) + # pred = utils.general.apply_classifier(pred, classifier_model, im, im0s) - # Process detections - for i, det in enumerate(pred): # detections per image + # Process predictions + for i, det in enumerate(pred): # per image + seen += 1 if webcam: # batch_size >= 1 - p, s, im0, frame = path[i], f'{i}: ', im0s[i].copy(), dataset.count + p, im0, frame = path[i], im0s[i].copy(), dataset.count + s += f'{i}: ' else: - p, s, im0, frame = path, '', im0s.copy(), getattr(dataset, 'frame', 0) + p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0) p = Path(p) # to Path - save_path = str(save_dir / p.name) # img.jpg - txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # img.txt - s += '%gx%g ' % img.shape[2:] # print string + save_path = str(save_dir / p.name) # im.jpg + txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt + s += '%gx%g ' % im.shape[2:] # print string gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh - imc = im0.copy() if opt.save_crop else im0 # for opt.save_crop + imc = im0.copy() if save_crop else im0 # for save_crop + annotator = Annotator(im0, line_width=line_thickness, example=str(names)) if len(det): # Rescale boxes from img_size to im0 size - det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round() + det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round() # Print results for c in det[:, -1].unique(): @@ -103,21 +152,22 @@ def detect(opt): for *xyxy, conf, cls in reversed(det): if save_txt: # Write to file xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh - line = (cls, *xywh, conf) if opt.save_conf else (cls, *xywh) # label format + line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format with open(txt_path + '.txt', 'a') as f: f.write(('%g ' * len(line)).rstrip() % line + '\n') - if save_img or opt.save_crop or view_img: # Add bbox to image + if save_img or save_crop or view_img: # Add bbox to image c = int(cls) # integer class - label = None if opt.hide_labels else (names[c] if opt.hide_conf else f'{names[c]} {conf:.2f}') - plot_one_box(xyxy, im0, label=label, color=colors(c, True), line_thickness=opt.line_thickness) - if opt.save_crop: + label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}') + annotator.box_label(xyxy, label, color=colors(c, True)) + if save_crop: save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True) - # Print time (inference + NMS) - print(f'{s}Done. ({t2 - t1:.3f}s)') + # Print time (inference-only) + LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)') # Stream results + im0 = annotator.result() if view_img: cv2.imshow(str(p), im0) cv2.waitKey(1) # 1 millisecond @@ -127,10 +177,10 @@ def detect(opt): if dataset.mode == 'image': cv2.imwrite(save_path, im0) else: # 'video' or 'stream' - if vid_path != save_path: # new video - vid_path = save_path - if isinstance(vid_writer, cv2.VideoWriter): - vid_writer.release() # release previous video writer + if vid_path[i] != save_path: # new video + vid_path[i] = save_path + if isinstance(vid_writer[i], cv2.VideoWriter): + vid_writer[i].release() # release previous video writer if vid_cap: # video fps = vid_cap.get(cv2.CAP_PROP_FPS) w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) @@ -138,47 +188,57 @@ def detect(opt): else: # stream fps, w, h = 30, im0.shape[1], im0.shape[0] save_path += '.mp4' - vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) - vid_writer.write(im0) + vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) + vid_writer[i].write(im0) + # Print results + t = tuple(x / seen * 1E3 for x in dt) # speeds per image + LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t) if save_txt or save_img: s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' - print(f"Results saved to {save_dir}{s}") + LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") + if update: + strip_optimizer(weights) # update model (to fix SourceChangeWarning) - print(f'Done. ({time.time() - t0:.3f}s)') - -if __name__ == '__main__': +def parse_opt(): parser = argparse.ArgumentParser() - parser.add_argument('--weights', nargs='+', type=str, default='yolov3.pt', help='model.pt path(s)') - parser.add_argument('--source', type=str, default='data/images', help='source') # file/folder, 0 for webcam - parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)') - parser.add_argument('--conf-thres', type=float, default=0.25, help='object confidence threshold') - parser.add_argument('--iou-thres', type=float, default=0.45, help='IOU threshold for NMS') - parser.add_argument('--max-det', type=int, default=1000, help='maximum number of detections per image') + parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov3.pt', help='model path(s)') + parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob, 0 for webcam') + parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w') + parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold') + parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold') + parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') - parser.add_argument('--view-img', action='store_true', help='display results') + parser.add_argument('--view-img', action='store_true', help='show results') parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes') parser.add_argument('--nosave', action='store_true', help='do not save images/videos') - parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --class 0, or --class 0 2 3') + parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3') parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS') parser.add_argument('--augment', action='store_true', help='augmented inference') + parser.add_argument('--visualize', action='store_true', help='visualize features') parser.add_argument('--update', action='store_true', help='update all models') - parser.add_argument('--project', default='runs/detect', help='save results to project/name') + parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name') parser.add_argument('--name', default='exp', help='save results to project/name') parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)') parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels') parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences') + parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') + parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') opt = parser.parse_args() - print(opt) - check_requirements(exclude=('tensorboard', 'pycocotools', 'thop')) + opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand + print_args(FILE.stem, opt) + return opt - if opt.update: # update all models (to fix SourceChangeWarning) - for opt.weights in ['yolov3.pt', 'yolov3-spp.pt', 'yolov3-tiny.pt']: - detect(opt=opt) - strip_optimizer(opt.weights) - else: - detect(opt=opt) + +def main(opt): + check_requirements(exclude=('tensorboard', 'thop')) + run(**vars(opt)) + + +if __name__ == "__main__": + opt = parse_opt() + main(opt) diff --git a/export.py b/export.py new file mode 100644 index 0000000000..ce23cf5be1 --- /dev/null +++ b/export.py @@ -0,0 +1,369 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Export a PyTorch model to TorchScript, ONNX, CoreML, TensorFlow (saved_model, pb, TFLite, TF.js,) formats +TensorFlow exports authored by https://github.com/zldrobit + +Usage: + $ python path/to/export.py --weights yolov3.pt --include torchscript onnx coreml saved_model pb tflite tfjs + +Inference: + $ python path/to/detect.py --weights yolov3.pt + yolov3.onnx (must export with --dynamic) + yolov3_saved_model + yolov3.pb + yolov3.tflite + +TensorFlow.js: + $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example + $ npm install + $ ln -s ../../yolov5/yolov3_web_model public/yolov3_web_model + $ npm start +""" + +import argparse +import json +import os +import subprocess +import sys +import time +from pathlib import Path + +import torch +import torch.nn as nn +from torch.utils.mobile_optimizer import optimize_for_mobile + +FILE = Path(__file__).resolve() +ROOT = FILE.parents[0] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH +ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative + +from models.common import Conv +from models.experimental import attempt_load +from models.yolo import Detect +from utils.activations import SiLU +from utils.datasets import LoadImages +from utils.general import (LOGGER, check_dataset, check_img_size, check_requirements, colorstr, file_size, print_args, + url2file) +from utils.torch_utils import select_device + + +def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:')): + # TorchScript model export + try: + LOGGER.info(f'\n{prefix} starting export with torch {torch.__version__}...') + f = file.with_suffix('.torchscript.pt') + + ts = torch.jit.trace(model, im, strict=False) + d = {"shape": im.shape, "stride": int(max(model.stride)), "names": model.names} + extra_files = {'config.txt': json.dumps(d)} # torch._C.ExtraFilesMap() + (optimize_for_mobile(ts) if optimize else ts).save(f, _extra_files=extra_files) + + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + except Exception as e: + LOGGER.info(f'{prefix} export failure: {e}') + + +def export_onnx(model, im, file, opset, train, dynamic, simplify, prefix=colorstr('ONNX:')): + # ONNX export + try: + check_requirements(('onnx',)) + import onnx + + LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__}...') + f = file.with_suffix('.onnx') + + torch.onnx.export(model, im, f, verbose=False, opset_version=opset, + training=torch.onnx.TrainingMode.TRAINING if train else torch.onnx.TrainingMode.EVAL, + do_constant_folding=not train, + input_names=['images'], + output_names=['output'], + dynamic_axes={'images': {0: 'batch', 2: 'height', 3: 'width'}, # shape(1,3,640,640) + 'output': {0: 'batch', 1: 'anchors'} # shape(1,25200,85) + } if dynamic else None) + + # Checks + model_onnx = onnx.load(f) # load onnx model + onnx.checker.check_model(model_onnx) # check onnx model + # LOGGER.info(onnx.helper.printable_graph(model_onnx.graph)) # print + + # Simplify + if simplify: + try: + check_requirements(('onnx-simplifier',)) + import onnxsim + + LOGGER.info(f'{prefix} simplifying with onnx-simplifier {onnxsim.__version__}...') + model_onnx, check = onnxsim.simplify( + model_onnx, + dynamic_input_shape=dynamic, + input_shapes={'images': list(im.shape)} if dynamic else None) + assert check, 'assert check failed' + onnx.save(model_onnx, f) + except Exception as e: + LOGGER.info(f'{prefix} simplifier failure: {e}') + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + LOGGER.info(f"{prefix} run --dynamic ONNX model inference with: 'python detect.py --weights {f}'") + except Exception as e: + LOGGER.info(f'{prefix} export failure: {e}') + + +def export_coreml(model, im, file, prefix=colorstr('CoreML:')): + # CoreML export + ct_model = None + try: + check_requirements(('coremltools',)) + import coremltools as ct + + LOGGER.info(f'\n{prefix} starting export with coremltools {ct.__version__}...') + f = file.with_suffix('.mlmodel') + + model.train() # CoreML exports should be placed in model.train() mode + ts = torch.jit.trace(model, im, strict=False) # TorchScript model + ct_model = ct.convert(ts, inputs=[ct.ImageType('image', shape=im.shape, scale=1 / 255, bias=[0, 0, 0])]) + ct_model.save(f) + + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + except Exception as e: + LOGGER.info(f'\n{prefix} export failure: {e}') + + return ct_model + + +def export_saved_model(model, im, file, dynamic, + tf_nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45, + conf_thres=0.25, prefix=colorstr('TensorFlow saved_model:')): + # TensorFlow saved_model export + keras_model = None + try: + import tensorflow as tf + from tensorflow import keras + + from models.tf import TFDetect, TFModel + + LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...') + f = str(file).replace('.pt', '_saved_model') + batch_size, ch, *imgsz = list(im.shape) # BCHW + + tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz) + im = tf.zeros((batch_size, *imgsz, 3)) # BHWC order for TensorFlow + y = tf_model.predict(im, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres) + inputs = keras.Input(shape=(*imgsz, 3), batch_size=None if dynamic else batch_size) + outputs = tf_model.predict(inputs, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres) + keras_model = keras.Model(inputs=inputs, outputs=outputs) + keras_model.trainable = False + keras_model.summary() + keras_model.save(f, save_format='tf') + + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + except Exception as e: + LOGGER.info(f'\n{prefix} export failure: {e}') + + return keras_model + + +def export_pb(keras_model, im, file, prefix=colorstr('TensorFlow GraphDef:')): + # TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow + try: + import tensorflow as tf + from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 + + LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...') + f = file.with_suffix('.pb') + + m = tf.function(lambda x: keras_model(x)) # full model + m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype)) + frozen_func = convert_variables_to_constants_v2(m) + frozen_func.graph.as_graph_def() + tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False) + + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + except Exception as e: + LOGGER.info(f'\n{prefix} export failure: {e}') + + +def export_tflite(keras_model, im, file, int8, data, ncalib, prefix=colorstr('TensorFlow Lite:')): + # TensorFlow Lite export + try: + import tensorflow as tf + + from models.tf import representative_dataset_gen + + LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...') + batch_size, ch, *imgsz = list(im.shape) # BCHW + f = str(file).replace('.pt', '-fp16.tflite') + + converter = tf.lite.TFLiteConverter.from_keras_model(keras_model) + converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS] + converter.target_spec.supported_types = [tf.float16] + converter.optimizations = [tf.lite.Optimize.DEFAULT] + if int8: + dataset = LoadImages(check_dataset(data)['train'], img_size=imgsz, auto=False) # representative data + converter.representative_dataset = lambda: representative_dataset_gen(dataset, ncalib) + converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] + converter.target_spec.supported_types = [] + converter.inference_input_type = tf.uint8 # or tf.int8 + converter.inference_output_type = tf.uint8 # or tf.int8 + converter.experimental_new_quantizer = False + f = str(file).replace('.pt', '-int8.tflite') + + tflite_model = converter.convert() + open(f, "wb").write(tflite_model) + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + + except Exception as e: + LOGGER.info(f'\n{prefix} export failure: {e}') + + +def export_tfjs(keras_model, im, file, prefix=colorstr('TensorFlow.js:')): + # TensorFlow.js export + try: + check_requirements(('tensorflowjs',)) + import re + + import tensorflowjs as tfjs + + LOGGER.info(f'\n{prefix} starting export with tensorflowjs {tfjs.__version__}...') + f = str(file).replace('.pt', '_web_model') # js dir + f_pb = file.with_suffix('.pb') # *.pb path + f_json = f + '/model.json' # *.json path + + cmd = f"tensorflowjs_converter --input_format=tf_frozen_model " \ + f"--output_node_names='Identity,Identity_1,Identity_2,Identity_3' {f_pb} {f}" + subprocess.run(cmd, shell=True) + + json = open(f_json).read() + with open(f_json, 'w') as j: # sort JSON Identity_* in ascending order + subst = re.sub( + r'{"outputs": {"Identity.?.?": {"name": "Identity.?.?"}, ' + r'"Identity.?.?": {"name": "Identity.?.?"}, ' + r'"Identity.?.?": {"name": "Identity.?.?"}, ' + r'"Identity.?.?": {"name": "Identity.?.?"}}}', + r'{"outputs": {"Identity": {"name": "Identity"}, ' + r'"Identity_1": {"name": "Identity_1"}, ' + r'"Identity_2": {"name": "Identity_2"}, ' + r'"Identity_3": {"name": "Identity_3"}}}', + json) + j.write(subst) + + LOGGER.info(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') + except Exception as e: + LOGGER.info(f'\n{prefix} export failure: {e}') + + +@torch.no_grad() +def run(data=ROOT / 'data/coco128.yaml', # 'dataset.yaml path' + weights=ROOT / 'yolov3.pt', # weights path + imgsz=(640, 640), # image (height, width) + batch_size=1, # batch size + device='cpu', # cuda device, i.e. 0 or 0,1,2,3 or cpu + include=('torchscript', 'onnx', 'coreml'), # include formats + half=False, # FP16 half-precision export + inplace=False, # set Detect() inplace=True + train=False, # model.train() mode + optimize=False, # TorchScript: optimize for mobile + int8=False, # CoreML/TF INT8 quantization + dynamic=False, # ONNX/TF: dynamic axes + simplify=False, # ONNX: simplify model + opset=12, # ONNX: opset version + topk_per_class=100, # TF.js NMS: topk per class to keep + topk_all=100, # TF.js NMS: topk for all classes to keep + iou_thres=0.45, # TF.js NMS: IoU threshold + conf_thres=0.25 # TF.js NMS: confidence threshold + ): + t = time.time() + include = [x.lower() for x in include] + tf_exports = list(x in include for x in ('saved_model', 'pb', 'tflite', 'tfjs')) # TensorFlow exports + imgsz *= 2 if len(imgsz) == 1 else 1 # expand + file = Path(url2file(weights) if str(weights).startswith(('http:/', 'https:/')) else weights) + + # Load PyTorch model + device = select_device(device) + assert not (device.type == 'cpu' and half), '--half only compatible with GPU export, i.e. use --device 0' + model = attempt_load(weights, map_location=device, inplace=True, fuse=True) # load FP32 model + nc, names = model.nc, model.names # number of classes, class names + + # Input + gs = int(max(model.stride)) # grid size (max stride) + imgsz = [check_img_size(x, gs) for x in imgsz] # verify img_size are gs-multiples + im = torch.zeros(batch_size, 3, *imgsz).to(device) # image size(1,3,320,192) BCHW iDetection + + # Update model + if half: + im, model = im.half(), model.half() # to FP16 + model.train() if train else model.eval() # training mode = no Detect() layer grid construction + for k, m in model.named_modules(): + if isinstance(m, Conv): # assign export-friendly activations + if isinstance(m.act, nn.SiLU): + m.act = SiLU() + elif isinstance(m, Detect): + m.inplace = inplace + m.onnx_dynamic = dynamic + # m.forward = m.forward_export # assign forward (optional) + + for _ in range(2): + y = model(im) # dry runs + LOGGER.info(f"\n{colorstr('PyTorch:')} starting from {file} ({file_size(file):.1f} MB)") + + # Exports + if 'torchscript' in include: + export_torchscript(model, im, file, optimize) + if 'onnx' in include: + export_onnx(model, im, file, opset, train, dynamic, simplify) + if 'coreml' in include: + export_coreml(model, im, file) + + # TensorFlow Exports + if any(tf_exports): + pb, tflite, tfjs = tf_exports[1:] + assert not (tflite and tfjs), 'TFLite and TF.js models must be exported separately, please pass only one type.' + model = export_saved_model(model, im, file, dynamic, tf_nms=tfjs, agnostic_nms=tfjs, + topk_per_class=topk_per_class, topk_all=topk_all, conf_thres=conf_thres, + iou_thres=iou_thres) # keras model + if pb or tfjs: # pb prerequisite to tfjs + export_pb(model, im, file) + if tflite: + export_tflite(model, im, file, int8=int8, data=data, ncalib=100) + if tfjs: + export_tfjs(model, im, file) + + # Finish + LOGGER.info(f'\nExport complete ({time.time() - t:.2f}s)' + f"\nResults saved to {colorstr('bold', file.parent.resolve())}" + f'\nVisualize with https://netron.app') + + +def parse_opt(): + parser = argparse.ArgumentParser() + parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') + parser.add_argument('--weights', type=str, default=ROOT / 'yolov3.pt', help='weights path') + parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640, 640], help='image (h, w)') + parser.add_argument('--batch-size', type=int, default=1, help='batch size') + parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') + parser.add_argument('--half', action='store_true', help='FP16 half-precision export') + parser.add_argument('--inplace', action='store_true', help='set YOLOv3 Detect() inplace=True') + parser.add_argument('--train', action='store_true', help='model.train() mode') + parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile') + parser.add_argument('--int8', action='store_true', help='CoreML/TF INT8 quantization') + parser.add_argument('--dynamic', action='store_true', help='ONNX/TF: dynamic axes') + parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model') + parser.add_argument('--opset', type=int, default=13, help='ONNX: opset version') + parser.add_argument('--topk-per-class', type=int, default=100, help='TF.js NMS: topk per class to keep') + parser.add_argument('--topk-all', type=int, default=100, help='TF.js NMS: topk for all classes to keep') + parser.add_argument('--iou-thres', type=float, default=0.45, help='TF.js NMS: IoU threshold') + parser.add_argument('--conf-thres', type=float, default=0.25, help='TF.js NMS: confidence threshold') + parser.add_argument('--include', nargs='+', + default=['torchscript', 'onnx'], + help='available formats are (torchscript, onnx, coreml, saved_model, pb, tflite, tfjs)') + opt = parser.parse_args() + print_args(FILE.stem, opt) + return opt + + +def main(opt): + run(**vars(opt)) + + +if __name__ == "__main__": + opt = parse_opt() + main(opt) diff --git a/hubconf.py b/hubconf.py index 683e22f748..d610aa3672 100644 --- a/hubconf.py +++ b/hubconf.py @@ -1,56 +1,61 @@ -"""YOLOv3 PyTorch Hub models https://pytorch.org/hub/ultralytics_yolov3/ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +PyTorch Hub models https://pytorch.org/hub/ultralytics_yolov5/ Usage: import torch - model = torch.hub.load('ultralytics/yolov3', 'yolov3_tiny') + model = torch.hub.load('ultralytics/yolov3', 'yolov3') """ import torch def _create(name, pretrained=True, channels=3, classes=80, autoshape=True, verbose=True, device=None): - """Creates a specified YOLOv3 model + """Creates a specified model Arguments: name (str): name of model, i.e. 'yolov3' pretrained (bool): load pretrained weights into the model channels (int): number of input channels classes (int): number of model classes - autoshape (bool): apply YOLOv3 .autoshape() wrapper to model + autoshape (bool): apply .autoshape() wrapper to model verbose (bool): print all information to screen device (str, torch.device, None): device to use for model parameters Returns: - YOLOv3 pytorch model + pytorch model """ from pathlib import Path - from models.yolo import Model, attempt_load - from utils.general import check_requirements, set_logging - from utils.google_utils import attempt_download + from models.experimental import attempt_load + from models.yolo import Model + from utils.downloads import attempt_download + from utils.general import check_requirements, intersect_dicts, set_logging from utils.torch_utils import select_device - check_requirements(Path(__file__).parent / 'requirements.txt', exclude=('tensorboard', 'pycocotools', 'thop')) + file = Path(__file__).resolve() + check_requirements(exclude=('tensorboard', 'thop', 'opencv-python')) set_logging(verbose=verbose) - fname = Path(name).with_suffix('.pt') # checkpoint filename + save_dir = Path('') if str(name).endswith('.pt') else file.parent + path = (save_dir / name).with_suffix('.pt') # checkpoint path try: + device = select_device(('0' if torch.cuda.is_available() else 'cpu') if device is None else device) + if pretrained and channels == 3 and classes == 80: - model = attempt_load(fname, map_location=torch.device('cpu')) # download/load FP32 model + model = attempt_load(path, map_location=device) # download/load FP32 model else: cfg = list((Path(__file__).parent / 'models').rglob(f'{name}.yaml'))[0] # model.yaml path model = Model(cfg, channels, classes) # create model if pretrained: - ckpt = torch.load(attempt_download(fname), map_location=torch.device('cpu')) # load - msd = model.state_dict() # model state_dict + ckpt = torch.load(attempt_download(path), map_location=device) # load csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 - csd = {k: v for k, v in csd.items() if msd[k].shape == v.shape} # filter + csd = intersect_dicts(csd, model.state_dict(), exclude=['anchors']) # intersect model.load_state_dict(csd, strict=False) # load if len(ckpt['model'].names) == classes: model.names = ckpt['model'].names # set class names attribute if autoshape: model = model.autoshape() # for file/URI/PIL/cv2/np inputs and NMS - device = select_device('0' if torch.cuda.is_available() else 'cpu') if device is None else torch.device(device) return model.to(device) except Exception as e: @@ -60,7 +65,7 @@ def _create(name, pretrained=True, channels=3, classes=80, autoshape=True, verbo def custom(path='path/to/model.pt', autoshape=True, verbose=True, device=None): - # YOLOv3 custom or local model + # custom or local model return _create(path, autoshape=autoshape, verbose=verbose, device=device) @@ -68,26 +73,31 @@ def yolov3(pretrained=True, channels=3, classes=80, autoshape=True, verbose=True # YOLOv3 model https://github.com/ultralytics/yolov3 return _create('yolov3', pretrained, channels, classes, autoshape, verbose, device) + def yolov3_spp(pretrained=True, channels=3, classes=80, autoshape=True, verbose=True, device=None): # YOLOv3-SPP model https://github.com/ultralytics/yolov3 return _create('yolov3-spp', pretrained, channels, classes, autoshape, verbose, device) + def yolov3_tiny(pretrained=True, channels=3, classes=80, autoshape=True, verbose=True, device=None): # YOLOv3-tiny model https://github.com/ultralytics/yolov3 return _create('yolov3-tiny', pretrained, channels, classes, autoshape, verbose, device) if __name__ == '__main__': - model = _create(name='yolov3', pretrained=True, channels=3, classes=80, autoshape=True, verbose=True) # pretrained + model = _create(name='yolov3-tiny', pretrained=True, channels=3, classes=80, autoshape=True, verbose=True) # pretrained # model = custom(path='path/to/model.pt') # custom # Verify inference + from pathlib import Path + import cv2 import numpy as np from PIL import Image imgs = ['data/images/zidane.jpg', # filename - 'https://github.com/ultralytics/yolov5/releases/download/v1.0/zidane.jpg', # URI + Path('data/images/zidane.jpg'), # Path + 'https://ultralytics.com/images/zidane.jpg', # URI cv2.imread('data/images/bus.jpg')[:, :, ::-1], # OpenCV Image.open('data/images/bus.jpg'), # PIL np.zeros((320, 640, 3))] # numpy diff --git a/models/common.py b/models/common.py index 0b4e600459..82b348aebb 100644 --- a/models/common.py +++ b/models/common.py @@ -1,9 +1,16 @@ -# YOLOv3 common modules +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Common modules +""" +import json import math +import platform +import warnings from copy import copy from pathlib import Path +import cv2 import numpy as np import pandas as pd import requests @@ -12,10 +19,11 @@ from PIL import Image from torch.cuda import amp -from utils.datasets import letterbox -from utils.general import non_max_suppression, make_divisible, scale_coords, increment_path, xyxy2xywh, save_one_box -from utils.plots import colors, plot_one_box -from utils.torch_utils import time_synchronized +from utils.datasets import exif_transpose, letterbox +from utils.general import (LOGGER, check_requirements, check_suffix, colorstr, increment_path, make_divisible, + non_max_suppression, scale_coords, xywh2xyxy, xyxy2xywh) +from utils.plots import Annotator, colors, save_one_box +from utils.torch_utils import time_sync def autopad(k, p=None): # kernel, padding @@ -25,26 +33,27 @@ def autopad(k, p=None): # kernel, padding return p -def DWConv(c1, c2, k=1, s=1, act=True): - # Depthwise convolution - return Conv(c1, c2, k, s, g=math.gcd(c1, c2), act=act) - - class Conv(nn.Module): # Standard convolution def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups - super(Conv, self).__init__() + super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False) self.bn = nn.BatchNorm2d(c2) - self.act = nn.LeakyReLU(0.1) if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) + self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) def forward(self, x): return self.act(self.bn(self.conv(x))) - def fuseforward(self, x): + def forward_fuse(self, x): return self.act(self.conv(x)) +class DWConv(Conv): + # Depth-wise convolution class + def __init__(self, c1, c2, k=1, s=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups + super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), act=act) + + class TransformerLayer(nn.Module): # Transformer layer https://arxiv.org/abs/2010.11929 (LayerNorm layers removed for better performance) def __init__(self, c, num_heads): @@ -70,31 +79,21 @@ def __init__(self, c1, c2, num_heads, num_layers): if c1 != c2: self.conv = Conv(c1, c2) self.linear = nn.Linear(c2, c2) # learnable position embedding - self.tr = nn.Sequential(*[TransformerLayer(c2, num_heads) for _ in range(num_layers)]) + self.tr = nn.Sequential(*(TransformerLayer(c2, num_heads) for _ in range(num_layers))) self.c2 = c2 def forward(self, x): if self.conv is not None: x = self.conv(x) b, _, w, h = x.shape - p = x.flatten(2) - p = p.unsqueeze(0) - p = p.transpose(0, 3) - p = p.squeeze(3) - e = self.linear(p) - x = p + e - - x = self.tr(x) - x = x.unsqueeze(3) - x = x.transpose(0, 3) - x = x.reshape(b, self.c2, w, h) - return x + p = x.flatten(2).unsqueeze(0).transpose(0, 3).squeeze(3) + return self.tr(p + self.linear(p)).unsqueeze(3).transpose(0, 3).reshape(b, self.c2, w, h) class Bottleneck(nn.Module): # Standard bottleneck def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion - super(Bottleneck, self).__init__() + super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_, c2, 3, 1, g=g) @@ -107,15 +106,15 @@ def forward(self, x): class BottleneckCSP(nn.Module): # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion - super(BottleneckCSP, self).__init__() + super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False) self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False) self.cv4 = Conv(2 * c_, c2, 1, 1) self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3) - self.act = nn.LeakyReLU(0.1, inplace=True) - self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) + self.act = nn.SiLU() + self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n))) def forward(self, x): y1 = self.cv3(self.m(self.cv1(x))) @@ -126,12 +125,12 @@ def forward(self, x): class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion - super(C3, self).__init__() + super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2) - self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) + self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n))) # self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)]) def forward(self, x): @@ -146,10 +145,26 @@ def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): self.m = TransformerBlock(c_, c_, 4, n) +class C3SPP(C3): + # C3 module with SPP() + def __init__(self, c1, c2, k=(5, 9, 13), n=1, shortcut=True, g=1, e=0.5): + super().__init__(c1, c2, n, shortcut, g, e) + c_ = int(c2 * e) + self.m = SPP(c_, c_, k) + + +class C3Ghost(C3): + # C3 module with GhostBottleneck() + def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): + super().__init__(c1, c2, n, shortcut, g, e) + c_ = int(c2 * e) # hidden channels + self.m = nn.Sequential(*(GhostBottleneck(c_, c_) for _ in range(n))) + + class SPP(nn.Module): - # Spatial pyramid pooling layer used in YOLOv3-SPP + # Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729 def __init__(self, c1, c2, k=(5, 9, 13)): - super(SPP, self).__init__() + super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) @@ -157,13 +172,33 @@ def __init__(self, c1, c2, k=(5, 9, 13)): def forward(self, x): x = self.cv1(x) - return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning + return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) + + +class SPPF(nn.Module): + # Spatial Pyramid Pooling - Fast (SPPF) layer for by Glenn Jocher + def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) + super().__init__() + c_ = c1 // 2 # hidden channels + self.cv1 = Conv(c1, c_, 1, 1) + self.cv2 = Conv(c_ * 4, c2, 1, 1) + self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) + + def forward(self, x): + x = self.cv1(x) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning + y1 = self.m(x) + y2 = self.m(y1) + return self.cv2(torch.cat([x, y1, y2, self.m(y2)], 1)) class Focus(nn.Module): # Focus wh information into c-space def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups - super(Focus, self).__init__() + super().__init__() self.conv = Conv(c1 * 4, c2, k, s, p, g, act) # self.contract = Contract(gain=2) @@ -172,6 +207,34 @@ def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2) # return self.conv(self.contract(x)) +class GhostConv(nn.Module): + # Ghost Convolution https://github.com/huawei-noah/ghostnet + def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups + super().__init__() + c_ = c2 // 2 # hidden channels + self.cv1 = Conv(c1, c_, k, s, None, g, act) + self.cv2 = Conv(c_, c_, 5, 1, None, c_, act) + + def forward(self, x): + y = self.cv1(x) + return torch.cat([y, self.cv2(y)], 1) + + +class GhostBottleneck(nn.Module): + # Ghost Bottleneck https://github.com/huawei-noah/ghostnet + def __init__(self, c1, c2, k=3, s=1): # ch_in, ch_out, kernel, stride + super().__init__() + c_ = c2 // 2 + self.conv = nn.Sequential(GhostConv(c1, c_, 1, 1), # pw + DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(), # dw + GhostConv(c_, c2, 1, 1, act=False)) # pw-linear + self.shortcut = nn.Sequential(DWConv(c1, c1, k, s, act=False), + Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity() + + def forward(self, x): + return self.conv(x) + self.shortcut(x) + + class Contract(nn.Module): # Contract width-height into channels, i.e. x(1,64,80,80) to x(1,256,40,40) def __init__(self, gain=2): @@ -179,11 +242,11 @@ def __init__(self, gain=2): self.gain = gain def forward(self, x): - N, C, H, W = x.size() # assert (H / s == 0) and (W / s == 0), 'Indivisible gain' + b, c, h, w = x.size() # assert (h / s == 0) and (W / s == 0), 'Indivisible gain' s = self.gain - x = x.view(N, C, H // s, s, W // s, s) # x(1,64,40,2,40,2) + x = x.view(b, c, h // s, s, w // s, s) # x(1,64,40,2,40,2) x = x.permute(0, 3, 5, 1, 2, 4).contiguous() # x(1,2,2,64,40,40) - return x.view(N, C * s * s, H // s, W // s) # x(1,256,40,40) + return x.view(b, c * s * s, h // s, w // s) # x(1,256,40,40) class Expand(nn.Module): @@ -193,64 +256,183 @@ def __init__(self, gain=2): self.gain = gain def forward(self, x): - N, C, H, W = x.size() # assert C / s ** 2 == 0, 'Indivisible gain' + b, c, h, w = x.size() # assert C / s ** 2 == 0, 'Indivisible gain' s = self.gain - x = x.view(N, s, s, C // s ** 2, H, W) # x(1,2,2,16,80,80) + x = x.view(b, s, s, c // s ** 2, h, w) # x(1,2,2,16,80,80) x = x.permute(0, 3, 4, 1, 5, 2).contiguous() # x(1,16,80,2,80,2) - return x.view(N, C // s ** 2, H * s, W * s) # x(1,16,160,160) + return x.view(b, c // s ** 2, h * s, w * s) # x(1,16,160,160) class Concat(nn.Module): # Concatenate a list of tensors along dimension def __init__(self, dimension=1): - super(Concat, self).__init__() + super().__init__() self.d = dimension def forward(self, x): return torch.cat(x, self.d) -class NMS(nn.Module): - # Non-Maximum Suppression (NMS) module - conf = 0.25 # confidence threshold - iou = 0.45 # IoU threshold - classes = None # (optional list) filter by class - max_det = 1000 # maximum number of detections per image - - def __init__(self): - super(NMS, self).__init__() - - def forward(self, x): - return non_max_suppression(x[0], self.conf, iou_thres=self.iou, classes=self.classes, max_det=self.max_det) +class DetectMultiBackend(nn.Module): + # MultiBackend class for python inference on various backends + def __init__(self, weights='yolov3.pt', device=None, dnn=True): + # Usage: + # PyTorch: weights = *.pt + # TorchScript: *.torchscript.pt + # CoreML: *.mlmodel + # TensorFlow: *_saved_model + # TensorFlow: *.pb + # TensorFlow Lite: *.tflite + # ONNX Runtime: *.onnx + # OpenCV DNN: *.onnx with dnn=True + super().__init__() + w = str(weights[0] if isinstance(weights, list) else weights) + suffix, suffixes = Path(w).suffix.lower(), ['.pt', '.onnx', '.tflite', '.pb', '', '.mlmodel'] + check_suffix(w, suffixes) # check weights have acceptable suffix + pt, onnx, tflite, pb, saved_model, coreml = (suffix == x for x in suffixes) # backend booleans + jit = pt and 'torchscript' in w.lower() + stride, names = 64, [f'class{i}' for i in range(1000)] # assign defaults + + if jit: # TorchScript + LOGGER.info(f'Loading {w} for TorchScript inference...') + extra_files = {'config.txt': ''} # model metadata + model = torch.jit.load(w, _extra_files=extra_files) + if extra_files['config.txt']: + d = json.loads(extra_files['config.txt']) # extra_files dict + stride, names = int(d['stride']), d['names'] + elif pt: # PyTorch + from models.experimental import attempt_load # scoped to avoid circular import + model = torch.jit.load(w) if 'torchscript' in w else attempt_load(weights, map_location=device) + stride = int(model.stride.max()) # model stride + names = model.module.names if hasattr(model, 'module') else model.names # get class names + elif coreml: # CoreML *.mlmodel + import coremltools as ct + model = ct.models.MLModel(w) + elif dnn: # ONNX OpenCV DNN + LOGGER.info(f'Loading {w} for ONNX OpenCV DNN inference...') + check_requirements(('opencv-python>=4.5.4',)) + net = cv2.dnn.readNetFromONNX(w) + elif onnx: # ONNX Runtime + LOGGER.info(f'Loading {w} for ONNX Runtime inference...') + check_requirements(('onnx', 'onnxruntime-gpu' if torch.has_cuda else 'onnxruntime')) + import onnxruntime + session = onnxruntime.InferenceSession(w, None) + else: # TensorFlow model (TFLite, pb, saved_model) + import tensorflow as tf + if pb: # https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt + def wrap_frozen_graph(gd, inputs, outputs): + x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), []) # wrapped + return x.prune(tf.nest.map_structure(x.graph.as_graph_element, inputs), + tf.nest.map_structure(x.graph.as_graph_element, outputs)) + + LOGGER.info(f'Loading {w} for TensorFlow *.pb inference...') + graph_def = tf.Graph().as_graph_def() + graph_def.ParseFromString(open(w, 'rb').read()) + frozen_func = wrap_frozen_graph(gd=graph_def, inputs="x:0", outputs="Identity:0") + elif saved_model: + LOGGER.info(f'Loading {w} for TensorFlow saved_model inference...') + model = tf.keras.models.load_model(w) + elif tflite: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python + if 'edgetpu' in w.lower(): + LOGGER.info(f'Loading {w} for TensorFlow Edge TPU inference...') + import tflite_runtime.interpreter as tfli + delegate = {'Linux': 'libedgetpu.so.1', # install https://coral.ai/software/#edgetpu-runtime + 'Darwin': 'libedgetpu.1.dylib', + 'Windows': 'edgetpu.dll'}[platform.system()] + interpreter = tfli.Interpreter(model_path=w, experimental_delegates=[tfli.load_delegate(delegate)]) + else: + LOGGER.info(f'Loading {w} for TensorFlow Lite inference...') + interpreter = tf.lite.Interpreter(model_path=w) # load TFLite model + interpreter.allocate_tensors() # allocate + input_details = interpreter.get_input_details() # inputs + output_details = interpreter.get_output_details() # outputs + self.__dict__.update(locals()) # assign all variables to self + + def forward(self, im, augment=False, visualize=False, val=False): + # MultiBackend inference + b, ch, h, w = im.shape # batch, channel, height, width + if self.pt: # PyTorch + y = self.model(im) if self.jit else self.model(im, augment=augment, visualize=visualize) + return y if val else y[0] + elif self.coreml: # CoreML *.mlmodel + im = im.permute(0, 2, 3, 1).cpu().numpy() # torch BCHW to numpy BHWC shape(1,320,192,3) + im = Image.fromarray((im[0] * 255).astype('uint8')) + # im = im.resize((192, 320), Image.ANTIALIAS) + y = self.model.predict({'image': im}) # coordinates are xywh normalized + box = xywh2xyxy(y['coordinates'] * [[w, h, w, h]]) # xyxy pixels + conf, cls = y['confidence'].max(1), y['confidence'].argmax(1).astype(np.float) + y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1) + elif self.onnx: # ONNX + im = im.cpu().numpy() # torch to numpy + if self.dnn: # ONNX OpenCV DNN + self.net.setInput(im) + y = self.net.forward() + else: # ONNX Runtime + y = self.session.run([self.session.get_outputs()[0].name], {self.session.get_inputs()[0].name: im})[0] + else: # TensorFlow model (TFLite, pb, saved_model) + im = im.permute(0, 2, 3, 1).cpu().numpy() # torch BCHW to numpy BHWC shape(1,320,192,3) + if self.pb: + y = self.frozen_func(x=self.tf.constant(im)).numpy() + elif self.saved_model: + y = self.model(im, training=False).numpy() + elif self.tflite: + input, output = self.input_details[0], self.output_details[0] + int8 = input['dtype'] == np.uint8 # is TFLite quantized uint8 model + if int8: + scale, zero_point = input['quantization'] + im = (im / scale + zero_point).astype(np.uint8) # de-scale + self.interpreter.set_tensor(input['index'], im) + self.interpreter.invoke() + y = self.interpreter.get_tensor(output['index']) + if int8: + scale, zero_point = output['quantization'] + y = (y.astype(np.float32) - zero_point) * scale # re-scale + y[..., 0] *= w # x + y[..., 1] *= h # y + y[..., 2] *= w # w + y[..., 3] *= h # h + y = torch.tensor(y) + return (y, []) if val else y class AutoShape(nn.Module): - # input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS + # input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS conf = 0.25 # NMS confidence threshold iou = 0.45 # NMS IoU threshold - classes = None # (optional list) filter by class + classes = None # (optional list) filter by class, i.e. = [0, 15, 16] for COCO persons, cats and dogs + multi_label = False # NMS multiple labels per box max_det = 1000 # maximum number of detections per image def __init__(self, model): - super(AutoShape, self).__init__() + super().__init__() self.model = model.eval() def autoshape(self): - print('AutoShape already enabled, skipping... ') # model already converted to model.autoshape() + LOGGER.info('AutoShape already enabled, skipping... ') # model already converted to model.autoshape() + return self + + def _apply(self, fn): + # Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers + self = super()._apply(fn) + m = self.model.model[-1] # Detect() + m.stride = fn(m.stride) + m.grid = list(map(fn, m.grid)) + if isinstance(m.anchor_grid, list): + m.anchor_grid = list(map(fn, m.anchor_grid)) return self @torch.no_grad() def forward(self, imgs, size=640, augment=False, profile=False): # Inference from various sources. For height=640, width=1280, RGB images example inputs are: - # filename: imgs = 'data/images/zidane.jpg' - # URI: = 'https://github.com/ultralytics/yolov5/releases/download/v1.0/zidane.jpg' + # file: imgs = 'data/images/zidane.jpg' # str or PosixPath + # URI: = 'https://ultralytics.com/images/zidane.jpg' # OpenCV: = cv2.imread('image.jpg')[:,:,::-1] # HWC BGR to RGB x(640,1280,3) - # PIL: = Image.open('image.jpg') # HWC x(640,1280,3) + # PIL: = Image.open('image.jpg') or ImageGrab.grab() # HWC x(640,1280,3) # numpy: = np.zeros((640,1280,3)) # HWC # torch: = torch.zeros(16,3,320,640) # BCHW (scaled to size=640, 0-1 values) # multiple: = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...] # list of images - t = [time_synchronized()] + t = [time_sync()] p = next(self.model.parameters()) # for device and type if isinstance(imgs, torch.Tensor): # torch with amp.autocast(enabled=p.device.type != 'cpu'): @@ -261,14 +443,15 @@ def forward(self, imgs, size=640, augment=False, profile=False): shape0, shape1, files = [], [], [] # image and inference shapes, filenames for i, im in enumerate(imgs): f = f'image{i}' # filename - if isinstance(im, str): # filename or uri - im, f = np.asarray(Image.open(requests.get(im, stream=True).raw if im.startswith('http') else im)), im + if isinstance(im, (str, Path)): # filename or uri + im, f = Image.open(requests.get(im, stream=True).raw if str(im).startswith('http') else im), im + im = np.asarray(exif_transpose(im)) elif isinstance(im, Image.Image): # PIL Image - im, f = np.asarray(im), getattr(im, 'filename', f) or f + im, f = np.asarray(exif_transpose(im)), getattr(im, 'filename', f) or f files.append(Path(f).with_suffix('.jpg').name) if im.shape[0] < 5: # image in CHW im = im.transpose((1, 2, 0)) # reverse dataloader .transpose(2, 0, 1) - im = im[:, :, :3] if im.ndim == 3 else np.tile(im[:, :, None], 3) # enforce 3ch input + im = im[..., :3] if im.ndim == 3 else np.tile(im[..., None], 3) # enforce 3ch input s = im.shape[:2] # HWC shape0.append(s) # image shape g = (size / max(s)) # gain @@ -278,29 +461,30 @@ def forward(self, imgs, size=640, augment=False, profile=False): x = [letterbox(im, new_shape=shape1, auto=False)[0] for im in imgs] # pad x = np.stack(x, 0) if n > 1 else x[0][None] # stack x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW - x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 - t.append(time_synchronized()) + x = torch.from_numpy(x).to(p.device).type_as(p) / 255 # uint8 to fp16/32 + t.append(time_sync()) with amp.autocast(enabled=p.device.type != 'cpu'): # Inference y = self.model(x, augment, profile)[0] # forward - t.append(time_synchronized()) + t.append(time_sync()) # Post-process - y = non_max_suppression(y, self.conf, iou_thres=self.iou, classes=self.classes, max_det=self.max_det) # NMS + y = non_max_suppression(y, self.conf, iou_thres=self.iou, classes=self.classes, + multi_label=self.multi_label, max_det=self.max_det) # NMS for i in range(n): scale_coords(shape1, y[i][:, :4], shape0[i]) - t.append(time_synchronized()) + t.append(time_sync()) return Detections(imgs, y, files, t, self.names, x.shape) class Detections: - # detections class for YOLOv3 inference results + # detections class for inference results def __init__(self, imgs, pred, files, times=None, names=None, shape=None): - super(Detections, self).__init__() + super().__init__() d = pred[0].device # device - gn = [torch.tensor([*[im.shape[i] for i in [1, 0, 1, 0]], 1., 1.], device=d) for im in imgs] # normalizations + gn = [torch.tensor([*(im.shape[i] for i in [1, 0, 1, 0]), 1, 1], device=d) for im in imgs] # normalizations self.imgs = imgs # list of images as numpy arrays self.pred = pred # list of tensors pred[0] = (xyxy, conf, cls) self.names = names # class names @@ -314,47 +498,59 @@ def __init__(self, imgs, pred, files, times=None, names=None, shape=None): self.s = shape # inference BCHW shape def display(self, pprint=False, show=False, save=False, crop=False, render=False, save_dir=Path('')): + crops = [] for i, (im, pred) in enumerate(zip(self.imgs, self.pred)): - str = f'image {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} ' - if pred is not None: + s = f'image {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} ' # string + if pred.shape[0]: for c in pred[:, -1].unique(): n = (pred[:, -1] == c).sum() # detections per class - str += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, " # add to string + s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, " # add to string if show or save or render or crop: - for *box, conf, cls in pred: # xyxy, confidence, class + annotator = Annotator(im, example=str(self.names)) + for *box, conf, cls in reversed(pred): # xyxy, confidence, class label = f'{self.names[int(cls)]} {conf:.2f}' if crop: - save_one_box(box, im, file=save_dir / 'crops' / self.names[int(cls)] / self.files[i]) + file = save_dir / 'crops' / self.names[int(cls)] / self.files[i] if save else None + crops.append({'box': box, 'conf': conf, 'cls': cls, 'label': label, + 'im': save_one_box(box, im, file=file, save=save)}) else: # all others - plot_one_box(box, im, label=label, color=colors(cls)) + annotator.box_label(box, label, color=colors(cls)) + im = annotator.im + else: + s += '(no detections)' im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im # from np if pprint: - print(str.rstrip(', ')) + LOGGER.info(s.rstrip(', ')) if show: im.show(self.files[i]) # show if save: f = self.files[i] im.save(save_dir / f) # save - print(f"{'Saved' * (i == 0)} {f}", end=',' if i < self.n - 1 else f' to {save_dir}\n') + if i == self.n - 1: + LOGGER.info(f"Saved {self.n} image{'s' * (self.n > 1)} to {colorstr('bold', save_dir)}") if render: self.imgs[i] = np.asarray(im) + if crop: + if save: + LOGGER.info(f'Saved results to {save_dir}\n') + return crops def print(self): self.display(pprint=True) # print results - print(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {tuple(self.s)}' % self.t) + LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {tuple(self.s)}' % + self.t) def show(self): self.display(show=True) # show results - def save(self, save_dir='runs/hub/exp'): - save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/hub/exp', mkdir=True) # increment save_dir + def save(self, save_dir='runs/detect/exp'): + save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/detect/exp', mkdir=True) # increment save_dir self.display(save=True, save_dir=save_dir) # save results - def crop(self, save_dir='runs/hub/exp'): - save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/hub/exp', mkdir=True) # increment save_dir - self.display(crop=True, save_dir=save_dir) # crop results - print(f'Saved results to {save_dir}\n') + def crop(self, save=True, save_dir='runs/detect/exp'): + save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/detect/exp', mkdir=True) if save else None + return self.display(crop=True, save=save, save_dir=save_dir) # crop results def render(self): self.display(render=True) # render results @@ -385,7 +581,7 @@ def __len__(self): class Classify(nn.Module): # Classification head, i.e. x(b,c1,20,20) to x(b,c2) def __init__(self, c1, c2, k=1, s=1, p=None, g=1): # ch_in, ch_out, kernel, stride, padding, groups - super(Classify, self).__init__() + super().__init__() self.aap = nn.AdaptiveAvgPool2d(1) # to x(b,c1,1,1) self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g) # to x(b,c2,1,1) self.flat = nn.Flatten() diff --git a/models/experimental.py b/models/experimental.py index 867c8db5cd..81fc9bb222 100644 --- a/models/experimental.py +++ b/models/experimental.py @@ -1,18 +1,22 @@ -# YOLOv3 experimental modules +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Experimental modules +""" +import math import numpy as np import torch import torch.nn as nn -from models.common import Conv, DWConv -from utils.google_utils import attempt_download +from models.common import Conv +from utils.downloads import attempt_download class CrossConv(nn.Module): # Cross Convolution Downsample def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False): # ch_in, ch_out, kernel, stride, groups, expansion, shortcut - super(CrossConv, self).__init__() + super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, (1, k), (1, s)) self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g) @@ -25,11 +29,11 @@ def forward(self, x): class Sum(nn.Module): # Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070 def __init__(self, n, weight=False): # n: number of inputs - super(Sum, self).__init__() + super().__init__() self.weight = weight # apply weights boolean self.iter = range(n - 1) # iter object if weight: - self.w = nn.Parameter(-torch.arange(1., n) / 2, requires_grad=True) # layer weights + self.w = nn.Parameter(-torch.arange(1.0, n) / 2, requires_grad=True) # layer weights def forward(self, x): y = x[0] # no weight @@ -43,86 +47,66 @@ def forward(self, x): return y -class GhostConv(nn.Module): - # Ghost Convolution https://github.com/huawei-noah/ghostnet - def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups - super(GhostConv, self).__init__() - c_ = c2 // 2 # hidden channels - self.cv1 = Conv(c1, c_, k, s, None, g, act) - self.cv2 = Conv(c_, c_, 5, 1, None, c_, act) - - def forward(self, x): - y = self.cv1(x) - return torch.cat([y, self.cv2(y)], 1) - - -class GhostBottleneck(nn.Module): - # Ghost Bottleneck https://github.com/huawei-noah/ghostnet - def __init__(self, c1, c2, k=3, s=1): # ch_in, ch_out, kernel, stride - super(GhostBottleneck, self).__init__() - c_ = c2 // 2 - self.conv = nn.Sequential(GhostConv(c1, c_, 1, 1), # pw - DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(), # dw - GhostConv(c_, c2, 1, 1, act=False)) # pw-linear - self.shortcut = nn.Sequential(DWConv(c1, c1, k, s, act=False), - Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity() - - def forward(self, x): - return self.conv(x) + self.shortcut(x) - - class MixConv2d(nn.Module): - # Mixed Depthwise Conv https://arxiv.org/abs/1907.09595 - def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True): - super(MixConv2d, self).__init__() - groups = len(k) + # Mixed Depth-wise Conv https://arxiv.org/abs/1907.09595 + def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True): # ch_in, ch_out, kernel, stride, ch_strategy + super().__init__() + n = len(k) # number of convolutions if equal_ch: # equal c_ per group - i = torch.linspace(0, groups - 1E-6, c2).floor() # c2 indices - c_ = [(i == g).sum() for g in range(groups)] # intermediate channels + i = torch.linspace(0, n - 1E-6, c2).floor() # c2 indices + c_ = [(i == g).sum() for g in range(n)] # intermediate channels else: # equal weight.numel() per group - b = [c2] + [0] * groups - a = np.eye(groups + 1, groups, k=-1) + b = [c2] + [0] * n + a = np.eye(n + 1, n, k=-1) a -= np.roll(a, 1, axis=1) a *= np.array(k) ** 2 a[0] = 1 c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b - self.m = nn.ModuleList([nn.Conv2d(c1, int(c_[g]), k[g], s, k[g] // 2, bias=False) for g in range(groups)]) + self.m = nn.ModuleList( + [nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False) for k, c_ in zip(k, c_)]) self.bn = nn.BatchNorm2d(c2) - self.act = nn.LeakyReLU(0.1, inplace=True) + self.act = nn.SiLU() def forward(self, x): - return x + self.act(self.bn(torch.cat([m(x) for m in self.m], 1))) + return self.act(self.bn(torch.cat([m(x) for m in self.m], 1))) class Ensemble(nn.ModuleList): # Ensemble of models def __init__(self): - super(Ensemble, self).__init__() + super().__init__() - def forward(self, x, augment=False): + def forward(self, x, augment=False, profile=False, visualize=False): y = [] for module in self: - y.append(module(x, augment)[0]) + y.append(module(x, augment, profile, visualize)[0]) # y = torch.stack(y).max(0)[0] # max ensemble # y = torch.stack(y).mean(0) # mean ensemble y = torch.cat(y, 1) # nms ensemble return y, None # inference, train output -def attempt_load(weights, map_location=None, inplace=True): +def attempt_load(weights, map_location=None, inplace=True, fuse=True): from models.yolo import Detect, Model # Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a model = Ensemble() for w in weights if isinstance(weights, list) else [weights]: ckpt = torch.load(attempt_download(w), map_location=map_location) # load - model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 model + if fuse: + model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 model + else: + model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().eval()) # without layer fuse # Compatibility updates for m in model.modules(): if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model]: m.inplace = inplace # pytorch 1.7.0 compatibility + if type(m) is Detect: + if not isinstance(m.anchor_grid, list): # new Detect Layer compatibility + delattr(m, 'anchor_grid') + setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl) elif type(m) is Conv: m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility diff --git a/models/export.py b/models/export.py deleted file mode 100644 index 8da43b3a5e..0000000000 --- a/models/export.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Exports a YOLOv3 *.pt model to TorchScript, ONNX, CoreML formats - -Usage: - $ python path/to/models/export.py --weights yolov3.pt --img 640 --batch 1 -""" - -import argparse -import sys -import time -from pathlib import Path - -sys.path.append(Path(__file__).parent.parent.absolute().__str__()) # to run '$ python *.py' files in subdirectories - -import torch -import torch.nn as nn -from torch.utils.mobile_optimizer import optimize_for_mobile - -import models -from models.experimental import attempt_load -from utils.activations import Hardswish, SiLU -from utils.general import colorstr, check_img_size, check_requirements, file_size, set_logging -from utils.torch_utils import select_device - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--weights', type=str, default='./yolov3.pt', help='weights path') - parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='image size') # height, width - parser.add_argument('--batch-size', type=int, default=1, help='batch size') - parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') - parser.add_argument('--include', nargs='+', default=['torchscript', 'onnx', 'coreml'], help='include formats') - parser.add_argument('--half', action='store_true', help='FP16 half-precision export') - parser.add_argument('--inplace', action='store_true', help='set YOLOv3 Detect() inplace=True') - parser.add_argument('--train', action='store_true', help='model.train() mode') - parser.add_argument('--optimize', action='store_true', help='optimize TorchScript for mobile') # TorchScript-only - parser.add_argument('--dynamic', action='store_true', help='dynamic ONNX axes') # ONNX-only - parser.add_argument('--simplify', action='store_true', help='simplify ONNX model') # ONNX-only - parser.add_argument('--opset-version', type=int, default=12, help='ONNX opset version') # ONNX-only - opt = parser.parse_args() - opt.img_size *= 2 if len(opt.img_size) == 1 else 1 # expand - opt.include = [x.lower() for x in opt.include] - print(opt) - set_logging() - t = time.time() - - # Load PyTorch model - device = select_device(opt.device) - model = attempt_load(opt.weights, map_location=device) # load FP32 model - labels = model.names - - # Checks - gs = int(max(model.stride)) # grid size (max stride) - opt.img_size = [check_img_size(x, gs) for x in opt.img_size] # verify img_size are gs-multiples - assert not (opt.device.lower() == 'cpu' and opt.half), '--half only compatible with GPU export, i.e. use --device 0' - - # Input - img = torch.zeros(opt.batch_size, 3, *opt.img_size).to(device) # image size(1,3,320,192) iDetection - - # Update model - if opt.half: - img, model = img.half(), model.half() # to FP16 - if opt.train: - model.train() # training mode (no grid construction in Detect layer) - for k, m in model.named_modules(): - m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility - if isinstance(m, models.common.Conv): # assign export-friendly activations - if isinstance(m.act, nn.Hardswish): - m.act = Hardswish() - elif isinstance(m.act, nn.SiLU): - m.act = SiLU() - elif isinstance(m, models.yolo.Detect): - m.inplace = opt.inplace - m.onnx_dynamic = opt.dynamic - # m.forward = m.forward_export # assign forward (optional) - - for _ in range(2): - y = model(img) # dry runs - print(f"\n{colorstr('PyTorch:')} starting from {opt.weights} ({file_size(opt.weights):.1f} MB)") - - # TorchScript export ----------------------------------------------------------------------------------------------- - if 'torchscript' in opt.include or 'coreml' in opt.include: - prefix = colorstr('TorchScript:') - try: - print(f'\n{prefix} starting export with torch {torch.__version__}...') - f = opt.weights.replace('.pt', '.torchscript.pt') # filename - ts = torch.jit.trace(model, img, strict=False) - (optimize_for_mobile(ts) if opt.optimize else ts).save(f) - print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') - except Exception as e: - print(f'{prefix} export failure: {e}') - - # ONNX export ------------------------------------------------------------------------------------------------------ - if 'onnx' in opt.include: - prefix = colorstr('ONNX:') - try: - import onnx - - print(f'{prefix} starting export with onnx {onnx.__version__}...') - f = opt.weights.replace('.pt', '.onnx') # filename - torch.onnx.export(model, img, f, verbose=False, opset_version=opt.opset_version, input_names=['images'], - training=torch.onnx.TrainingMode.TRAINING if opt.train else torch.onnx.TrainingMode.EVAL, - do_constant_folding=not opt.train, - dynamic_axes={'images': {0: 'batch', 2: 'height', 3: 'width'}, # size(1,3,640,640) - 'output': {0: 'batch', 2: 'y', 3: 'x'}} if opt.dynamic else None) - - # Checks - model_onnx = onnx.load(f) # load onnx model - onnx.checker.check_model(model_onnx) # check onnx model - # print(onnx.helper.printable_graph(model_onnx.graph)) # print - - # Simplify - if opt.simplify: - try: - check_requirements(['onnx-simplifier']) - import onnxsim - - print(f'{prefix} simplifying with onnx-simplifier {onnxsim.__version__}...') - model_onnx, check = onnxsim.simplify( - model_onnx, - dynamic_input_shape=opt.dynamic, - input_shapes={'images': list(img.shape)} if opt.dynamic else None) - assert check, 'assert check failed' - onnx.save(model_onnx, f) - except Exception as e: - print(f'{prefix} simplifier failure: {e}') - print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') - except Exception as e: - print(f'{prefix} export failure: {e}') - - # CoreML export ---------------------------------------------------------------------------------------------------- - if 'coreml' in opt.include: - prefix = colorstr('CoreML:') - try: - import coremltools as ct - - print(f'{prefix} starting export with coremltools {ct.__version__}...') - assert opt.train, 'CoreML exports should be placed in model.train() mode with `python export.py --train`' - model = ct.convert(ts, inputs=[ct.ImageType('image', shape=img.shape, scale=1 / 255.0, bias=[0, 0, 0])]) - f = opt.weights.replace('.pt', '.mlmodel') # filename - model.save(f) - print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)') - except Exception as e: - print(f'{prefix} export failure: {e}') - - # Finish - print(f'\nExport complete ({time.time() - t:.2f}s). Visualize with https://github.com/lutzroeder/netron.') diff --git a/models/tf.py b/models/tf.py new file mode 100644 index 0000000000..4076c9eab5 --- /dev/null +++ b/models/tf.py @@ -0,0 +1,465 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +TensorFlow, Keras and TFLite versions of +Authored by https://github.com/zldrobit in PR https://github.com/ultralytics/yolov5/pull/1127 + +Usage: + $ python models/tf.py --weights yolov3.pt + +Export: + $ python path/to/export.py --weights yolov3.pt --include saved_model pb tflite tfjs +""" + +import argparse +import logging +import sys +from copy import deepcopy +from pathlib import Path + +FILE = Path(__file__).resolve() +ROOT = FILE.parents[1] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH +# ROOT = ROOT.relative_to(Path.cwd()) # relative + +import numpy as np +import tensorflow as tf +import torch +import torch.nn as nn +from tensorflow import keras + +from models.common import C3, SPP, SPPF, Bottleneck, BottleneckCSP, Concat, Conv, DWConv, Focus, autopad +from models.experimental import CrossConv, MixConv2d, attempt_load +from models.yolo import Detect +from utils.activations import SiLU +from utils.general import LOGGER, make_divisible, print_args + + +class TFBN(keras.layers.Layer): + # TensorFlow BatchNormalization wrapper + def __init__(self, w=None): + super().__init__() + self.bn = keras.layers.BatchNormalization( + beta_initializer=keras.initializers.Constant(w.bias.numpy()), + gamma_initializer=keras.initializers.Constant(w.weight.numpy()), + moving_mean_initializer=keras.initializers.Constant(w.running_mean.numpy()), + moving_variance_initializer=keras.initializers.Constant(w.running_var.numpy()), + epsilon=w.eps) + + def call(self, inputs): + return self.bn(inputs) + + +class TFPad(keras.layers.Layer): + def __init__(self, pad): + super().__init__() + self.pad = tf.constant([[0, 0], [pad, pad], [pad, pad], [0, 0]]) + + def call(self, inputs): + return tf.pad(inputs, self.pad, mode='constant', constant_values=0) + + +class TFConv(keras.layers.Layer): + # Standard convolution + def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True, w=None): + # ch_in, ch_out, weights, kernel, stride, padding, groups + super().__init__() + assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument" + assert isinstance(k, int), "Convolution with multiple kernels are not allowed." + # TensorFlow convolution padding is inconsistent with PyTorch (e.g. k=3 s=2 'SAME' padding) + # see https://stackoverflow.com/questions/52975843/comparing-conv2d-with-padding-between-tensorflow-and-pytorch + + conv = keras.layers.Conv2D( + c2, k, s, 'SAME' if s == 1 else 'VALID', use_bias=False if hasattr(w, 'bn') else True, + kernel_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()), + bias_initializer='zeros' if hasattr(w, 'bn') else keras.initializers.Constant(w.conv.bias.numpy())) + self.conv = conv if s == 1 else keras.Sequential([TFPad(autopad(k, p)), conv]) + self.bn = TFBN(w.bn) if hasattr(w, 'bn') else tf.identity + + # activations + if isinstance(w.act, nn.LeakyReLU): + self.act = (lambda x: keras.activations.relu(x, alpha=0.1)) if act else tf.identity + elif isinstance(w.act, nn.Hardswish): + self.act = (lambda x: x * tf.nn.relu6(x + 3) * 0.166666667) if act else tf.identity + elif isinstance(w.act, (nn.SiLU, SiLU)): + self.act = (lambda x: keras.activations.swish(x)) if act else tf.identity + else: + raise Exception(f'no matching TensorFlow activation found for {w.act}') + + def call(self, inputs): + return self.act(self.bn(self.conv(inputs))) + + +class TFFocus(keras.layers.Layer): + # Focus wh information into c-space + def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True, w=None): + # ch_in, ch_out, kernel, stride, padding, groups + super().__init__() + self.conv = TFConv(c1 * 4, c2, k, s, p, g, act, w.conv) + + def call(self, inputs): # x(b,w,h,c) -> y(b,w/2,h/2,4c) + # inputs = inputs / 255 # normalize 0-255 to 0-1 + return self.conv(tf.concat([inputs[:, ::2, ::2, :], + inputs[:, 1::2, ::2, :], + inputs[:, ::2, 1::2, :], + inputs[:, 1::2, 1::2, :]], 3)) + + +class TFBottleneck(keras.layers.Layer): + # Standard bottleneck + def __init__(self, c1, c2, shortcut=True, g=1, e=0.5, w=None): # ch_in, ch_out, shortcut, groups, expansion + super().__init__() + c_ = int(c2 * e) # hidden channels + self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) + self.cv2 = TFConv(c_, c2, 3, 1, g=g, w=w.cv2) + self.add = shortcut and c1 == c2 + + def call(self, inputs): + return inputs + self.cv2(self.cv1(inputs)) if self.add else self.cv2(self.cv1(inputs)) + + +class TFConv2d(keras.layers.Layer): + # Substitution for PyTorch nn.Conv2D + def __init__(self, c1, c2, k, s=1, g=1, bias=True, w=None): + super().__init__() + assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument" + self.conv = keras.layers.Conv2D( + c2, k, s, 'VALID', use_bias=bias, + kernel_initializer=keras.initializers.Constant(w.weight.permute(2, 3, 1, 0).numpy()), + bias_initializer=keras.initializers.Constant(w.bias.numpy()) if bias else None, ) + + def call(self, inputs): + return self.conv(inputs) + + +class TFBottleneckCSP(keras.layers.Layer): + # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks + def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, w=None): + # ch_in, ch_out, number, shortcut, groups, expansion + super().__init__() + c_ = int(c2 * e) # hidden channels + self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) + self.cv2 = TFConv2d(c1, c_, 1, 1, bias=False, w=w.cv2) + self.cv3 = TFConv2d(c_, c_, 1, 1, bias=False, w=w.cv3) + self.cv4 = TFConv(2 * c_, c2, 1, 1, w=w.cv4) + self.bn = TFBN(w.bn) + self.act = lambda x: keras.activations.relu(x, alpha=0.1) + self.m = keras.Sequential([TFBottleneck(c_, c_, shortcut, g, e=1.0, w=w.m[j]) for j in range(n)]) + + def call(self, inputs): + y1 = self.cv3(self.m(self.cv1(inputs))) + y2 = self.cv2(inputs) + return self.cv4(self.act(self.bn(tf.concat((y1, y2), axis=3)))) + + +class TFC3(keras.layers.Layer): + # CSP Bottleneck with 3 convolutions + def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, w=None): + # ch_in, ch_out, number, shortcut, groups, expansion + super().__init__() + c_ = int(c2 * e) # hidden channels + self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) + self.cv2 = TFConv(c1, c_, 1, 1, w=w.cv2) + self.cv3 = TFConv(2 * c_, c2, 1, 1, w=w.cv3) + self.m = keras.Sequential([TFBottleneck(c_, c_, shortcut, g, e=1.0, w=w.m[j]) for j in range(n)]) + + def call(self, inputs): + return self.cv3(tf.concat((self.m(self.cv1(inputs)), self.cv2(inputs)), axis=3)) + + +class TFSPP(keras.layers.Layer): + # Spatial pyramid pooling layer used in YOLOv3-SPP + def __init__(self, c1, c2, k=(5, 9, 13), w=None): + super().__init__() + c_ = c1 // 2 # hidden channels + self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) + self.cv2 = TFConv(c_ * (len(k) + 1), c2, 1, 1, w=w.cv2) + self.m = [keras.layers.MaxPool2D(pool_size=x, strides=1, padding='SAME') for x in k] + + def call(self, inputs): + x = self.cv1(inputs) + return self.cv2(tf.concat([x] + [m(x) for m in self.m], 3)) + + +class TFSPPF(keras.layers.Layer): + # Spatial pyramid pooling-Fast layer + def __init__(self, c1, c2, k=5, w=None): + super().__init__() + c_ = c1 // 2 # hidden channels + self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) + self.cv2 = TFConv(c_ * 4, c2, 1, 1, w=w.cv2) + self.m = keras.layers.MaxPool2D(pool_size=k, strides=1, padding='SAME') + + def call(self, inputs): + x = self.cv1(inputs) + y1 = self.m(x) + y2 = self.m(y1) + return self.cv2(tf.concat([x, y1, y2, self.m(y2)], 3)) + + +class TFDetect(keras.layers.Layer): + def __init__(self, nc=80, anchors=(), ch=(), imgsz=(640, 640), w=None): # detection layer + super().__init__() + self.stride = tf.convert_to_tensor(w.stride.numpy(), dtype=tf.float32) + self.nc = nc # number of classes + self.no = nc + 5 # number of outputs per anchor + self.nl = len(anchors) # number of detection layers + self.na = len(anchors[0]) // 2 # number of anchors + self.grid = [tf.zeros(1)] * self.nl # init grid + self.anchors = tf.convert_to_tensor(w.anchors.numpy(), dtype=tf.float32) + self.anchor_grid = tf.reshape(self.anchors * tf.reshape(self.stride, [self.nl, 1, 1]), + [self.nl, 1, -1, 1, 2]) + self.m = [TFConv2d(x, self.no * self.na, 1, w=w.m[i]) for i, x in enumerate(ch)] + self.training = False # set to False after building model + self.imgsz = imgsz + for i in range(self.nl): + ny, nx = self.imgsz[0] // self.stride[i], self.imgsz[1] // self.stride[i] + self.grid[i] = self._make_grid(nx, ny) + + def call(self, inputs): + z = [] # inference output + x = [] + for i in range(self.nl): + x.append(self.m[i](inputs[i])) + # x(bs,20,20,255) to x(bs,3,20,20,85) + ny, nx = self.imgsz[0] // self.stride[i], self.imgsz[1] // self.stride[i] + x[i] = tf.transpose(tf.reshape(x[i], [-1, ny * nx, self.na, self.no]), [0, 2, 1, 3]) + + if not self.training: # inference + y = tf.sigmoid(x[i]) + xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy + wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] + # Normalize xywh to 0-1 to reduce calibration error + xy /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32) + wh /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32) + y = tf.concat([xy, wh, y[..., 4:]], -1) + z.append(tf.reshape(y, [-1, 3 * ny * nx, self.no])) + + return x if self.training else (tf.concat(z, 1), x) + + @staticmethod + def _make_grid(nx=20, ny=20): + # yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)]) + # return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float() + xv, yv = tf.meshgrid(tf.range(nx), tf.range(ny)) + return tf.cast(tf.reshape(tf.stack([xv, yv], 2), [1, 1, ny * nx, 2]), dtype=tf.float32) + + +class TFUpsample(keras.layers.Layer): + def __init__(self, size, scale_factor, mode, w=None): # warning: all arguments needed including 'w' + super().__init__() + assert scale_factor == 2, "scale_factor must be 2" + self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * 2, x.shape[2] * 2), method=mode) + # self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode) + # with default arguments: align_corners=False, half_pixel_centers=False + # self.upsample = lambda x: tf.raw_ops.ResizeNearestNeighbor(images=x, + # size=(x.shape[1] * 2, x.shape[2] * 2)) + + def call(self, inputs): + return self.upsample(inputs) + + +class TFConcat(keras.layers.Layer): + def __init__(self, dimension=1, w=None): + super().__init__() + assert dimension == 1, "convert only NCHW to NHWC concat" + self.d = 3 + + def call(self, inputs): + return tf.concat(inputs, self.d) + + +def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3) + LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") + anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'] + na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors + no = na * (nc + 5) # number of outputs = anchors * (classes + 5) + + layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out + for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args + m_str = m + m = eval(m) if isinstance(m, str) else m # eval strings + for j, a in enumerate(args): + try: + args[j] = eval(a) if isinstance(a, str) else a # eval strings + except NameError: + pass + + n = max(round(n * gd), 1) if n > 1 else n # depth gain + if m in [nn.Conv2d, Conv, Bottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv, BottleneckCSP, C3]: + c1, c2 = ch[f], args[0] + c2 = make_divisible(c2 * gw, 8) if c2 != no else c2 + + args = [c1, c2, *args[1:]] + if m in [BottleneckCSP, C3]: + args.insert(2, n) + n = 1 + elif m is nn.BatchNorm2d: + args = [ch[f]] + elif m is Concat: + c2 = sum(ch[-1 if x == -1 else x + 1] for x in f) + elif m is Detect: + args.append([ch[x + 1] for x in f]) + if isinstance(args[1], int): # number of anchors + args[1] = [list(range(args[1] * 2))] * len(f) + args.append(imgsz) + else: + c2 = ch[f] + + tf_m = eval('TF' + m_str.replace('nn.', '')) + m_ = keras.Sequential([tf_m(*args, w=model.model[i][j]) for j in range(n)]) if n > 1 \ + else tf_m(*args, w=model.model[i]) # module + + torch_m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module + t = str(m)[8:-2].replace('__main__.', '') # module type + np = sum(x.numel() for x in torch_m_.parameters()) # number params + m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params + LOGGER.info(f'{i:>3}{str(f):>18}{str(n):>3}{np:>10} {t:<40}{str(args):<30}') # print + save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist + layers.append(m_) + ch.append(c2) + return keras.Sequential(layers), sorted(save) + + +class TFModel: + def __init__(self, cfg='yolov3.yaml', ch=3, nc=None, model=None, imgsz=(640, 640)): # model, channels, classes + super().__init__() + if isinstance(cfg, dict): + self.yaml = cfg # model dict + else: # is *.yaml + import yaml # for torch hub + self.yaml_file = Path(cfg).name + with open(cfg) as f: + self.yaml = yaml.load(f, Loader=yaml.FullLoader) # model dict + + # Define model + if nc and nc != self.yaml['nc']: + LOGGER.info(f"Overriding {cfg} nc={self.yaml['nc']} with nc={nc}") + self.yaml['nc'] = nc # override yaml value + self.model, self.savelist = parse_model(deepcopy(self.yaml), ch=[ch], model=model, imgsz=imgsz) + + def predict(self, inputs, tf_nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45, + conf_thres=0.25): + y = [] # outputs + x = inputs + for i, m in enumerate(self.model.layers): + if m.f != -1: # if not from previous layer + x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers + + x = m(x) # run + y.append(x if m.i in self.savelist else None) # save output + + # Add TensorFlow NMS + if tf_nms: + boxes = self._xywh2xyxy(x[0][..., :4]) + probs = x[0][:, :, 4:5] + classes = x[0][:, :, 5:] + scores = probs * classes + if agnostic_nms: + nms = AgnosticNMS()((boxes, classes, scores), topk_all, iou_thres, conf_thres) + return nms, x[1] + else: + boxes = tf.expand_dims(boxes, 2) + nms = tf.image.combined_non_max_suppression( + boxes, scores, topk_per_class, topk_all, iou_thres, conf_thres, clip_boxes=False) + return nms, x[1] + + return x[0] # output only first tensor [1,6300,85] = [xywh, conf, class0, class1, ...] + # x = x[0][0] # [x(1,6300,85), ...] to x(6300,85) + # xywh = x[..., :4] # x(6300,4) boxes + # conf = x[..., 4:5] # x(6300,1) confidences + # cls = tf.reshape(tf.cast(tf.argmax(x[..., 5:], axis=1), tf.float32), (-1, 1)) # x(6300,1) classes + # return tf.concat([conf, cls, xywh], 1) + + @staticmethod + def _xywh2xyxy(xywh): + # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right + x, y, w, h = tf.split(xywh, num_or_size_splits=4, axis=-1) + return tf.concat([x - w / 2, y - h / 2, x + w / 2, y + h / 2], axis=-1) + + +class AgnosticNMS(keras.layers.Layer): + # TF Agnostic NMS + def call(self, input, topk_all, iou_thres, conf_thres): + # wrap map_fn to avoid TypeSpec related error https://stackoverflow.com/a/65809989/3036450 + return tf.map_fn(lambda x: self._nms(x, topk_all, iou_thres, conf_thres), input, + fn_output_signature=(tf.float32, tf.float32, tf.float32, tf.int32), + name='agnostic_nms') + + @staticmethod + def _nms(x, topk_all=100, iou_thres=0.45, conf_thres=0.25): # agnostic NMS + boxes, classes, scores = x + class_inds = tf.cast(tf.argmax(classes, axis=-1), tf.float32) + scores_inp = tf.reduce_max(scores, -1) + selected_inds = tf.image.non_max_suppression( + boxes, scores_inp, max_output_size=topk_all, iou_threshold=iou_thres, score_threshold=conf_thres) + selected_boxes = tf.gather(boxes, selected_inds) + padded_boxes = tf.pad(selected_boxes, + paddings=[[0, topk_all - tf.shape(selected_boxes)[0]], [0, 0]], + mode="CONSTANT", constant_values=0.0) + selected_scores = tf.gather(scores_inp, selected_inds) + padded_scores = tf.pad(selected_scores, + paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]], + mode="CONSTANT", constant_values=-1.0) + selected_classes = tf.gather(class_inds, selected_inds) + padded_classes = tf.pad(selected_classes, + paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]], + mode="CONSTANT", constant_values=-1.0) + valid_detections = tf.shape(selected_inds)[0] + return padded_boxes, padded_scores, padded_classes, valid_detections + + +def representative_dataset_gen(dataset, ncalib=100): + # Representative dataset generator for use with converter.representative_dataset, returns a generator of np arrays + for n, (path, img, im0s, vid_cap, string) in enumerate(dataset): + input = np.transpose(img, [1, 2, 0]) + input = np.expand_dims(input, axis=0).astype(np.float32) + input /= 255 + yield [input] + if n >= ncalib: + break + + +def run(weights=ROOT / 'yolov3.pt', # weights path + imgsz=(640, 640), # inference size h,w + batch_size=1, # batch size + dynamic=False, # dynamic batch size + ): + # PyTorch model + im = torch.zeros((batch_size, 3, *imgsz)) # BCHW image + model = attempt_load(weights, map_location=torch.device('cpu'), inplace=True, fuse=False) + y = model(im) # inference + model.info() + + # TensorFlow model + im = tf.zeros((batch_size, *imgsz, 3)) # BHWC image + tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz) + y = tf_model.predict(im) # inference + + # Keras model + im = keras.Input(shape=(*imgsz, 3), batch_size=None if dynamic else batch_size) + keras_model = keras.Model(inputs=im, outputs=tf_model.predict(im)) + keras_model.summary() + + LOGGER.info('PyTorch, TensorFlow and Keras models successfully verified.\nUse export.py for TF model export.') + + +def parse_opt(): + parser = argparse.ArgumentParser() + parser.add_argument('--weights', type=str, default=ROOT / 'yolov3.pt', help='weights path') + parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w') + parser.add_argument('--batch-size', type=int, default=1, help='batch size') + parser.add_argument('--dynamic', action='store_true', help='dynamic batch size') + opt = parser.parse_args() + opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand + print_args(FILE.stem, opt) + return opt + + +def main(opt): + run(**vars(opt)) + + +if __name__ == "__main__": + opt = parse_opt() + main(opt) diff --git a/models/yolo.py b/models/yolo.py index 934cab69df..f398d3f971 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -1,27 +1,32 @@ -"""YOLOv3-specific modules +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +YOLO-specific modules Usage: $ python path/to/models/yolo.py --cfg yolov3.yaml """ import argparse -import logging import sys from copy import deepcopy from pathlib import Path -sys.path.append(Path(__file__).parent.parent.absolute().__str__()) # to run '$ python *.py' files in subdirectories -logger = logging.getLogger(__name__) +FILE = Path(__file__).resolve() +ROOT = FILE.parents[1] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH +# ROOT = ROOT.relative_to(Path.cwd()) # relative from models.common import * from models.experimental import * from utils.autoanchor import check_anchor_order -from utils.general import make_divisible, check_file, set_logging -from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \ - select_device, copy_attr +from utils.general import LOGGER, check_version, check_yaml, make_divisible, print_args +from utils.plots import feature_visualization +from utils.torch_utils import (copy_attr, fuse_conv_and_bn, initialize_weights, model_info, scale_img, select_device, + time_sync) try: - import thop # for FLOPS computation + import thop # for FLOPs computation except ImportError: thop = None @@ -31,20 +36,18 @@ class Detect(nn.Module): onnx_dynamic = False # ONNX export parameter def __init__(self, nc=80, anchors=(), ch=(), inplace=True): # detection layer - super(Detect, self).__init__() + super().__init__() self.nc = nc # number of classes self.no = nc + 5 # number of outputs per anchor self.nl = len(anchors) # number of detection layers self.na = len(anchors[0]) // 2 # number of anchors self.grid = [torch.zeros(1)] * self.nl # init grid - a = torch.tensor(anchors).float().view(self.nl, -1, 2) - self.register_buffer('anchors', a) # shape(nl,na,2) - self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) # shape(nl,1,na,1,1,2) + self.anchor_grid = [torch.zeros(1)] * self.nl # init anchor grid + self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2) self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv self.inplace = inplace # use in-place ops (e.g. slice assignment) def forward(self, x): - # x = x.copy() # for profiling z = [] # inference output for i in range(self.nl): x[i] = self.m[i](x[i]) # conv @@ -52,50 +55,55 @@ def forward(self, x): x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous() if not self.training: # inference - if self.grid[i].shape[2:4] != x[i].shape[2:4] or self.onnx_dynamic: - self.grid[i] = self._make_grid(nx, ny).to(x[i].device) + if self.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]: + self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i) y = x[i].sigmoid() if self.inplace: - y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy + y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh - else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953 - xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy - wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i].view(1, self.na, 1, 1, 2) # wh + else: # for on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953 + xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy + wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh y = torch.cat((xy, wh, y[..., 4:]), -1) z.append(y.view(bs, -1, self.no)) return x if self.training else (torch.cat(z, 1), x) - @staticmethod - def _make_grid(nx=20, ny=20): - yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)]) - return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float() + def _make_grid(self, nx=20, ny=20, i=0): + d = self.anchors[i].device + if check_version(torch.__version__, '1.10.0'): # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility + yv, xv = torch.meshgrid([torch.arange(ny).to(d), torch.arange(nx).to(d)], indexing='ij') + else: + yv, xv = torch.meshgrid([torch.arange(ny).to(d), torch.arange(nx).to(d)]) + grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float() + anchor_grid = (self.anchors[i].clone() * self.stride[i]) \ + .view((1, self.na, 1, 1, 2)).expand((1, self.na, ny, nx, 2)).float() + return grid, anchor_grid class Model(nn.Module): def __init__(self, cfg='yolov3.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes - super(Model, self).__init__() + super().__init__() if isinstance(cfg, dict): self.yaml = cfg # model dict else: # is *.yaml import yaml # for torch hub self.yaml_file = Path(cfg).name - with open(cfg) as f: + with open(cfg, encoding='ascii', errors='ignore') as f: self.yaml = yaml.safe_load(f) # model dict # Define model ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels if nc and nc != self.yaml['nc']: - logger.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}") + LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}") self.yaml['nc'] = nc # override yaml value if anchors: - logger.info(f'Overriding model.yaml anchors with anchors={anchors}') + LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}') self.yaml['anchors'] = round(anchors) # override yaml value self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist self.names = [str(i) for i in range(self.yaml['nc'])] # default names self.inplace = self.yaml.get('inplace', True) - # logger.info([x.shape for x in self.forward(torch.zeros(1, ch, 64, 64))]) # Build strides, anchors m = self.model[-1] # Detect() @@ -107,53 +115,42 @@ def __init__(self, cfg='yolov3.yaml', ch=3, nc=None, anchors=None): # model, in check_anchor_order(m) self.stride = m.stride self._initialize_biases() # only run once - # logger.info('Strides: %s' % m.stride.tolist()) # Init weights, biases initialize_weights(self) self.info() - logger.info('') + LOGGER.info('') - def forward(self, x, augment=False, profile=False): + def forward(self, x, augment=False, profile=False, visualize=False): if augment: - return self.forward_augment(x) # augmented inference, None - else: - return self.forward_once(x, profile) # single-scale inference, train + return self._forward_augment(x) # augmented inference, None + return self._forward_once(x, profile, visualize) # single-scale inference, train - def forward_augment(self, x): + def _forward_augment(self, x): img_size = x.shape[-2:] # height, width s = [1, 0.83, 0.67] # scales f = [None, 3, None] # flips (2-ud, 3-lr) y = [] # outputs for si, fi in zip(s, f): xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max())) - yi = self.forward_once(xi)[0] # forward + yi = self._forward_once(xi)[0] # forward # cv2.imwrite(f'img_{si}.jpg', 255 * xi[0].cpu().numpy().transpose((1, 2, 0))[:, :, ::-1]) # save yi = self._descale_pred(yi, fi, si, img_size) y.append(yi) + y = self._clip_augmented(y) # clip augmented tails return torch.cat(y, 1), None # augmented inference, train - def forward_once(self, x, profile=False): + def _forward_once(self, x, profile=False, visualize=False): y, dt = [], [] # outputs for m in self.model: if m.f != -1: # if not from previous layer x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers - if profile: - o = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPS - t = time_synchronized() - for _ in range(10): - _ = m(x) - dt.append((time_synchronized() - t) * 100) - if m == self.model[0]: - logger.info(f"{'time (ms)':>10s} {'GFLOPS':>10s} {'params':>10s} {'module'}") - logger.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}') - + self._profile_one_layer(m, x, dt) x = m(x) # run y.append(x if m.i in self.save else None) # save output - - if profile: - logger.info('%.1fms total' % sum(dt)) + if visualize: + feature_visualization(x, m.type, m.i, save_dir=visualize) return x def _descale_pred(self, p, flips, scale, img_size): @@ -173,6 +170,30 @@ def _descale_pred(self, p, flips, scale, img_size): p = torch.cat((x, y, wh, p[..., 4:]), -1) return p + def _clip_augmented(self, y): + # Clip augmented inference tails + nl = self.model[-1].nl # number of detection layers (P3-P5) + g = sum(4 ** x for x in range(nl)) # grid points + e = 1 # exclude layer count + i = (y[0].shape[1] // g) * sum(4 ** x for x in range(e)) # indices + y[0] = y[0][:, :-i] # large + i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices + y[-1] = y[-1][:, i:] # small + return y + + def _profile_one_layer(self, m, x, dt): + c = isinstance(m, Detect) # is final layer, copy input as inplace fix + o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs + t = time_sync() + for _ in range(10): + m(x.copy() if c else x) + dt.append((time_sync() - t) * 100) + if m == self.model[0]: + LOGGER.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} {'module'}") + LOGGER.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}') + if c: + LOGGER.info(f"{sum(dt):10.2f} {'-':>10s} {'-':>10s} Total") + def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency # https://arxiv.org/abs/1708.02002 section 3.3 # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1. @@ -180,47 +201,33 @@ def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is for mi, s in zip(m.m, m.stride): # from b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85) b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image) - b.data[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum()) # cls + b.data[:, 5:] += math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum()) # cls mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True) def _print_biases(self): m = self.model[-1] # Detect() module for mi in m.m: # from b = mi.bias.detach().view(m.na, -1).T # conv.bias(255) to (3,85) - logger.info( + LOGGER.info( ('%6g Conv2d.bias:' + '%10.3g' * 6) % (mi.weight.shape[1], *b[:5].mean(1).tolist(), b[5:].mean())) # def _print_weights(self): # for m in self.model.modules(): # if type(m) is Bottleneck: - # logger.info('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights + # LOGGER.info('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers - logger.info('Fusing layers... ') + LOGGER.info('Fusing layers... ') for m in self.model.modules(): - if type(m) is Conv and hasattr(m, 'bn'): + if isinstance(m, (Conv, DWConv)) and hasattr(m, 'bn'): m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv delattr(m, 'bn') # remove batchnorm - m.forward = m.fuseforward # update forward + m.forward = m.forward_fuse # update forward self.info() return self - def nms(self, mode=True): # add or remove NMS module - present = type(self.model[-1]) is NMS # last layer is NMS - if mode and not present: - logger.info('Adding NMS... ') - m = NMS() # module - m.f = -1 # from - m.i = self.model[-1].i + 1 # index - self.model.add_module(name='%s' % m.i, module=m) # add - self.eval() - elif not mode and present: - logger.info('Removing NMS... ') - self.model = self.model[:-1] # remove - return self - def autoshape(self): # add AutoShape module - logger.info('Adding AutoShape... ') + LOGGER.info('Adding AutoShape... ') m = AutoShape(self) # wrap model copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes return m @@ -228,9 +235,20 @@ def autoshape(self): # add AutoShape module def info(self, verbose=False, img_size=640): # print model information model_info(self, verbose, img_size) + def _apply(self, fn): + # Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers + self = super()._apply(fn) + m = self.model[-1] # Detect() + if isinstance(m, Detect): + m.stride = fn(m.stride) + m.grid = list(map(fn, m.grid)) + if isinstance(m.anchor_grid, list): + m.anchor_grid = list(map(fn, m.anchor_grid)) + return self + def parse_model(d, ch): # model_dict, input_channels(3) - logger.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments')) + LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'] na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors no = na * (nc + 5) # number of outputs = anchors * (classes + 5) @@ -241,24 +259,24 @@ def parse_model(d, ch): # model_dict, input_channels(3) for j, a in enumerate(args): try: args[j] = eval(a) if isinstance(a, str) else a # eval strings - except: + except NameError: pass - n = max(round(n * gd), 1) if n > 1 else n # depth gain - if m in [Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, DWConv, MixConv2d, Focus, CrossConv, BottleneckCSP, - C3, C3TR]: + n = n_ = max(round(n * gd), 1) if n > 1 else n # depth gain + if m in [Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv, + BottleneckCSP, C3, C3TR, C3SPP, C3Ghost]: c1, c2 = ch[f], args[0] if c2 != no: # if not output c2 = make_divisible(c2 * gw, 8) args = [c1, c2, *args[1:]] - if m in [BottleneckCSP, C3, C3TR]: + if m in [BottleneckCSP, C3, C3TR, C3Ghost]: args.insert(2, n) # number of repeats n = 1 elif m is nn.BatchNorm2d: args = [ch[f]] elif m is Concat: - c2 = sum([ch[x] for x in f]) + c2 = sum(ch[x] for x in f) elif m is Detect: args.append([ch[x] for x in f]) if isinstance(args[1], int): # number of anchors @@ -270,11 +288,11 @@ def parse_model(d, ch): # model_dict, input_channels(3) else: c2 = ch[f] - m_ = nn.Sequential(*[m(*args) for _ in range(n)]) if n > 1 else m(*args) # module + m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module t = str(m)[8:-2].replace('__main__.', '') # module type - np = sum([x.numel() for x in m_.parameters()]) # number params + np = sum(x.numel() for x in m_.parameters()) # number params m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params - logger.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print + LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}') # print save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist layers.append(m_) if i == 0: @@ -285,11 +303,13 @@ def parse_model(d, ch): # model_dict, input_channels(3) if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--cfg', type=str, default='yolov3.yaml', help='model.yaml') + parser.add_argument('--cfg', type=str, default='yolov3yaml', help='model.yaml') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') + parser.add_argument('--profile', action='store_true', help='profile model speed') + parser.add_argument('--test', action='store_true', help='test all yolo*.yaml') opt = parser.parse_args() - opt.cfg = check_file(opt.cfg) # check file - set_logging() + opt.cfg = check_yaml(opt.cfg) # check YAML + print_args(FILE.stem, opt) device = select_device(opt.device) # Create model @@ -297,12 +317,20 @@ def parse_model(d, ch): # model_dict, input_channels(3) model.train() # Profile - # img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 320, 320).to(device) - # y = model(img, profile=True) + if opt.profile: + img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 640, 640).to(device) + y = model(img, profile=True) + + # Test all models + if opt.test: + for cfg in Path(ROOT / 'models').rglob('yolo*.yaml'): + try: + _ = Model(cfg) + except Exception as e: + print(f'Error in {cfg}: {e}') # Tensorboard (not working https://github.com/ultralytics/yolov5/issues/2898) # from torch.utils.tensorboard import SummaryWriter # tb_writer = SummaryWriter('.') - # logger.info("Run 'tensorboard --logdir=models' to view tensorboard at http://localhost:6006/") + # LOGGER.info("Run 'tensorboard --logdir=models' to view tensorboard at http://localhost:6006/") # tb_writer.add_graph(torch.jit.trace(model, img, strict=False), []) # add model graph - # tb_writer.add_image('test', img[0], dataformats='CWH') # add model to tensorboard diff --git a/models/yolov3-spp.yaml b/models/yolov3-spp.yaml index 38dcc449f0..04593a47d2 100644 --- a/models/yolov3-spp.yaml +++ b/models/yolov3-spp.yaml @@ -1,9 +1,9 @@ -# parameters +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license + +# Parameters nc: 80 # number of classes depth_multiple: 1.0 # model depth multiple width_multiple: 1.0 # layer channel multiple - -# anchors anchors: - [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 diff --git a/models/yolov3-tiny.yaml b/models/yolov3-tiny.yaml index ff7638cad3..04f9b446a6 100644 --- a/models/yolov3-tiny.yaml +++ b/models/yolov3-tiny.yaml @@ -1,9 +1,9 @@ -# parameters +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license + +# Parameters nc: 80 # number of classes depth_multiple: 1.0 # model depth multiple width_multiple: 1.0 # layer channel multiple - -# anchors anchors: - [10,14, 23,27, 37,58] # P4/16 - [81,82, 135,169, 344,319] # P5/32 diff --git a/models/yolov3.yaml b/models/yolov3.yaml index f2e7613554..3d041bb393 100644 --- a/models/yolov3.yaml +++ b/models/yolov3.yaml @@ -1,9 +1,9 @@ -# parameters +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license + +# Parameters nc: 80 # number of classes depth_multiple: 1.0 # model depth multiple width_multiple: 1.0 # layer channel multiple - -# anchors anchors: - [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 diff --git a/requirements.txt b/requirements.txt index 1c07c65115..22b51fc490 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,36 @@ # pip install -r requirements.txt -# base ---------------------------------------- +# Base ---------------------------------------- matplotlib>=3.2.2 numpy>=1.18.5 opencv-python>=4.1.2 -Pillow +Pillow>=7.1.2 PyYAML>=5.3.1 +requests>=2.23.0 scipy>=1.4.1 torch>=1.7.0 torchvision>=0.8.1 tqdm>=4.41.0 -# logging ------------------------------------- +# Logging ------------------------------------- tensorboard>=2.4.1 # wandb -# plotting ------------------------------------ +# Plotting ------------------------------------ +pandas>=1.1.4 seaborn>=0.11.0 -pandas -# export -------------------------------------- -# coremltools>=4.1 -# onnx>=1.9.0 -# scikit-learn==0.19.2 # for coreml quantization +# Export -------------------------------------- +# coremltools>=4.1 # CoreML export +# onnx>=1.9.0 # ONNX export +# onnx-simplifier>=0.3.6 # ONNX simplifier +# scikit-learn==0.19.2 # CoreML quantization +# tensorflow>=2.4.1 # TFLite export +# tensorflowjs>=3.9.0 # TF.js export -# extras -------------------------------------- +# Extras -------------------------------------- +# albumentations>=1.0.3 # Cython # for pycocotools https://github.com/cocodataset/cocoapi/issues/172 -pycocotools>=2.0 # COCO mAP -thop # FLOPS computation +# pycocotools>=2.0 # COCO mAP +# roboflow +thop # FLOPs computation diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..4ca0f0d7aa --- /dev/null +++ b/setup.cfg @@ -0,0 +1,51 @@ +# Project-wide configuration file, can be used for package metadata and other toll configurations +# Example usage: global configuration for PEP8 (via flake8) setting or default pytest arguments + +[metadata] +license_file = LICENSE +description-file = README.md + + +[tool:pytest] +norecursedirs = + .git + dist + build +addopts = + --doctest-modules + --durations=25 + --color=yes + + +[flake8] +max-line-length = 120 +exclude = .tox,*.egg,build,temp +select = E,W,F +doctests = True +verbose = 2 +# https://pep8.readthedocs.io/en/latest/intro.html#error-codes +format = pylint +# see: https://www.flake8rules.com/ +ignore = + E731 # Do not assign a lambda expression, use a def + F405 + E402 + F841 + E741 + F821 + E722 + F401 + W504 + E127 + W504 + E231 + E501 + F403 + E302 + F541 + + +[isort] +# https://pycqa.github.io/isort/docs/configuration/options.html +line_length = 120 +multi_line_output = 0 diff --git a/test.py b/test.py deleted file mode 100644 index 396beef6e1..0000000000 --- a/test.py +++ /dev/null @@ -1,349 +0,0 @@ -import argparse -import json -import os -from pathlib import Path -from threading import Thread - -import numpy as np -import torch -import yaml -from tqdm import tqdm - -from models.experimental import attempt_load -from utils.datasets import create_dataloader -from utils.general import coco80_to_coco91_class, check_dataset, check_file, check_img_size, check_requirements, \ - box_iou, non_max_suppression, scale_coords, xyxy2xywh, xywh2xyxy, set_logging, increment_path, colorstr -from utils.metrics import ap_per_class, ConfusionMatrix -from utils.plots import plot_images, output_to_target, plot_study_txt -from utils.torch_utils import select_device, time_synchronized - - -@torch.no_grad() -def test(data, - weights=None, - batch_size=32, - imgsz=640, - conf_thres=0.001, - iou_thres=0.6, # for NMS - save_json=False, - single_cls=False, - augment=False, - verbose=False, - model=None, - dataloader=None, - save_dir=Path(''), # for saving images - save_txt=False, # for auto-labelling - save_hybrid=False, # for hybrid auto-labelling - save_conf=False, # save auto-label confidences - plots=True, - wandb_logger=None, - compute_loss=None, - half_precision=True, - is_coco=False, - opt=None): - # Initialize/load model and set device - training = model is not None - if training: # called by train.py - device = next(model.parameters()).device # get model device - - else: # called directly - set_logging() - device = select_device(opt.device, batch_size=batch_size) - - # Directories - save_dir = increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok) # increment run - (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir - - # Load model - model = attempt_load(weights, map_location=device) # load FP32 model - gs = max(int(model.stride.max()), 32) # grid size (max stride) - imgsz = check_img_size(imgsz, s=gs) # check img_size - - # Multi-GPU disabled, incompatible with .half() https://github.com/ultralytics/yolov5/issues/99 - # if device.type != 'cpu' and torch.cuda.device_count() > 1: - # model = nn.DataParallel(model) - - # Half - half = device.type != 'cpu' and half_precision # half precision only supported on CUDA - if half: - model.half() - - # Configure - model.eval() - if isinstance(data, str): - is_coco = data.endswith('coco.yaml') - with open(data) as f: - data = yaml.safe_load(f) - check_dataset(data) # check - nc = 1 if single_cls else int(data['nc']) # number of classes - iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95 - niou = iouv.numel() - - # Logging - log_imgs = 0 - if wandb_logger and wandb_logger.wandb: - log_imgs = min(wandb_logger.log_imgs, 100) - # Dataloader - if not training: - if device.type != 'cpu': - model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once - task = opt.task if opt.task in ('train', 'val', 'test') else 'val' # path to train/val/test images - dataloader = create_dataloader(data[task], imgsz, batch_size, gs, opt, pad=0.5, rect=True, - prefix=colorstr(f'{task}: '))[0] - - seen = 0 - confusion_matrix = ConfusionMatrix(nc=nc) - names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)} - coco91class = coco80_to_coco91_class() - s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95') - p, r, f1, mp, mr, map50, map, t0, t1 = 0., 0., 0., 0., 0., 0., 0., 0., 0. - loss = torch.zeros(3, device=device) - jdict, stats, ap, ap_class, wandb_images = [], [], [], [], [] - for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)): - img = img.to(device, non_blocking=True) - img = img.half() if half else img.float() # uint8 to fp16/32 - img /= 255.0 # 0 - 255 to 0.0 - 1.0 - targets = targets.to(device) - nb, _, height, width = img.shape # batch size, channels, height, width - - # Run model - t = time_synchronized() - out, train_out = model(img, augment=augment) # inference and training outputs - t0 += time_synchronized() - t - - # Compute loss - if compute_loss: - loss += compute_loss([x.float() for x in train_out], targets)[1][:3] # box, obj, cls - - # Run NMS - targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels - lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling - t = time_synchronized() - out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls) - t1 += time_synchronized() - t - - # Statistics per image - for si, pred in enumerate(out): - labels = targets[targets[:, 0] == si, 1:] - nl = len(labels) - tcls = labels[:, 0].tolist() if nl else [] # target class - path = Path(paths[si]) - seen += 1 - - if len(pred) == 0: - if nl: - stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls)) - continue - - # Predictions - if single_cls: - pred[:, 5] = 0 - predn = pred.clone() - scale_coords(img[si].shape[1:], predn[:, :4], shapes[si][0], shapes[si][1]) # native-space pred - - # Append to text file - if save_txt: - gn = torch.tensor(shapes[si][0])[[1, 0, 1, 0]] # normalization gain whwh - for *xyxy, conf, cls in predn.tolist(): - xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh - line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format - with open(save_dir / 'labels' / (path.stem + '.txt'), 'a') as f: - f.write(('%g ' * len(line)).rstrip() % line + '\n') - - # W&B logging - Media Panel Plots - if len(wandb_images) < log_imgs and wandb_logger.current_epoch > 0: # Check for test operation - if wandb_logger.current_epoch % wandb_logger.bbox_interval == 0: - box_data = [{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]}, - "class_id": int(cls), - "box_caption": "%s %.3f" % (names[cls], conf), - "scores": {"class_score": conf}, - "domain": "pixel"} for *xyxy, conf, cls in pred.tolist()] - boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space - wandb_images.append(wandb_logger.wandb.Image(img[si], boxes=boxes, caption=path.name)) - wandb_logger.log_training_progress(predn, path, names) if wandb_logger and wandb_logger.wandb_run else None - - # Append to pycocotools JSON dictionary - if save_json: - # [{"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}, ... - image_id = int(path.stem) if path.stem.isnumeric() else path.stem - box = xyxy2xywh(predn[:, :4]) # xywh - box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner - for p, b in zip(pred.tolist(), box.tolist()): - jdict.append({'image_id': image_id, - 'category_id': coco91class[int(p[5])] if is_coco else int(p[5]), - 'bbox': [round(x, 3) for x in b], - 'score': round(p[4], 5)}) - - # Assign all predictions as incorrect - correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device) - if nl: - detected = [] # target indices - tcls_tensor = labels[:, 0] - - # target boxes - tbox = xywh2xyxy(labels[:, 1:5]) - scale_coords(img[si].shape[1:], tbox, shapes[si][0], shapes[si][1]) # native-space labels - if plots: - confusion_matrix.process_batch(predn, torch.cat((labels[:, 0:1], tbox), 1)) - - # Per target class - for cls in torch.unique(tcls_tensor): - ti = (cls == tcls_tensor).nonzero(as_tuple=False).view(-1) # target indices - pi = (cls == pred[:, 5]).nonzero(as_tuple=False).view(-1) # prediction indices - - # Search for detections - if pi.shape[0]: - # Prediction to target ious - ious, i = box_iou(predn[pi, :4], tbox[ti]).max(1) # best ious, indices - - # Append detections - detected_set = set() - for j in (ious > iouv[0]).nonzero(as_tuple=False): - d = ti[i[j]] # detected target - if d.item() not in detected_set: - detected_set.add(d.item()) - detected.append(d) - correct[pi[j]] = ious[j] > iouv # iou_thres is 1xn - if len(detected) == nl: # all targets already located in image - break - - # Append statistics (correct, conf, pcls, tcls) - stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) - - # Plot images - if plots and batch_i < 3: - f = save_dir / f'test_batch{batch_i}_labels.jpg' # labels - Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start() - f = save_dir / f'test_batch{batch_i}_pred.jpg' # predictions - Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start() - - # Compute statistics - stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy - if len(stats) and stats[0].any(): - p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names) - ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95 - mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean() - nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class - else: - nt = torch.zeros(1) - - # Print results - pf = '%20s' + '%12i' * 2 + '%12.3g' * 4 # print format - print(pf % ('all', seen, nt.sum(), mp, mr, map50, map)) - - # Print results per class - if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): - for i, c in enumerate(ap_class): - print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i])) - - # Print speeds - t = tuple(x / seen * 1E3 for x in (t0, t1, t0 + t1)) + (imgsz, imgsz, batch_size) # tuple - if not training: - print('Speed: %.1f/%.1f/%.1f ms inference/NMS/total per %gx%g image at batch-size %g' % t) - - # Plots - if plots: - confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) - if wandb_logger and wandb_logger.wandb: - val_batches = [wandb_logger.wandb.Image(str(f), caption=f.name) for f in sorted(save_dir.glob('test*.jpg'))] - wandb_logger.log({"Validation": val_batches}) - if wandb_images: - wandb_logger.log({"Bounding Box Debugger/Images": wandb_images}) - - # Save JSON - if save_json and len(jdict): - w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights - anno_json = '../coco/annotations/instances_val2017.json' # annotations json - pred_json = str(save_dir / f"{w}_predictions.json") # predictions json - print('\nEvaluating pycocotools mAP... saving %s...' % pred_json) - with open(pred_json, 'w') as f: - json.dump(jdict, f) - - try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb - from pycocotools.coco import COCO - from pycocotools.cocoeval import COCOeval - - anno = COCO(anno_json) # init annotations api - pred = anno.loadRes(pred_json) # init predictions api - eval = COCOeval(anno, pred, 'bbox') - if is_coco: - eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files] # image IDs to evaluate - eval.evaluate() - eval.accumulate() - eval.summarize() - map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5) - except Exception as e: - print(f'pycocotools unable to run: {e}') - - # Return results - model.float() # for training - if not training: - s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' - print(f"Results saved to {save_dir}{s}") - maps = np.zeros(nc) + map - for i, c in enumerate(ap_class): - maps[c] = ap[i] - return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(prog='test.py') - parser.add_argument('--weights', nargs='+', type=str, default='yolov3.pt', help='model.pt path(s)') - parser.add_argument('--data', type=str, default='data/coco128.yaml', help='*.data path') - parser.add_argument('--batch-size', type=int, default=32, help='size of each image batch') - parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)') - parser.add_argument('--conf-thres', type=float, default=0.001, help='object confidence threshold') - parser.add_argument('--iou-thres', type=float, default=0.6, help='IOU threshold for NMS') - parser.add_argument('--task', default='val', help='train, val, test, speed or study') - parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') - parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset') - parser.add_argument('--augment', action='store_true', help='augmented inference') - parser.add_argument('--verbose', action='store_true', help='report mAP by class') - parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') - parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt') - parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') - parser.add_argument('--save-json', action='store_true', help='save a cocoapi-compatible JSON results file') - parser.add_argument('--project', default='runs/test', help='save to project/name') - parser.add_argument('--name', default='exp', help='save to project/name') - parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') - opt = parser.parse_args() - opt.save_json |= opt.data.endswith('coco.yaml') - opt.data = check_file(opt.data) # check file - print(opt) - check_requirements(exclude=('tensorboard', 'pycocotools', 'thop')) - - if opt.task in ('train', 'val', 'test'): # run normally - test(opt.data, - opt.weights, - opt.batch_size, - opt.img_size, - opt.conf_thres, - opt.iou_thres, - opt.save_json, - opt.single_cls, - opt.augment, - opt.verbose, - save_txt=opt.save_txt | opt.save_hybrid, - save_hybrid=opt.save_hybrid, - save_conf=opt.save_conf, - opt=opt - ) - - elif opt.task == 'speed': # speed benchmarks - for w in opt.weights: - test(opt.data, w, opt.batch_size, opt.img_size, 0.25, 0.45, save_json=False, plots=False, opt=opt) - - elif opt.task == 'study': # run over a range of settings and save/plot - # python test.py --task study --data coco.yaml --iou 0.7 --weights yolov3.pt yolov3-spp.pt yolov3-tiny.pt - x = list(range(256, 1536 + 128, 128)) # x axis (image sizes) - for w in opt.weights: - f = f'study_{Path(opt.data).stem}_{Path(w).stem}.txt' # filename to save to - y = [] # y axis - for i in x: # img-size - print(f'\nRunning {f} point {i}...') - r, _, t = test(opt.data, w, opt.batch_size, i, opt.conf_thres, opt.iou_thres, opt.save_json, - plots=False, opt=opt) - y.append(r + t) # results and times - np.savetxt(f, y, fmt='%10.4g') # save - os.system('zip -r study.zip study_*.txt') - plot_study_txt(x=x) # plot diff --git a/train.py b/train.py index 8e70ae38d4..cfe80ecc74 100644 --- a/train.py +++ b/train.py @@ -1,40 +1,55 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Train a model on a custom dataset + +Usage: + $ python path/to/train.py --data coco128.yaml --weights yolov3.pt --img 640 +""" import argparse -import logging import math import os import random +import sys import time from copy import deepcopy +from datetime import datetime from pathlib import Path -from threading import Thread import numpy as np +import torch import torch.distributed as dist import torch.nn as nn -import torch.nn.functional as F -import torch.optim as optim -import torch.optim.lr_scheduler as lr_scheduler -import torch.utils.data import yaml from torch.cuda import amp from torch.nn.parallel import DistributedDataParallel as DDP -from torch.utils.tensorboard import SummaryWriter +from torch.optim import SGD, Adam, lr_scheduler from tqdm import tqdm from pathlib import Path -import test # import test.py to get mAP after each epoch +FILE = Path(__file__).resolve() +ROOT = FILE.parents[0] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH +ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative + +import val # for end-of-epoch mAP from models.experimental import attempt_load from models.yolo import Model from utils.autoanchor import check_anchors +from utils.autobatch import check_train_batch_size +from utils.callbacks import Callbacks from utils.datasets import create_dataloader -from utils.general import labels_to_class_weights, increment_path, labels_to_image_weights, init_seeds, \ - fitness, strip_optimizer, get_latest_run, check_dataset, check_file, check_git_status, check_img_size, \ - check_requirements, print_mutation, set_logging, one_cycle, colorstr -from utils.google_utils import attempt_download +from utils.downloads import attempt_download +from utils.general import (LOGGER, NCOLS, check_dataset, check_file, check_git_status, check_img_size, + check_requirements, check_suffix, check_yaml, colorstr, get_latest_run, increment_path, + init_seeds, intersect_dicts, labels_to_class_weights, labels_to_image_weights, methods, + one_cycle, print_args, print_mutation, strip_optimizer) +from utils.loggers import Loggers +from utils.loggers.wandb.wandb_utils import check_wandb_resume from utils.loss import ComputeLoss -from utils.plots import plot_images, plot_labels, plot_results, plot_evolution -from utils.torch_utils import ModelEMA, select_device, intersect_dicts, torch_distributed_zero_first, de_parallel -from utils.wandb_logging.wandb_utils import WandbLogger, check_wandb_resume +from utils.metrics import fitness +from utils.plots import plot_evolve, plot_labels +from utils.torch_utils import EarlyStopping, ModelEMA, de_parallel, select_device, torch_distributed_zero_first ################# # Azure ML Logger @@ -47,119 +62,132 @@ ################# -logger = logging.getLogger(__name__) +LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html +RANK = int(os.getenv('RANK', -1)) +WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) -def train(hyp, opt, device, tb_writer=None): - logger.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items())) - save_dir, epochs, batch_size, total_batch_size, weights, rank = \ - Path(opt.save_dir), opt.epochs, opt.batch_size, opt.total_batch_size, opt.weights, opt.global_rank +def train(hyp, # path/to/hyp.yaml or hyp dictionary + opt, + device, + callbacks + ): + save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze, = \ + Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \ + opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze best_pt = Path(os.getcwd()).name + "_best.pt" last_pt = Path(os.getcwd()).name + "_last.pt" # Directories - wdir = save_dir / 'weights' - wdir.mkdir(parents=True, exist_ok=True) # make dir - last = wdir / last_pt - best = wdir / best_pt - results_file = save_dir / 'results.txt' + w = save_dir / 'weights' # weights dir + (w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir + last, best = w / 'last.pt', w / 'best.pt' + + # Hyperparameters + if isinstance(hyp, str): + with open(hyp, errors='ignore') as f: + hyp = yaml.safe_load(f) # load hyps dict + LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items())) # Save run settings with open(save_dir / 'hyp.yaml', 'w') as f: yaml.safe_dump(hyp, f, sort_keys=False) with open(save_dir / 'opt.yaml', 'w') as f: yaml.safe_dump(vars(opt), f, sort_keys=False) - - # Configure - plots = not opt.evolve # create plots + data_dict = None + + # Loggers + if RANK in [-1, 0]: + loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance + if loggers.wandb: + data_dict = loggers.wandb.data_dict + if resume: + weights, epochs, hyp = opt.weights, opt.epochs, opt.hyp + + # Register actions + for k in methods(loggers): + callbacks.register_action(k, callback=getattr(loggers, k)) + + # Config + plots = not evolve # create plots cuda = device.type != 'cpu' - init_seeds(2 + rank) - with open(opt.data) as f: - data_dict = yaml.safe_load(f) # data dict - - # Logging- Doing this before checking the dataset. Might update data_dict - loggers = {'wandb': None} # loggers dict - if rank in [-1, 0]: - opt.hyp = hyp # add hyperparameters - run_id = torch.load(weights).get('wandb_id') if weights.endswith('.pt') and os.path.isfile(weights) else None - wandb_logger = WandbLogger(opt, save_dir.stem, run_id, data_dict) - loggers['wandb'] = wandb_logger.wandb - data_dict = wandb_logger.data_dict - if wandb_logger.wandb: - weights, epochs, hyp = opt.weights, opt.epochs, opt.hyp # WandbLogger might update weights, epochs if resuming - - nc = 1 if opt.single_cls else int(data_dict['nc']) # number of classes - names = ['item'] if opt.single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names - assert len(names) == nc, '%g names found for nc=%g dataset in %s' % (len(names), nc, opt.data) # check - is_coco = opt.data.endswith('coco.yaml') and nc == 80 # COCO dataset + init_seeds(1 + RANK) + with torch_distributed_zero_first(LOCAL_RANK): + data_dict = data_dict or check_dataset(data) # check if None + train_path, val_path = data_dict['train'], data_dict['val'] + nc = 1 if single_cls else int(data_dict['nc']) # number of classes + names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names + assert len(names) == nc, f'{len(names)} names found for nc={nc} dataset in {data}' # check + is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset # Model + check_suffix(weights, '.pt') # check weights pretrained = weights.endswith('.pt') if pretrained: - with torch_distributed_zero_first(rank): + with torch_distributed_zero_first(LOCAL_RANK): weights = attempt_download(weights) # download if not found locally ckpt = torch.load(weights, map_location=device) # load checkpoint - model = Model(opt.cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create - exclude = ['anchor'] if (opt.cfg or hyp.get('anchors')) and not opt.resume else [] # exclude keys - state_dict = ckpt['model'].float().state_dict() # to FP32 - state_dict = intersect_dicts(state_dict, model.state_dict(), exclude=exclude) # intersect - model.load_state_dict(state_dict, strict=False) # load - logger.info('Transferred %g/%g items from %s' % (len(state_dict), len(model.state_dict()), weights)) # report + model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create + exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys + csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 + csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect + model.load_state_dict(csd, strict=False) # load + LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report else: - model = Model(opt.cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create - with torch_distributed_zero_first(rank): - check_dataset(data_dict) # check - train_path = data_dict['train'] - test_path = data_dict['val'] + model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create # Freeze - if opt.transfer_learning: - freeze = ['model.%s.' % x for x in range(13)] - else: - freeze = [] + freeze = [f'model.{x}.' for x in range(freeze)] # layers to freeze for k, v in model.named_parameters(): v.requires_grad = True # train all layers if any(x in k for x in freeze): - print('freezing %s' % k) + LOGGER.info(f'freezing {k}') v.requires_grad = False + # Image size + gs = max(int(model.stride.max()), 32) # grid size (max stride) + imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2) # verify imgsz is gs-multiple + + # Batch size + if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size + batch_size = check_train_batch_size(model, imgsz) + # Optimizer nbs = 64 # nominal batch size - accumulate = max(round(nbs / total_batch_size), 1) # accumulate loss before optimizing - hyp['weight_decay'] *= total_batch_size * accumulate / nbs # scale weight_decay - logger.info(f"Scaled weight_decay = {hyp['weight_decay']}") - - pg0, pg1, pg2 = [], [], [] # optimizer parameter groups - for k, v in model.named_modules(): - if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter): - pg2.append(v.bias) # biases - if isinstance(v, nn.BatchNorm2d): - pg0.append(v.weight) # no decay - elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter): - pg1.append(v.weight) # apply decay + accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing + hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay + LOGGER.info(f"Scaled weight_decay = {hyp['weight_decay']}") + + g0, g1, g2 = [], [], [] # optimizer parameter groups + for v in model.modules(): + if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter): # bias + g2.append(v.bias) + if isinstance(v, nn.BatchNorm2d): # weight (no decay) + g0.append(v.weight) + elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter): # weight (with decay) + g1.append(v.weight) if opt.adam: - optimizer = optim.Adam(pg0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999)) # adjust beta1 to momentum + optimizer = Adam(g0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999)) # adjust beta1 to momentum else: - optimizer = optim.SGD(pg0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True) + optimizer = SGD(g0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True) - optimizer.add_param_group({'params': pg1, 'weight_decay': hyp['weight_decay']}) # add pg1 with weight_decay - optimizer.add_param_group({'params': pg2}) # add pg2 (biases) - logger.info('Optimizer groups: %g .bias, %g conv.weight, %g other' % (len(pg2), len(pg1), len(pg0))) - del pg0, pg1, pg2 + optimizer.add_param_group({'params': g1, 'weight_decay': hyp['weight_decay']}) # add g1 with weight_decay + optimizer.add_param_group({'params': g2}) # add g2 (biases) + LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__} with parameter groups " + f"{len(g0)} weight, {len(g1)} weight (no decay), {len(g2)} bias") + del g0, g1, g2 - # Scheduler https://arxiv.org/pdf/1812.01187.pdf - # https://pytorch.org/docs/stable/_modules/torch/optim/lr_scheduler.html#OneCycleLR + # Scheduler if opt.linear_lr: lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp['lrf']) + hyp['lrf'] # linear else: lf = one_cycle(1, hyp['lrf'], epochs) # cosine 1->hyp['lrf'] - scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) - # plot_lr_scheduler(optimizer, scheduler, epochs) + scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs) # EMA - ema = ModelEMA(model) if rank in [-1, 0] else None + ema = ModelEMA(model) if RANK in [-1, 0] else None # Resume start_epoch, best_fitness = 0, 0.0 @@ -174,80 +202,70 @@ def train(hyp, opt, device, tb_writer=None): ema.ema.load_state_dict(ckpt['ema'].float().state_dict()) ema.updates = ckpt['updates'] - # Results - if ckpt.get('training_results') is not None: - results_file.write_text(ckpt['training_results']) # write results.txt - # Epochs start_epoch = ckpt['epoch'] + 1 - if opt.resume: - assert start_epoch > 0, '%s training to %g epochs is finished, nothing to resume.' % (weights, epochs) + if resume: + assert start_epoch > 0, f'{weights} training to {epochs} epochs is finished, nothing to resume.' if epochs < start_epoch: - logger.info('%s has been trained for %g epochs. Fine-tuning for %g additional epochs.' % - (weights, ckpt['epoch'], epochs)) + LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.") epochs += ckpt['epoch'] # finetune additional epochs - del ckpt, state_dict - - # Image sizes - gs = max(int(model.stride.max()), 32) # grid size (max stride) - nl = model.model[-1].nl # number of detection layers (used for scaling hyp['obj']) - imgsz, imgsz_test = [check_img_size(x, gs) for x in opt.img_size] # verify imgsz are gs-multiples + del ckpt, csd # DP mode - if cuda and rank == -1 and torch.cuda.device_count() > 1: + if cuda and RANK == -1 and torch.cuda.device_count() > 1: + LOGGER.warning('WARNING: DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n' + 'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.') model = torch.nn.DataParallel(model) # SyncBatchNorm - if opt.sync_bn and cuda and rank != -1: + if opt.sync_bn and cuda and RANK != -1: model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device) - logger.info('Using SyncBatchNorm()') + LOGGER.info('Using SyncBatchNorm()') # Trainloader - dataloader, dataset = create_dataloader(train_path, imgsz, batch_size, gs, opt, - hyp=hyp, augment=True, cache=opt.cache_images, rect=opt.rect, rank=rank, - world_size=opt.world_size, workers=opt.workers, - image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr('train: ')) - mlc = np.concatenate(dataset.labels, 0)[:, 0].max() # max label class - nb = len(dataloader) # number of batches - assert mlc < nc, 'Label class %g exceeds nc=%g in %s. Possible class labels are 0-%g' % (mlc, nc, opt.data, nc - 1) + train_loader, dataset = create_dataloader(train_path, imgsz, batch_size // WORLD_SIZE, gs, single_cls, + hyp=hyp, augment=True, cache=opt.cache, rect=opt.rect, rank=LOCAL_RANK, + workers=workers, image_weights=opt.image_weights, quad=opt.quad, + prefix=colorstr('train: '), shuffle=True) + mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max()) # max label class + nb = len(train_loader) # number of batches + assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}' # Process 0 - if rank in [-1, 0]: - testloader = create_dataloader(test_path, imgsz_test, batch_size * 2, gs, opt, # testloader - hyp=hyp, cache=opt.cache_images and not opt.notest, rect=True, rank=-1, - world_size=opt.world_size, workers=opt.workers, - pad=0.5, prefix=colorstr('val: '))[0] + if RANK in [-1, 0]: + val_loader = create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls, + hyp=hyp, cache=None if noval else opt.cache, rect=True, rank=-1, + workers=workers, pad=0.5, + prefix=colorstr('val: '))[0] - if not opt.resume: + if not resume: labels = np.concatenate(dataset.labels, 0) - c = torch.tensor(labels[:, 0]) # classes + # c = torch.tensor(labels[:, 0]) # classes # cf = torch.bincount(c.long(), minlength=nc) + 1. # frequency # model._initialize_biases(cf.to(device)) if plots: - plot_labels(labels, names, save_dir, loggers) - if tb_writer: - tb_writer.add_histogram('classes', c, 0) + plot_labels(labels, names, save_dir) # Anchors if not opt.noautoanchor: check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) model.half().float() # pre-reduce anchor precision + callbacks.run('on_pretrain_routine_end') + # DDP mode - if cuda and rank != -1: - model = DDP(model, device_ids=[opt.local_rank], output_device=opt.local_rank, - # nn.MultiheadAttention incompatibility with DDP https://github.com/pytorch/pytorch/issues/26698 - find_unused_parameters=any(isinstance(layer, nn.MultiheadAttention) for layer in model.modules())) + if cuda and RANK != -1: + model = DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK) # Model parameters - hyp['box'] *= 3. / nl # scale to layers - hyp['cls'] *= nc / 80. * 3. / nl # scale to classes and layers - hyp['obj'] *= (imgsz / 640) ** 2 * 3. / nl # scale to image size and layers + nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps) + hyp['box'] *= 3 / nl # scale to layers + hyp['cls'] *= nc / 80 * 3 / nl # scale to classes and layers + hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers hyp['label_smoothing'] = opt.label_smoothing model.nc = nc # attach number of classes to model model.hyp = hyp # attach hyperparameters to model - model.gr = 1.0 # iou loss ratio (obj_loss = 1.0 or iou) model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights model.names = names @@ -255,53 +273,47 @@ def train(hyp, opt, device, tb_writer=None): t0 = time.time() nw = max(round(hyp['warmup_epochs'] * nb), 1000) # number of warmup iterations, max(3 epochs, 1k iterations) # nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training + last_opt_step = -1 maps = np.zeros(nc) # mAP per class results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) scheduler.last_epoch = start_epoch - 1 # do not move scaler = amp.GradScaler(enabled=cuda) + stopper = EarlyStopping(patience=opt.patience) compute_loss = ComputeLoss(model) # init loss class - logger.info(f'Image sizes {imgsz} train, {imgsz_test} test\n' - f'Using {dataloader.num_workers} dataloader workers\n' - f'Logging results to {save_dir}\n' + LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n' + f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n' + f"Logging results to {colorstr('bold', save_dir)}\n" f'Starting training for {epochs} epochs...') for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------ model.train() - # Update image weights (optional) + # Update image weights (optional, single-GPU only) if opt.image_weights: - # Generate indices - if rank in [-1, 0]: - cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights - iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights - dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx - # Broadcast if DDP - if rank != -1: - indices = (torch.tensor(dataset.indices) if rank == 0 else torch.zeros(dataset.n)).int() - dist.broadcast(indices, 0) - if rank != 0: - dataset.indices = indices.cpu().numpy() - - # Update mosaic border + cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights + iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights + dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx + + # Update mosaic border (optional) # b = int(random.uniform(0.25 * imgsz, 0.75 * imgsz + gs) // gs * gs) # dataset.mosaic_border = [b - imgsz, -b] # height, width borders - mloss = torch.zeros(4, device=device) # mean losses - if rank != -1: - dataloader.sampler.set_epoch(epoch) - pbar = enumerate(dataloader) - logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'total', 'labels', 'img_size')) - if rank in [-1, 0]: - pbar = tqdm(pbar, total=nb) # progress bar + mloss = torch.zeros(3, device=device) # mean losses + if RANK != -1: + train_loader.sampler.set_epoch(epoch) + pbar = enumerate(train_loader) + LOGGER.info(('\n' + '%10s' * 7) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'labels', 'img_size')) + if RANK in [-1, 0]: + pbar = tqdm(pbar, total=nb, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar optimizer.zero_grad() for i, (imgs, targets, paths, _) in pbar: # batch ------------------------------------------------------------- ni = i + nb * epoch # number integrated batches (since train start) - imgs = imgs.to(device, non_blocking=True).float() / 255.0 # uint8 to float32, 0-255 to 0.0-1.0 + imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0 # Warmup if ni <= nw: xi = [0, nw] # x interp - # model.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou) - accumulate = max(1, np.interp(ni, xi, [1, nbs / total_batch_size]).round()) + # compute_loss.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou) + accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round()) for j, x in enumerate(optimizer.param_groups): # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)]) @@ -314,14 +326,14 @@ def train(hyp, opt, device, tb_writer=None): sf = sz / max(imgs.shape[2:]) # scale factor if sf != 1: ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple) - imgs = F.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) + imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) # Forward with amp.autocast(enabled=cuda): pred = model(imgs) # forward loss, loss_items = compute_loss(pred, targets.to(device)) # loss scaled by batch_size - if rank != -1: - loss *= opt.world_size # gradient averaged between devices in DDP mode + if RANK != -1: + loss *= WORLD_SIZE # gradient averaged between devices in DDP mode if opt.quad: loss *= 4. @@ -329,196 +341,170 @@ def train(hyp, opt, device, tb_writer=None): scaler.scale(loss).backward() # Optimize - if ni % accumulate == 0: + if ni - last_opt_step >= accumulate: scaler.step(optimizer) # optimizer.step scaler.update() optimizer.zero_grad() if ema: ema.update(model) + last_opt_step = ni - # Print - if rank in [-1, 0]: + # Log + if RANK in [-1, 0]: mloss = (mloss * i + loss_items) / (i + 1) # update mean losses - run.log('mean_train_loss', np.float(mloss[3])) - mem = '%.3gG' % (torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0) # (GB) - s = ('%10s' * 2 + '%10.4g' * 6) % ( - '%g/%g' % (epoch, epochs - 1), mem, *mloss, targets.shape[0], imgs.shape[-1]) - pbar.set_description(s) - - # Plot - if plots and ni < 3: - f = save_dir / f'train_batch{ni}.jpg' # filename - Thread(target=plot_images, args=(imgs, targets, paths, f), daemon=True).start() - if tb_writer: - tb_writer.add_graph(torch.jit.trace(de_parallel(model), imgs, strict=False), []) # model graph - # tb_writer.add_image(f, result, dataformats='HWC', global_step=epoch) - elif plots and ni == 10 and wandb_logger.wandb: - wandb_logger.log({"Mosaics": [wandb_logger.wandb.Image(str(x), caption=x.name) for x in - save_dir.glob('train*.jpg') if x.exists()]}) - + mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB) + pbar.set_description(('%10s' * 2 + '%10.4g' * 5) % ( + f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1])) + callbacks.run('on_train_batch_end', ni, model, imgs, targets, paths, plots, opt.sync_bn) # end batch ------------------------------------------------------------------------------------------------ - # end epoch ---------------------------------------------------------------------------------------------------- # Scheduler - lr = [x['lr'] for x in optimizer.param_groups] # for tensorboard + lr = [x['lr'] for x in optimizer.param_groups] # for loggers scheduler.step() - # DDP process 0 or single-GPU - if rank in [-1, 0]: + if RANK in [-1, 0]: # mAP - ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'gr', 'names', 'stride', 'class_weights']) - final_epoch = epoch + 1 == epochs - if not opt.notest or final_epoch: # Calculate mAP - wandb_logger.current_epoch = epoch + 1 - results, maps, times = test.test(data_dict, - batch_size=batch_size * 2, - imgsz=imgsz_test, - model=ema.ema, - single_cls=opt.single_cls, - dataloader=testloader, - save_dir=save_dir, - save_json=is_coco and final_epoch, - verbose=nc < 50 and final_epoch, - plots=plots and final_epoch, - wandb_logger=wandb_logger, - compute_loss=compute_loss, - is_coco=is_coco) - run.log('precision', results[0]) - run.log('recall', results[1]) - run.log('mAP@.5-.95', results[3]) - - # Write - with open(results_file, 'a') as f: - f.write(s + '%10.4g' * 7 % results + '\n') # append metrics, val_loss - - # Log - tags = ['train/box_loss', 'train/obj_loss', 'train/cls_loss', # train loss - 'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', - 'val/box_loss', 'val/obj_loss', 'val/cls_loss', # val loss - 'x/lr0', 'x/lr1', 'x/lr2'] # params - for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags): - if tb_writer: - tb_writer.add_scalar(tag, x, epoch) # tensorboard - if wandb_logger.wandb: - wandb_logger.log({tag: x}) # W&B + callbacks.run('on_train_epoch_end', epoch=epoch) + ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) + final_epoch = (epoch + 1 == epochs) or stopper.possible_stop + if not noval or final_epoch: # Calculate mAP + results, maps, _ = val.run(data_dict, + batch_size=batch_size // WORLD_SIZE * 2, + imgsz=imgsz, + model=ema.ema, + single_cls=single_cls, + dataloader=val_loader, + save_dir=save_dir, + plots=False, + callbacks=callbacks, + compute_loss=compute_loss) # Update best mAP fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] if fi > best_fitness: best_fitness = fi - wandb_logger.end_epoch(best_result=best_fitness == fi) + log_vals = list(mloss) + list(results) + lr + callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) # Save model - if (not opt.nosave) or (final_epoch and not opt.evolve): # if save + if (not nosave) or (final_epoch and not evolve): # if save ckpt = {'epoch': epoch, 'best_fitness': best_fitness, - 'training_results': results_file.read_text(), 'model': deepcopy(de_parallel(model)).half(), 'ema': deepcopy(ema.ema).half(), 'updates': ema.updates, 'optimizer': optimizer.state_dict(), - 'wandb_id': wandb_logger.wandb_run.id if wandb_logger.wandb else None} + 'wandb_id': loggers.wandb.wandb_run.id if loggers.wandb else None, + 'date': datetime.now().isoformat()} # Save last, best and delete torch.save(ckpt, last) if best_fitness == fi: torch.save(ckpt, best) - if wandb_logger.wandb: - if ((epoch + 1) % opt.save_period == 0 and not final_epoch) and opt.save_period != -1: - wandb_logger.log_model( - last.parent, opt, epoch, fi, best_model=best_fitness == fi) + if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0): + torch.save(ckpt, w / f'epoch{epoch}.pt') del ckpt + callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) + + # Stop Single-GPU + if RANK == -1 and stopper(epoch=epoch, fitness=fi): + break + + # Stop DDP TODO: known issues shttps://github.com/ultralytics/yolov5/pull/4576 + # stop = stopper(epoch=epoch, fitness=fi) + # if RANK == 0: + # dist.broadcast_object_list([stop], 0) # broadcast 'stop' to all ranks + + # Stop DPP + # with torch_distributed_zero_first(RANK): + # if stop: + # break # must break all DDP ranks # end epoch ---------------------------------------------------------------------------------------------------- - # end training - if rank in [-1, 0]: - logger.info(f'{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.\n') - if plots: - plot_results(save_dir=save_dir) # save as results.png - if wandb_logger.wandb: - files = ['results.png', 'confusion_matrix.png', *[f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R')]] - wandb_logger.log({"Results": [wandb_logger.wandb.Image(str(save_dir / f), caption=f) for f in files - if (save_dir / f).exists()]}) - - if not opt.evolve: - if is_coco: # COCO dataset - for m in [last, best] if best.exists() else [last]: # speed, mAP tests - results, _, _ = test.test(opt.data, - batch_size=batch_size * 2, - imgsz=imgsz_test, - conf_thres=0.001, - iou_thres=0.7, - model=attempt_load(m, device).half(), - single_cls=opt.single_cls, - dataloader=testloader, - save_dir=save_dir, - save_json=True, - plots=False, - is_coco=is_coco) - - # Strip optimizers - for f in last, best: - if f.exists(): - strip_optimizer(f) # strip optimizers - if wandb_logger.wandb: # Log the stripped model - wandb_logger.wandb.log_artifact(str(best if best.exists() else last), type='model', - name='run_' + wandb_logger.wandb_run.id + '_model', - aliases=['latest', 'best', 'stripped']) - wandb_logger.finish_run() - else: - dist.destroy_process_group() + # end training ----------------------------------------------------------------------------------------------------- + if RANK in [-1, 0]: + LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.') + for f in last, best: + if f.exists(): + strip_optimizer(f) # strip optimizers + if f is best: + LOGGER.info(f'\nValidating {f}...') + results, _, _ = val.run(data_dict, + batch_size=batch_size // WORLD_SIZE * 2, + imgsz=imgsz, + model=attempt_load(f, device).half(), + iou_thres=0.65 if is_coco else 0.60, # best pycocotools results at 0.65 + single_cls=single_cls, + dataloader=val_loader, + save_dir=save_dir, + save_json=is_coco, + verbose=True, + plots=True, + callbacks=callbacks, + compute_loss=compute_loss) # val best model with plots + if is_coco: + callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi) + + callbacks.run('on_train_end', last, best, plots, epoch, results) + LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}") + torch.cuda.empty_cache() return results -if __name__ == '__main__': +def parse_opt(known=False): parser = argparse.ArgumentParser() - parser.add_argument('--weights', type=str, default='yolov3.pt', help='initial weights path') + parser.add_argument('--weights', type=str, default=ROOT / 'yolov3.pt', help='initial weights path') parser.add_argument('--cfg', type=str, default='', help='model.yaml path') parser.add_argument('--yolo-data-dir', type=str, help='path to YOLO data') parser.add_argument('--num_classes', type=int, help='number of classes to train the model on') parser.add_argument('--class_labels', nargs='*', help='Labels of classes to train on') parser.add_argument('--hyp', type=str, default='data/hyp.scratch.yaml', help='hyperparameters path') parser.add_argument('--epochs', type=int, default=300) - parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs') - parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='[train, test] image sizes') + parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch') + parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)') parser.add_argument('--rect', action='store_true', help='rectangular training') parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training') parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') - parser.add_argument('--notest', action='store_true', help='only test final epoch') + parser.add_argument('--noval', action='store_true', help='only validate final epoch') parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check') - parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters') + parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations') parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') - parser.add_argument('--cache-images', action='store_true', help='cache images for faster training') + parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"') parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class') parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer') parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode') - parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify') - parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers') - parser.add_argument('--project', default='runs/train', help='save to project/name') - parser.add_argument('--entity', default=None, help='W&B entity') + parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') + parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name') parser.add_argument('--name', default='exp', help='save to project/name') - parser.add_argument('--transfer-learning', action='store_true', help='freeze backbone network and use custom weights') parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument('--quad', action='store_true', help='quad dataloader') parser.add_argument('--linear-lr', action='store_true', help='linear LR') parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon') - parser.add_argument('--upload_dataset', action='store_true', help='Upload dataset as W&B artifact table') - parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval for W&B') - parser.add_argument('--save_period', type=int, default=-1, help='Log model after every "save_period" epoch') - parser.add_argument('--artifact_alias', type=str, default="latest", help='version of dataset artifact to be used') - opt = parser.parse_args() - - # Set DDP variables - opt.world_size = int(os.environ['WORLD_SIZE']) if 'WORLD_SIZE' in os.environ else 1 - opt.global_rank = int(os.environ['RANK']) if 'RANK' in os.environ else -1 - set_logging(opt.global_rank) - if opt.global_rank in [-1, 0]: + parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)') + parser.add_argument('--freeze', type=int, default=0, help='Number of layers to freeze. backbone=10, all=24') + parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') + parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify') + + # Weights & Biases arguments + parser.add_argument('--entity', default=None, help='W&B: Entity') + parser.add_argument('--upload_dataset', action='store_true', help='W&B: Upload dataset as artifact table') + parser.add_argument('--bbox_interval', type=int, default=-1, help='W&B: Set bounding-box image logging interval') + parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use') + + opt = parser.parse_known_args()[0] if known else parser.parse_args() + return opt + + +def main(opt, callbacks=Callbacks()): + # Checks + if RANK in [-1, 0]: + print_args(FILE.stem, opt) check_git_status() - check_requirements(exclude=('pycocotools', 'thop')) + check_requirements(exclude=['thop']) # Retrieve Azure dataset in training script data_folder = opt.yolo_data_dir @@ -544,49 +530,39 @@ def train(hyp, opt, device, tb_writer=None): ############################ # Resume - wandb_run = check_wandb_resume(opt) - if opt.resume and not wandb_run: # resume an interrupted run + if opt.resume and not check_wandb_resume(opt) and not opt.evolve: # resume an interrupted run ckpt = opt.resume if isinstance(opt.resume, str) else get_latest_run() # specified or most recent path assert os.path.isfile(ckpt), 'ERROR: --resume checkpoint does not exist' - apriori = opt.global_rank, opt.local_rank - with open(Path(ckpt).parent.parent / 'opt.yaml') as f: + with open(Path(ckpt).parent.parent / 'opt.yaml', errors='ignore') as f: opt = argparse.Namespace(**yaml.safe_load(f)) # replace - opt.cfg, opt.weights, opt.resume, opt.batch_size, opt.global_rank, opt.local_rank = \ - '', ckpt, True, opt.total_batch_size, *apriori # reinstate - logger.info('Resuming training from %s' % ckpt) + opt.cfg, opt.weights, opt.resume = '', ckpt, True # reinstate + LOGGER.info(f'Resuming training from {ckpt}') else: - # opt.hyp = opt.hyp or ('hyp.finetune.yaml' if opt.weights else 'hyp.scratch.yaml') - opt.data, opt.cfg, opt.hyp = check_file(opt.data), check_file(opt.cfg), check_file(opt.hyp) # check files + opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = \ + check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project) # checks assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified' - opt.img_size.extend([opt.img_size[-1]] * (2 - len(opt.img_size))) # extend to 2 sizes (train, test) - opt.name = 'evolve' if opt.evolve else opt.name - opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok | opt.evolve)) + if opt.evolve: + opt.project = str(ROOT / 'runs/evolve') + opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume + opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) # DDP mode - opt.total_batch_size = opt.batch_size device = select_device(opt.device, batch_size=opt.batch_size) - if opt.local_rank != -1: - assert torch.cuda.device_count() > opt.local_rank - torch.cuda.set_device(opt.local_rank) - device = torch.device('cuda', opt.local_rank) - dist.init_process_group(backend='nccl', init_method='env://') # distributed backend - assert opt.batch_size % opt.world_size == 0, '--batch-size must be multiple of CUDA device count' + if LOCAL_RANK != -1: + assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command' + assert opt.batch_size % WORLD_SIZE == 0, '--batch-size must be multiple of CUDA device count' assert not opt.image_weights, '--image-weights argument is not compatible with DDP training' - opt.batch_size = opt.total_batch_size // opt.world_size - - # Hyperparameters - with open(opt.hyp) as f: - hyp = yaml.safe_load(f) # load hyps + assert not opt.evolve, '--evolve argument is not compatible with DDP training' + torch.cuda.set_device(LOCAL_RANK) + device = torch.device('cuda', LOCAL_RANK) + dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo") # Train - logger.info(opt) if not opt.evolve: - tb_writer = None # init loggers - if opt.global_rank in [-1, 0]: - prefix = colorstr('tensorboard: ') - logger.info(f"{prefix}Start with 'tensorboard --logdir {opt.project}', view at http://localhost:6006/") - tb_writer = SummaryWriter(opt.save_dir) # Tensorboard - train(hyp, opt, device, tb_writer) + train(opt.hyp, opt, device, callbacks) + if WORLD_SIZE > 1 and RANK == 0: + LOGGER.info('Destroying process group... ') + dist.destroy_process_group() # Evolve hyperparameters (optional) else: @@ -618,23 +594,27 @@ def train(hyp, opt, device, tb_writer=None): 'flipud': (1, 0.0, 1.0), # image flip up-down (probability) 'fliplr': (0, 0.0, 1.0), # image flip left-right (probability) 'mosaic': (1, 0.0, 1.0), # image mixup (probability) - 'mixup': (1, 0.0, 1.0)} # image mixup (probability) - - assert opt.local_rank == -1, 'DDP mode not implemented for --evolve' - opt.notest, opt.nosave = True, True # only test/save final epoch + 'mixup': (1, 0.0, 1.0), # image mixup (probability) + 'copy_paste': (1, 0.0, 1.0)} # segment copy-paste (probability) + + with open(opt.hyp, errors='ignore') as f: + hyp = yaml.safe_load(f) # load hyps dict + if 'anchors' not in hyp: # anchors commented in hyp.yaml + hyp['anchors'] = 3 + opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch # ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices - yaml_file = Path(opt.save_dir) / 'hyp_evolved.yaml' # save best result here + evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv' if opt.bucket: - os.system('gsutil cp gs://%s/evolve.txt .' % opt.bucket) # download evolve.txt if exists + os.system(f'gsutil cp gs://{opt.bucket}/evolve.csv {save_dir}') # download evolve.csv if exists - for _ in range(300): # generations to evolve - if Path('evolve.txt').exists(): # if evolve.txt exists: select best hyps and mutate + for _ in range(opt.evolve): # generations to evolve + if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate # Select parent(s) parent = 'single' # parent selection method: 'single' or 'weighted' - x = np.loadtxt('evolve.txt', ndmin=2) + x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1) n = min(5, len(x)) # number of previous results to consider x = x[np.argsort(-fitness(x))][:n] # top n mutations - w = fitness(x) - fitness(x).min() # weights + w = fitness(x) - fitness(x).min() + 1E-6 # weights (sum > 0) if parent == 'single' or len(x) == 1: # x = x[random.randint(0, n - 1)] # random selection x = x[random.choices(range(n), weights=w)[0]] # weighted selection @@ -645,7 +625,7 @@ def train(hyp, opt, device, tb_writer=None): mp, s = 0.8, 0.2 # mutation probability, sigma npr = np.random npr.seed(int(time.time())) - g = np.array([x[0] for x in meta.values()]) # gains 0-1 + g = np.array([meta[k][0] for k in hyp.keys()]) # gains 0-1 ng = len(meta) v = np.ones(ng) while all(v == 1): # mutate until a change occurs (prevent duplicates) @@ -660,12 +640,26 @@ def train(hyp, opt, device, tb_writer=None): hyp[k] = round(hyp[k], 5) # significant digits # Train mutation - results = train(hyp.copy(), opt, device) + results = train(hyp.copy(), opt, device, callbacks) # Write mutation results - print_mutation(hyp.copy(), results, yaml_file, opt.bucket) + print_mutation(results, hyp.copy(), save_dir, opt.bucket) # Plot results - plot_evolution(yaml_file) - print(f'Hyperparameter evolution complete. Best results saved as: {yaml_file}\n' - f'Command to train a new model with these hyperparameters: $ python train.py --hyp {yaml_file}') + plot_evolve(evolve_csv) + LOGGER.info(f'Hyperparameter evolution finished\n' + f"Results saved to {colorstr('bold', save_dir)}\n" + f'Use best hyperparameters example: $ python train.py --hyp {evolve_yaml}') + + +def run(**kwargs): + # Usage: import train; train.run(data='coco128.yaml', imgsz=320, weights='yolov3.pt') + opt = parse_opt(True) + for k, v in kwargs.items(): + setattr(opt, k, v) + main(opt) + + +if __name__ == "__main__": + opt = parse_opt() + main(opt) diff --git a/tutorial.ipynb b/tutorial.ipynb index 617c5c5238..828e434f6b 100644 --- a/tutorial.ipynb +++ b/tutorial.ipynb @@ -6,7 +6,6 @@ "name": "YOLOv3 Tutorial", "provenance": [], "collapsed_sections": [], - "toc_visible": true, "include_colab_link": true }, "kernelspec": { @@ -16,9 +15,10 @@ "accelerator": "GPU", "widgets": { "application/vnd.jupyter.widget-state+json": { - "355d9ee3dfc4487ebcae3b66ddbedce1": { + "eeda9d6850e8406f9bbc5b06051b3710": { "model_module": "@jupyter-widgets/controls", "model_name": "HBoxModel", + "model_module_version": "1.5.0", "state": { "_view_name": "HBoxView", "_dom_classes": [], @@ -28,17 +28,19 @@ "_view_count": null, "_view_module_version": "1.5.0", "box_style": "", - "layout": "IPY_MODEL_8209acd3185441e7b263eead5e8babdf", + "layout": "IPY_MODEL_1e823c45174a4216be7234a6cc5cfd99", "_model_module": "@jupyter-widgets/controls", "children": [ - "IPY_MODEL_b81d30356f7048b0abcba35bde811526", - "IPY_MODEL_7fcbf6b56f2e4b6dbf84e48465c96633" + "IPY_MODEL_cd8efd6c5de94ea8848a7d5b8766a4d6", + "IPY_MODEL_a4ec69c4697c4b0e84e6193be227f63e", + "IPY_MODEL_9a5694c133be46df8d2fe809b77c1c35" ] } }, - "8209acd3185441e7b263eead5e8babdf": { + "1e823c45174a4216be7234a6cc5cfd99": { "model_module": "@jupyter-widgets/base", "model_name": "LayoutModel", + "model_module_version": "1.2.0", "state": { "_view_name": "LayoutView", "grid_template_rows": null, @@ -87,118 +89,76 @@ "left": null } }, - "b81d30356f7048b0abcba35bde811526": { + "cd8efd6c5de94ea8848a7d5b8766a4d6": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_view_name": "HTMLView", + "style": "IPY_MODEL_d584167143f84a0484006dded3fd2620", + "_dom_classes": [], + "description": "", + "_model_name": "HTMLModel", + "placeholder": "​", + "_view_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "value": "100%", + "_view_count": null, + "_view_module_version": "1.5.0", + "description_tooltip": null, + "_model_module": "@jupyter-widgets/controls", + "layout": "IPY_MODEL_b9a25c0d425c4fe4b8cd51ae6a301b0d" + } + }, + "a4ec69c4697c4b0e84e6193be227f63e": { "model_module": "@jupyter-widgets/controls", "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", "state": { "_view_name": "ProgressView", - "style": "IPY_MODEL_6ee48f9f3af444a7b02ec2f074dec1f8", + "style": "IPY_MODEL_654525fe1ed34d5fbe1c36ed80ae1c1c", "_dom_classes": [], - "description": "100%", + "description": "", "_model_name": "FloatProgressModel", "bar_style": "success", - "max": 819257867, + "max": 818322941, "_view_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", - "value": 819257867, + "value": 818322941, "_view_count": null, "_view_module_version": "1.5.0", "orientation": "horizontal", "min": 0, "description_tooltip": null, "_model_module": "@jupyter-widgets/controls", - "layout": "IPY_MODEL_b7d819ed5f2f4e39a75a823792ab7249" + "layout": "IPY_MODEL_09544845070e47baafc5e37d45ff23e9" } }, - "7fcbf6b56f2e4b6dbf84e48465c96633": { + "9a5694c133be46df8d2fe809b77c1c35": { "model_module": "@jupyter-widgets/controls", "model_name": "HTMLModel", + "model_module_version": "1.5.0", "state": { "_view_name": "HTMLView", - "style": "IPY_MODEL_3af216dd7d024739b8168995800ed8be", + "style": "IPY_MODEL_1066f1d5b6104a3dae19f26269745bd0", "_dom_classes": [], "description": "", "_model_name": "HTMLModel", "placeholder": "​", "_view_module": "@jupyter-widgets/controls", "_model_module_version": "1.5.0", - "value": " 781M/781M [00:11<00:00, 71.1MB/s]", + "value": " 780M/780M [00:03<00:00, 200MB/s]", "_view_count": null, "_view_module_version": "1.5.0", "description_tooltip": null, "_model_module": "@jupyter-widgets/controls", - "layout": "IPY_MODEL_763141d8de8a498a92ffa66aafed0c5a" - } - }, - "6ee48f9f3af444a7b02ec2f074dec1f8": { - "model_module": "@jupyter-widgets/controls", - "model_name": "ProgressStyleModel", - "state": { - "_view_name": "StyleView", - "_model_name": "ProgressStyleModel", - "description_width": "initial", - "_view_module": "@jupyter-widgets/base", - "_model_module_version": "1.5.0", - "_view_count": null, - "_view_module_version": "1.2.0", - "bar_color": null, - "_model_module": "@jupyter-widgets/controls" - } - }, - "b7d819ed5f2f4e39a75a823792ab7249": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "state": { - "_view_name": "LayoutView", - "grid_template_rows": null, - "right": null, - "justify_content": null, - "_view_module": "@jupyter-widgets/base", - "overflow": null, - "_model_module_version": "1.2.0", - "_view_count": null, - "flex_flow": null, - "width": null, - "min_width": null, - "border": null, - "align_items": null, - "bottom": null, - "_model_module": "@jupyter-widgets/base", - "top": null, - "grid_column": null, - "overflow_y": null, - "overflow_x": null, - "grid_auto_flow": null, - "grid_area": null, - "grid_template_columns": null, - "flex": null, - "_model_name": "LayoutModel", - "justify_items": null, - "grid_row": null, - "max_height": null, - "align_content": null, - "visibility": null, - "align_self": null, - "height": null, - "min_height": null, - "padding": null, - "grid_auto_rows": null, - "grid_gap": null, - "max_width": null, - "order": null, - "_view_module_version": "1.2.0", - "grid_template_areas": null, - "object_position": null, - "object_fit": null, - "grid_auto_columns": null, - "margin": null, - "display": null, - "left": null + "layout": "IPY_MODEL_dd3a70e1ef4547ec8d3463749ce06285" } }, - "3af216dd7d024739b8168995800ed8be": { + "d584167143f84a0484006dded3fd2620": { "model_module": "@jupyter-widgets/controls", "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", "state": { "_view_name": "StyleView", "_model_name": "DescriptionStyleModel", @@ -210,80 +170,10 @@ "_model_module": "@jupyter-widgets/controls" } }, - "763141d8de8a498a92ffa66aafed0c5a": { - "model_module": "@jupyter-widgets/base", - "model_name": "LayoutModel", - "state": { - "_view_name": "LayoutView", - "grid_template_rows": null, - "right": null, - "justify_content": null, - "_view_module": "@jupyter-widgets/base", - "overflow": null, - "_model_module_version": "1.2.0", - "_view_count": null, - "flex_flow": null, - "width": null, - "min_width": null, - "border": null, - "align_items": null, - "bottom": null, - "_model_module": "@jupyter-widgets/base", - "top": null, - "grid_column": null, - "overflow_y": null, - "overflow_x": null, - "grid_auto_flow": null, - "grid_area": null, - "grid_template_columns": null, - "flex": null, - "_model_name": "LayoutModel", - "justify_items": null, - "grid_row": null, - "max_height": null, - "align_content": null, - "visibility": null, - "align_self": null, - "height": null, - "min_height": null, - "padding": null, - "grid_auto_rows": null, - "grid_gap": null, - "max_width": null, - "order": null, - "_view_module_version": "1.2.0", - "grid_template_areas": null, - "object_position": null, - "object_fit": null, - "grid_auto_columns": null, - "margin": null, - "display": null, - "left": null - } - }, - "0fffa335322b41658508e06aed0acbf0": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HBoxModel", - "state": { - "_view_name": "HBoxView", - "_dom_classes": [], - "_model_name": "HBoxModel", - "_view_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_view_count": null, - "_view_module_version": "1.5.0", - "box_style": "", - "layout": "IPY_MODEL_a354c6f80ce347e5a3ef64af87c0eccb", - "_model_module": "@jupyter-widgets/controls", - "children": [ - "IPY_MODEL_85823e71fea54c39bd11e2e972348836", - "IPY_MODEL_fb11acd663fa4e71b041d67310d045fd" - ] - } - }, - "a354c6f80ce347e5a3ef64af87c0eccb": { + "b9a25c0d425c4fe4b8cd51ae6a301b0d": { "model_module": "@jupyter-widgets/base", "model_name": "LayoutModel", + "model_module_version": "1.2.0", "state": { "_view_name": "LayoutView", "grid_template_rows": null, @@ -332,56 +222,14 @@ "left": null } }, - "85823e71fea54c39bd11e2e972348836": { - "model_module": "@jupyter-widgets/controls", - "model_name": "FloatProgressModel", - "state": { - "_view_name": "ProgressView", - "style": "IPY_MODEL_8a919053b780449aae5523658ad611fa", - "_dom_classes": [], - "description": "100%", - "_model_name": "FloatProgressModel", - "bar_style": "success", - "max": 22091032, - "_view_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "value": 22091032, - "_view_count": null, - "_view_module_version": "1.5.0", - "orientation": "horizontal", - "min": 0, - "description_tooltip": null, - "_model_module": "@jupyter-widgets/controls", - "layout": "IPY_MODEL_5bae9393a58b44f7b69fb04816f94f6f" - } - }, - "fb11acd663fa4e71b041d67310d045fd": { - "model_module": "@jupyter-widgets/controls", - "model_name": "HTMLModel", - "state": { - "_view_name": "HTMLView", - "style": "IPY_MODEL_d26c6d16c7f24030ab2da5285bf198ee", - "_dom_classes": [], - "description": "", - "_model_name": "HTMLModel", - "placeholder": "​", - "_view_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "value": " 21.1M/21.1M [00:02<00:00, 9.36MB/s]", - "_view_count": null, - "_view_module_version": "1.5.0", - "description_tooltip": null, - "_model_module": "@jupyter-widgets/controls", - "layout": "IPY_MODEL_f7767886b2364c8d9efdc79e175ad8eb" - } - }, - "8a919053b780449aae5523658ad611fa": { + "654525fe1ed34d5fbe1c36ed80ae1c1c": { "model_module": "@jupyter-widgets/controls", "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", "state": { "_view_name": "StyleView", "_model_name": "ProgressStyleModel", - "description_width": "initial", + "description_width": "", "_view_module": "@jupyter-widgets/base", "_model_module_version": "1.5.0", "_view_count": null, @@ -390,9 +238,10 @@ "_model_module": "@jupyter-widgets/controls" } }, - "5bae9393a58b44f7b69fb04816f94f6f": { + "09544845070e47baafc5e37d45ff23e9": { "model_module": "@jupyter-widgets/base", "model_name": "LayoutModel", + "model_module_version": "1.2.0", "state": { "_view_name": "LayoutView", "grid_template_rows": null, @@ -441,9 +290,10 @@ "left": null } }, - "d26c6d16c7f24030ab2da5285bf198ee": { + "1066f1d5b6104a3dae19f26269745bd0": { "model_module": "@jupyter-widgets/controls", "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", "state": { "_view_name": "StyleView", "_model_name": "DescriptionStyleModel", @@ -455,9 +305,10 @@ "_model_module": "@jupyter-widgets/controls" } }, - "f7767886b2364c8d9efdc79e175ad8eb": { + "dd3a70e1ef4547ec8d3463749ce06285": { "model_module": "@jupyter-widgets/base", "model_name": "LayoutModel", + "model_module_version": "1.2.0", "state": { "_view_name": "LayoutView", "grid_template_rows": null, @@ -517,20 +368,20 @@ "colab_type": "text" }, "source": [ - "\"Open", - "\"Kaggle\"" + "\"Open" ] }, { "cell_type": "markdown", "metadata": { - "id": "HvhYZrIZCEyo" + "id": "t6MPjfT5NrKQ" }, "source": [ - "\n", + "\n", + "\n", "\n", - "This is the **official YOLOv3 πŸš€ notebook** authored by **Ultralytics**, and is freely available for redistribution under the [GPL-3.0 license](https://choosealicense.com/licenses/gpl-3.0/). \n", - "For more information please visit https://github.com/ultralytics/yolov3 and https://www.ultralytics.com. Thank you!" + "This is the **official YOLOv3 πŸš€ notebook** by **Ultralytics**, and is freely available for redistribution under the [GPL-3.0 license](https://choosealicense.com/licenses/gpl-3.0/). \n", + "For more information please visit https://github.com/ultralytics/yolov3 and https://ultralytics.com. Thank you!" ] }, { @@ -551,27 +402,32 @@ "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "56f7b795-7a7b-46a1-8c5e-d06040187a85" + "outputId": "2e5d0950-2978-4304-856f-3b39f0d6627c" }, "source": [ - "!git clone https://github.com/ultralytics/yolov3 # clone repo\n", + "!git clone https://github.com/ultralytics/yolov3 -b update/yolov5_v6.0_release # clone\n", "%cd yolov3\n", - "%pip install -qr requirements.txt # install dependencies\n", + "%pip install -qr requirements.txt # install\n", "\n", "import torch\n", - "from IPython.display import Image, clear_output # to display images\n", - "\n", - "clear_output()\n", - "print(f\"Setup complete. Using torch {torch.__version__} ({torch.cuda.get_device_properties(0).name if torch.cuda.is_available() else 'CPU'})\")" + "from yolov3 import utils\n", + "display = utils.notebook_init() # checks" ], - "execution_count": null, + "execution_count": 1, "outputs": [ { "output_type": "stream", + "name": "stderr", "text": [ - "Setup complete. Using torch 1.8.1+cu101 (Tesla P100-PCIE-16GB)\n" - ], - "name": "stdout" + "YOLOv3 πŸš€ v9.5.0-20-g9d10fe5 torch 1.10.0+cu111 CUDA:0 (A100-SXM4-40GB, 40536MiB)\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Setup complete βœ…\n" + ] } ] }, @@ -585,7 +441,15 @@ "\n", "`detect.py` runs YOLOv3 inference on a variety of sources, downloading models automatically from the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases), and saving results to `runs/detect`. Example inference sources are:\n", "\n", - " " + "```shell\n", + "python detect.py --source 0 # webcam\n", + " img.jpg # image \n", + " vid.mp4 # video\n", + " path/ # directory\n", + " path/*.jpg # glob\n", + " 'https://youtu.be/Zgi9g1ksQHc' # YouTube\n", + " 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream\n", + "```" ] }, { @@ -593,58 +457,51 @@ "metadata": { "id": "zR9ZbuQCH7FX", "colab": { - "base_uri": "https://localhost:8080/", - "height": 521 + "base_uri": "https://localhost:8080/" }, - "outputId": "bd41a070-3498-42e1-ac1b-3900ac0c2ec2" + "outputId": "499c53a7-95f7-4fc1-dab8-7a660b813546" }, "source": [ - "!python detect.py --weights yolov3.pt --img 640 --conf 0.25 --source data/images/\n", - "Image(filename='runs/detect/exp/zidane.jpg', width=600)" + "!python detect.py --weights yolov3.pt --img 640 --conf 0.25 --source data/images\n", + "display.Image(filename='runs/detect/exp/zidane.jpg', width=600)" ], - "execution_count": null, + "execution_count": 3, "outputs": [ { "output_type": "stream", + "name": "stdout", "text": [ - "Namespace(agnostic_nms=False, augment=False, classes=None, conf_thres=0.25, device='', exist_ok=False, img_size=640, iou_thres=0.45, name='exp', nosave=False, project='runs/detect', save_conf=False, save_txt=False, source='data/images/', update=False, view_img=False, weights=['yolov3.pt'])\n", - "YOLOv3 πŸš€ v9.5.0-1-gbe29298 torch 1.8.1+cu101 CUDA:0 (Tesla P100-PCIE-16GB, 16280.875MB)\n", + "\u001b[34m\u001b[1mdetect: \u001b[0mweights=['yolov3.pt'], source=data/images, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs/detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=False\n", + "YOLOv3 πŸš€ v9.5.0-20-g9d10fe5 torch 1.10.0+cu111 CUDA:0 (A100-SXM4-40GB, 40536MiB)\n", "\n", "Fusing layers... \n", - "Model Summary: 261 layers, 61922845 parameters, 0 gradients, 156.3 GFLOPS\n", - "image 1/2 /content/yolov3/data/images/bus.jpg: 640x480 4 persons, 1 bus, Done. (0.026s)\n", - "image 2/2 /content/yolov3/data/images/zidane.jpg: 384x640 2 persons, 3 ties, Done. (0.025s)\n", - "Results saved to runs/detect/exp2\n", - "Done. (0.119s)\n" - ], - "name": "stdout" - }, - { - "output_type": "execute_result", - "data": { - "image/jpeg": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": { - "tags": [], - "image/jpeg": { - "width": 600 - } - }, - "execution_count": 5 + "Model Summary: 261 layers, 61922845 parameters, 0 gradients\n", + "image 1/2 /content/yolov3/data/images/bus.jpg: 640x480 4 persons, 1 bus, 1 tie, Done. (0.020s)\n", + "image 2/2 /content/yolov3/data/images/zidane.jpg: 384x640 2 persons, 3 ties, Done. (0.020s)\n", + "Speed: 0.5ms pre-process, 19.8ms inference, 1.2ms NMS per image at shape (1, 3, 640, 640)\n", + "Results saved to \u001b[1mruns/detect/exp\u001b[0m\n" + ] } ] }, + { + "cell_type": "markdown", + "metadata": { + "id": "hkAzDWJ7cWTr" + }, + "source": [ + "        \n", + "" + ] + }, { "cell_type": "markdown", "metadata": { "id": "0eq1SMWl6Sfn" }, "source": [ - "# 2. Test\n", - "Test a model's accuracy on [COCO](https://cocodataset.org/#home) val or test-dev datasets. Models are downloaded automatically from the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases). To show results by class use the `--verbose` flag. Note that `pycocotools` metrics may be ~1% better than the equivalent repo metrics, as is visible below, due to slight differences in mAP computation." + "# 2. Validate\n", + "Validate a model's accuracy on [COCO](https://cocodataset.org/#home) val or test-dev datasets. Models are downloaded automatically from the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases). To show results by class use the `--verbose` flag. Note that `pycocotools` metrics may be ~1% better than the equivalent repo metrics, as is visible below, due to slight differences in mAP computation." ] }, { @@ -653,8 +510,8 @@ "id": "eyTZYGgRjnMc" }, "source": [ - "## COCO val2017\n", - "Download [COCO val 2017](https://github.com/ultralytics/yolov5/blob/74b34872fdf41941cddcf243951cdb090fbac17b/data/coco.yaml#L14) dataset (1GB - 5000 images), and test model accuracy." + "## COCO val\n", + "Download [COCO val 2017](https://github.com/ultralytics/yolov3/blob/master/data/coco.yaml) dataset (1GB - 5000 images), and test model accuracy." ] }, { @@ -663,49 +520,43 @@ "id": "WQPtK1QYVaD_", "colab": { "base_uri": "https://localhost:8080/", - "height": 65, + "height": 48, "referenced_widgets": [ - "355d9ee3dfc4487ebcae3b66ddbedce1", - "8209acd3185441e7b263eead5e8babdf", - "b81d30356f7048b0abcba35bde811526", - "7fcbf6b56f2e4b6dbf84e48465c96633", - "6ee48f9f3af444a7b02ec2f074dec1f8", - "b7d819ed5f2f4e39a75a823792ab7249", - "3af216dd7d024739b8168995800ed8be", - "763141d8de8a498a92ffa66aafed0c5a" + "eeda9d6850e8406f9bbc5b06051b3710", + "1e823c45174a4216be7234a6cc5cfd99", + "cd8efd6c5de94ea8848a7d5b8766a4d6", + "a4ec69c4697c4b0e84e6193be227f63e", + "9a5694c133be46df8d2fe809b77c1c35", + "d584167143f84a0484006dded3fd2620", + "b9a25c0d425c4fe4b8cd51ae6a301b0d", + "654525fe1ed34d5fbe1c36ed80ae1c1c", + "09544845070e47baafc5e37d45ff23e9", + "1066f1d5b6104a3dae19f26269745bd0", + "dd3a70e1ef4547ec8d3463749ce06285" ] }, - "outputId": "f7e4fb76-74db-4810-c705-b416bc862b52" + "outputId": "56199bac-5a5e-41eb-8892-bf387a1ec7cb" }, "source": [ - "# Download COCO val2017\n", - "torch.hub.download_url_to_file('https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017val.zip', 'tmp.zip')\n", - "!unzip -q tmp.zip -d ../ && rm tmp.zip" + "# Download COCO val\n", + "torch.hub.download_url_to_file('https://ultralytics.com/assets/coco2017val.zip', 'tmp.zip')\n", + "!unzip -q tmp.zip -d ../datasets && rm tmp.zip" ], - "execution_count": null, + "execution_count": 4, "outputs": [ { "output_type": "display_data", "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "355d9ee3dfc4487ebcae3b66ddbedce1", + "model_id": "eeda9d6850e8406f9bbc5b06051b3710", "version_minor": 0, "version_major": 2 }, "text/plain": [ - "HBox(children=(FloatProgress(value=0.0, max=819257867.0), HTML(value='')))" + " 0%| | 0.00/780M [00:00

\n", + "Close the active learning loop by sampling images from your inference conditions with the `roboflow` pip package\n", + "

\n", "\n", - "All training results are saved to `runs/train/` with incrementing run directories, i.e. `runs/train/exp2`, `runs/train/exp3` etc.\n" + "Train a YOLOv3 model on the [COCO128](https://www.kaggle.com/ultralytics/coco128) dataset with `--data coco128.yaml`, starting from pretrained `--weights yolov3.pt`, or from randomly initialized `--weights '' --cfg yolov3yaml`.\n", + "\n", + "- **Pretrained [Models](https://github.com/ultralytics/yolov3/tree/master/models)** are downloaded\n", + "automatically from the [latest YOLOv3 release](https://github.com/ultralytics/yolov3/releases)\n", + "- **[Datasets](https://github.com/ultralytics/yolov3/tree/master/data)** available for autodownload include: [COCO](https://github.com/ultralytics/yolov3/blob/master/data/coco.yaml), [COCO128](https://github.com/ultralytics/yolov3/blob/master/data/coco128.yaml), [VOC](https://github.com/ultralytics/yolov3/blob/master/data/VOC.yaml), [Argoverse](https://github.com/ultralytics/yolov3/blob/master/data/Argoverse.yaml), [VisDrone](https://github.com/ultralytics/yolov3/blob/master/data/VisDrone.yaml), [GlobalWheat](https://github.com/ultralytics/yolov3/blob/master/data/GlobalWheat2020.yaml), [xView](https://github.com/ultralytics/yolov3/blob/master/data/xView.yaml), [Objects365](https://github.com/ultralytics/yolov3/blob/master/data/Objects365.yaml), [SKU-110K](https://github.com/ultralytics/yolov3/blob/master/data/SKU-110K.yaml).\n", + "- **Training Results** are saved to `runs/train/` with incrementing run directories, i.e. `runs/train/exp2`, `runs/train/exp3` etc.\n", + "

\n" ] }, { @@ -887,7 +684,7 @@ "id": "bOy5KI2ncnWd" }, "source": [ - "# Tensorboard (optional)\n", + "# Tensorboard (optional)\n", "%load_ext tensorboard\n", "%tensorboard --logdir runs/train" ], @@ -915,25 +712,25 @@ "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "3638328f-e897-40d5-c49f-3dfbcea258a9" + "outputId": "28039ba4-b23b-4e59-ea0e-e1f8f7df0cdb" }, "source": [ "# Train YOLOv3 on COCO128 for 3 epochs\n", - "!python train.py --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov3.pt --nosave --cache" + "!python train.py --img 640 --batch 16 --epochs 3 --data coco128.yaml --weights yolov3.pt --cache" ], - "execution_count": null, + "execution_count": 8, "outputs": [ { "output_type": "stream", + "name": "stdout", "text": [ + "\u001b[34m\u001b[1mtrain: \u001b[0mweights=yolov3.pt, cfg=, data=coco128.yaml, hyp=data/hyps/hyp.scratch.yaml, epochs=3, batch_size=16, imgsz=640, rect=False, resume=False, nosave=False, noval=False, noautoanchor=False, evolve=None, bucket=, cache=ram, image_weights=False, device=, multi_scale=False, single_cls=False, adam=False, sync_bn=False, workers=8, project=runs/train, name=exp, exist_ok=False, quad=False, linear_lr=False, label_smoothing=0.0, patience=100, freeze=0, save_period=-1, local_rank=-1, entity=None, upload_dataset=False, bbox_interval=-1, artifact_alias=latest\n", "\u001b[34m\u001b[1mgithub: \u001b[0mup to date with https://github.com/ultralytics/yolov3 βœ…\n", - "YOLOv3 πŸš€ v9.5.0-1-gbe29298 torch 1.8.1+cu101 CUDA:0 (Tesla P100-PCIE-16GB, 16280.875MB)\n", + "YOLOv3 πŸš€ v9.5.0-20-g9d10fe5 torch 1.10.0+cu111 CUDA:0 (A100-SXM4-40GB, 40536MiB)\n", "\n", - "Namespace(adam=False, artifact_alias='latest', batch_size=16, bbox_interval=-1, bucket='', cache_images=True, cfg='', data='./data/coco128.yaml', device='', entity=None, epochs=3, evolve=False, exist_ok=False, global_rank=-1, hyp='data/hyp.scratch.yaml', image_weights=False, img_size=[640, 640], label_smoothing=0.0, linear_lr=False, local_rank=-1, multi_scale=False, name='exp', noautoanchor=False, nosave=True, notest=False, project='runs/train', quad=False, rect=False, resume=False, save_dir='runs/train/exp', save_period=-1, single_cls=False, sync_bn=False, total_batch_size=16, upload_dataset=False, weights='yolov3.pt', workers=8, world_size=1)\n", - "\u001b[34m\u001b[1mtensorboard: \u001b[0mStart with 'tensorboard --logdir runs/train', view at http://localhost:6006/\n", - "2021-04-12 21:26:33.963524: I tensorflow/stream_executor/platform/default/dso_loader.cc:49] Successfully opened dynamic library libcudart.so.11.0\n", - "\u001b[34m\u001b[1mhyperparameters: \u001b[0mlr0=0.01, lrf=0.2, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0\n", - "\u001b[34m\u001b[1mwandb: \u001b[0mInstall Weights & Biases for YOLOv5 logging with 'pip install wandb' (recommended)\n", + "\u001b[34m\u001b[1mhyperparameters: \u001b[0mlr0=0.01, lrf=0.1, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0, copy_paste=0.0\n", + "\u001b[34m\u001b[1mWeights & Biases: \u001b[0mrun 'pip install wandb' to automatically track and visualize YOLOv3 πŸš€ runs (RECOMMENDED)\n", + "\u001b[34m\u001b[1mTensorBoard: \u001b[0mStart with 'tensorboard --logdir runs/train', view at http://localhost:6006/\n", "\n", " from n params module arguments \n", " 0 -1 1 928 models.common.Conv [3, 32, 3, 1] \n", @@ -965,49 +762,121 @@ " 26 -1 1 344832 models.common.Bottleneck [384, 256, False] \n", " 27 -1 2 656896 models.common.Bottleneck [256, 256, False] \n", " 28 [27, 22, 15] 1 457725 models.yolo.Detect [80, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [256, 512, 1024]]\n", - "Model Summary: 333 layers, 61949149 parameters, 61949149 gradients, 156.4 GFLOPS\n", - "\n", - "Transferred 440/440 items from yolov3.pt\n", - "\n", - "WARNING: Dataset not found, nonexistent paths: ['/content/coco128/images/train2017']\n", - "Downloading https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128.zip ...\n", - "100% 21.1M/21.1M [00:01<00:00, 13.6MB/s]\n", - "Dataset autodownload success\n", + "Model Summary: 333 layers, 61949149 parameters, 61949149 gradients, 156.3 GFLOPs\n", "\n", + "Transferred 439/439 items from yolov3.pt\n", "Scaled weight_decay = 0.0005\n", - "Optimizer groups: 75 .bias, 75 conv.weight, 72 other\n", - "\u001b[34m\u001b[1mtrain: \u001b[0mScanning '../coco128/labels/train2017' images and labels... 128 found, 0 missing, 2 empty, 0 corrupted: 100% 128/128 [00:00<00:00, 3076.66it/s]\n", - "\u001b[34m\u001b[1mtrain: \u001b[0mNew cache created: ../coco128/labels/train2017.cache\n", - "\u001b[34m\u001b[1mtrain: \u001b[0mCaching images (0.1GB): 100% 128/128 [00:00<00:00, 217.17it/s]\n", - "\u001b[34m\u001b[1mval: \u001b[0mScanning '../coco128/labels/train2017.cache' images and labels... 128 found, 0 missing, 2 empty, 0 corrupted: 100% 128/128 [00:00<00:00, 797727.95it/s]\n", - "\u001b[34m\u001b[1mval: \u001b[0mCaching images (0.1GB): 100% 128/128 [00:00<00:00, 149.28it/s]\n", - "Plotting labels... \n", + "\u001b[34m\u001b[1moptimizer:\u001b[0m SGD with parameter groups 72 weight, 75 weight (no decay), 75 bias\n", + "\u001b[34m\u001b[1malbumentations: \u001b[0mversion 1.0.3 required by YOLOv3, but version 0.1.12 is currently installed\n", + "\u001b[34m\u001b[1mtrain: \u001b[0mScanning '../datasets/coco128/labels/train2017.cache' images and labels... 128 found, 0 missing, 2 empty, 0 corrupted: 100% 128/128 [00:00" + "

\"Weights

" ] }, { @@ -1043,67 +912,25 @@ "source": [ "## Local Logging\n", "\n", - "All results are logged by default to `runs/train`, with a new experiment directory created for each new training as `runs/train/exp2`, `runs/train/exp3`, etc. View train and test jpgs to see mosaics, labels, predictions and augmentation effects. Note a **Mosaic Dataloader** is used for training (shown below), a new concept developed by Ultralytics and first featured in [YOLOv4](https://arxiv.org/abs/2004.10934)." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "riPdhraOTCO0" - }, - "source": [ - "Image(filename='runs/train/exp/train_batch0.jpg', width=800) # train batch 0 mosaics and labels\n", - "Image(filename='runs/train/exp/test_batch0_labels.jpg', width=800) # test batch 0 labels\n", - "Image(filename='runs/train/exp/test_batch0_pred.jpg', width=800) # test batch 0 predictions" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OYG4WFEnTVrI" - }, - "source": [ - "> \n", + "All results are logged by default to `runs/train`, with a new experiment directory created for each new training as `runs/train/exp2`, `runs/train/exp3`, etc. View train and val jpgs to see mosaics, labels, predictions and augmentation effects. Note an Ultralytics **Mosaic Dataloader** is used for training (shown below), which combines 4 images into 1 mosaic during training.\n", + "\n", + "> \n", "`train_batch0.jpg` shows train batch 0 mosaics and labels\n", "\n", - "> \n", - "`test_batch0_labels.jpg` shows test batch 0 labels\n", + "> \n", + "`test_batch0_labels.jpg` shows val batch 0 labels\n", "\n", - "> \n", - "`test_batch0_pred.jpg` shows test batch 0 _predictions_\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "7KN5ghjE6ZWh" - }, - "source": [ - "Training losses and performance metrics are also logged to [Tensorboard](https://www.tensorflow.org/tensorboard) and a custom `results.txt` logfile which is plotted as `results.png` (below) after training completes. Here we show YOLOv3 trained on COCO128 to 300 epochs, starting from scratch (blue), and from pretrained `--weights yolov3.pt` (orange)." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "MDznIqPF7nk3" - }, - "source": [ + "> \n", + "`test_batch0_pred.jpg` shows val batch 0 _predictions_\n", + "\n", + "Training results are automatically logged to [Tensorboard](https://www.tensorflow.org/tensorboard) and [CSV](https://github.com/ultralytics/yolov5/pull/4148) as `results.csv`, which is plotted as `results.png` (below) after training completes. You can also plot any `results.csv` file manually:\n", + "\n", + "```python\n", "from utils.plots import plot_results \n", - "plot_results(save_dir='runs/train/exp') # plot all results*.txt as results.png\n", - "Image(filename='runs/train/exp/results.png', width=800)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "lfrEegCSW3fK" - }, - "source": [ - "\n" + "plot_results('path/to/results.csv') # plot 'results.csv' as 'results.png'\n", + "```\n", + "\n", + "\"COCO128" ] }, { @@ -1116,10 +943,10 @@ "\n", "YOLOv3 may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled):\n", "\n", - "- **Google Colab and Kaggle** notebooks with free GPU: \"Open \"Open\n", - "- **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/GCP-Quickstart)\n", - "- **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/AWS-Quickstart)\n", - "- **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov5/wiki/Docker-Quickstart) \"Docker\n" + "- **Google Colab and Kaggle** notebooks with free GPU: \"Open \"Open\n", + "- **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/GCP-Quickstart)\n", + "- **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/AWS-Quickstart)\n", + "- **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/Docker-Quickstart) \"Docker\n" ] }, { @@ -1130,9 +957,9 @@ "source": [ "# Status\n", "\n", - "![CI CPU testing](https://github.com/ultralytics/yolov5/workflows/CI%20CPU%20testing/badge.svg)\n", + "![CI CPU testing](https://github.com/ultralytics/yolov3/workflows/CI%20CPU%20testing/badge.svg)\n", "\n", - "If this badge is green, all [YOLOv3 GitHub Actions](https://github.com/ultralytics/yolov5/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv3 training ([train.py](https://github.com/ultralytics/yolov5/blob/master/train.py)), testing ([test.py](https://github.com/ultralytics/yolov5/blob/master/test.py)), inference ([detect.py](https://github.com/ultralytics/yolov5/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov5/blob/master/models/export.py)) on MacOS, Windows, and Ubuntu every 24 hours and on every commit.\n" + "If this badge is green, all [YOLOv3 GitHub Actions](https://github.com/ultralytics/yolov3/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv3 training ([train.py](https://github.com/ultralytics/yolov3/blob/master/train.py)), testing ([val.py](https://github.com/ultralytics/yolov3/blob/master/val.py)), inference ([detect.py](https://github.com/ultralytics/yolov3/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov3/blob/master/export.py)) on MacOS, Windows, and Ubuntu every 24 hours and on every commit.\n" ] }, { @@ -1146,20 +973,6 @@ "Optional extras below. Unit tests validate repo functionality and should be run on any PRs submitted.\n" ] }, - { - "cell_type": "code", - "metadata": { - "id": "gI6NoBev8Ib1" - }, - "source": [ - "# Re-clone repo\n", - "%cd ..\n", - "%rm -rf yolov3 && git clone https://github.com/ultralytics/yolov3\n", - "%cd yolov3" - ], - "execution_count": null, - "outputs": [] - }, { "cell_type": "code", "metadata": { @@ -1168,8 +981,8 @@ "source": [ "# Reproduce\n", "for x in 'yolov3', 'yolov3-spp', 'yolov3-tiny':\n", - " !python test.py --weights {x}.pt --data coco.yaml --img 640 --conf 0.25 --iou 0.45 # speed\n", - " !python test.py --weights {x}.pt --data coco.yaml --img 640 --conf 0.001 --iou 0.65 # mAP" + " !python val.py --weights {x}.pt --data coco.yaml --img 640 --task speed # speed\n", + " !python val.py --weights {x}.pt --data coco.yaml --img 640 --conf 0.001 --iou 0.65 # mAP" ], "execution_count": null, "outputs": [] @@ -1184,7 +997,7 @@ "import torch\n", "\n", "# Model\n", - "model = torch.hub.load('ultralytics/yolov3', 'yolov3') # or 'yolov3_spp', 'yolov3_tiny'\n", + "model = torch.hub.load('ultralytics/yolov3', 'yolov3')\n", "\n", "# Images\n", "dir = 'https://ultralytics.com/images/'\n", @@ -1203,23 +1016,23 @@ "id": "FGH0ZjkGjejy" }, "source": [ - "# Unit tests\n", + "# CI Checks\n", "%%shell\n", "export PYTHONPATH=\"$PWD\" # to run *.py. files in subdirectories\n", - "\n", "rm -rf runs # remove runs/\n", - "for m in yolov3; do # models\n", - " python train.py --weights $m.pt --epochs 3 --img 320 --device 0 # train pretrained\n", - " python train.py --weights '' --cfg $m.yaml --epochs 3 --img 320 --device 0 # train scratch\n", + "for m in yolov3-tiny; do # models\n", + " python train.py --img 64 --batch 32 --weights $m.pt --epochs 1 --device 0 # train pretrained\n", + " python train.py --img 64 --batch 32 --weights '' --cfg $m.yaml --epochs 1 --device 0 # train scratch\n", " for d in 0 cpu; do # devices\n", + " python val.py --weights $m.pt --device $d # val official\n", + " python val.py --weights runs/train/exp/weights/best.pt --device $d # val custom\n", " python detect.py --weights $m.pt --device $d # detect official\n", " python detect.py --weights runs/train/exp/weights/best.pt --device $d # detect custom\n", - " python test.py --weights $m.pt --device $d # test official\n", - " python test.py --weights runs/train/exp/weights/best.pt --device $d # test custom\n", " done\n", " python hubconf.py # hub\n", - " python models/yolo.py --cfg $m.yaml # inspect\n", - " python models/export.py --weights $m.pt --img 640 --batch 1 # export\n", + " python models/yolo.py --cfg $m.yaml # build PyTorch model\n", + " python models/tf.py --weights $m.pt # build TensorFlow model\n", + " python export.py --img 64 --batch 1 --weights $m.pt --include torchscript onnx # export\n", "done" ], "execution_count": null, @@ -1232,11 +1045,11 @@ }, "source": [ "# Profile\n", - "from utils.torch_utils import profile \n", + "from utils.torch_utils import profile\n", "\n", "m1 = lambda x: x * torch.sigmoid(x)\n", "m2 = torch.nn.SiLU()\n", - "profile(x=torch.randn(16, 3, 640, 640), ops=[m1, m2], n=100)" + "results = profile(input=torch.randn(16, 3, 640, 640), ops=[m1, m2], n=100)" ], "execution_count": null, "outputs": [] @@ -1261,8 +1074,8 @@ }, "source": [ "# VOC\n", - "for b, m in zip([64, 48, 32, 16], ['yolov3', 'yolov3-spp', 'yolov3-tiny']): # zip(batch_size, model)\n", - " !python train.py --batch {b} --weights {m}.pt --data voc.yaml --epochs 50 --cache --img 512 --nosave --hyp hyp.finetune.yaml --project VOC --name {m}" + "for b, m in zip([24, 24, 64], ['yolov3', 'yolov3-spp', 'yolov3-tiny']): # zip(batch_size, model)\n", + " !python train.py --batch {b} --weights {m}.pt --data VOC.yaml --epochs 50 --cache --img 512 --nosave --hyp hyp.finetune.yaml --project VOC --name {m}" ], "execution_count": null, "outputs": [] diff --git a/utils/__init__.py b/utils/__init__.py index e69de29bb2..309830c830 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -0,0 +1,18 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +utils/initialization +""" + + +def notebook_init(): + # For notebooks + print('Checking setup...') + from IPython import display # to display images and clear console output + + from utils.general import emojis + from utils.torch_utils import select_device # imports + + display.clear_output() + select_device(newline=False) + print(emojis('Setup complete βœ…')) + return display diff --git a/utils/activations.py b/utils/activations.py index 92a3b5eaa5..ae2fef1c8c 100644 --- a/utils/activations.py +++ b/utils/activations.py @@ -1,4 +1,7 @@ -# Activation functions +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Activation functions +""" import torch import torch.nn as nn @@ -16,7 +19,7 @@ class Hardswish(nn.Module): # export-friendly version of nn.Hardswish() @staticmethod def forward(x): # return x * F.hardsigmoid(x) # for torchscript and CoreML - return x * F.hardtanh(x + 3, 0., 6.) / 6. # for torchscript, CoreML and ONNX + return x * F.hardtanh(x + 3, 0.0, 6.0) / 6.0 # for torchscript, CoreML and ONNX # Mish https://github.com/digantamisra98/Mish -------------------------------------------------------------------------- diff --git a/utils/augmentations.py b/utils/augmentations.py new file mode 100644 index 0000000000..16685044ea --- /dev/null +++ b/utils/augmentations.py @@ -0,0 +1,277 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Image augmentation functions +""" + +import math +import random + +import cv2 +import numpy as np + +from utils.general import LOGGER, check_version, colorstr, resample_segments, segment2box +from utils.metrics import bbox_ioa + + +class Albumentations: + # Albumentations class (optional, only used if package is installed) + def __init__(self): + self.transform = None + try: + import albumentations as A + check_version(A.__version__, '1.0.3', hard=True) # version requirement + + self.transform = A.Compose([ + A.Blur(p=0.01), + A.MedianBlur(p=0.01), + A.ToGray(p=0.01), + A.CLAHE(p=0.01), + A.RandomBrightnessContrast(p=0.0), + A.RandomGamma(p=0.0), + A.ImageCompression(quality_lower=75, p=0.0)], + bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) + + LOGGER.info(colorstr('albumentations: ') + ', '.join(f'{x}' for x in self.transform.transforms if x.p)) + except ImportError: # package not installed, skip + pass + except Exception as e: + LOGGER.info(colorstr('albumentations: ') + f'{e}') + + def __call__(self, im, labels, p=1.0): + if self.transform and random.random() < p: + new = self.transform(image=im, bboxes=labels[:, 1:], class_labels=labels[:, 0]) # transformed + im, labels = new['image'], np.array([[c, *b] for c, b in zip(new['class_labels'], new['bboxes'])]) + return im, labels + + +def augment_hsv(im, hgain=0.5, sgain=0.5, vgain=0.5): + # HSV color-space augmentation + if hgain or sgain or vgain: + r = np.random.uniform(-1, 1, 3) * [hgain, sgain, vgain] + 1 # random gains + hue, sat, val = cv2.split(cv2.cvtColor(im, cv2.COLOR_BGR2HSV)) + dtype = im.dtype # uint8 + + x = np.arange(0, 256, dtype=r.dtype) + lut_hue = ((x * r[0]) % 180).astype(dtype) + lut_sat = np.clip(x * r[1], 0, 255).astype(dtype) + lut_val = np.clip(x * r[2], 0, 255).astype(dtype) + + im_hsv = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val))) + cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR, dst=im) # no return needed + + +def hist_equalize(im, clahe=True, bgr=False): + # Equalize histogram on BGR image 'im' with im.shape(n,m,3) and range 0-255 + yuv = cv2.cvtColor(im, cv2.COLOR_BGR2YUV if bgr else cv2.COLOR_RGB2YUV) + if clahe: + c = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + yuv[:, :, 0] = c.apply(yuv[:, :, 0]) + else: + yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0]) # equalize Y channel histogram + return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR if bgr else cv2.COLOR_YUV2RGB) # convert YUV image to RGB + + +def replicate(im, labels): + # Replicate labels + h, w = im.shape[:2] + boxes = labels[:, 1:].astype(int) + x1, y1, x2, y2 = boxes.T + s = ((x2 - x1) + (y2 - y1)) / 2 # side length (pixels) + for i in s.argsort()[:round(s.size * 0.5)]: # smallest indices + x1b, y1b, x2b, y2b = boxes[i] + bh, bw = y2b - y1b, x2b - x1b + yc, xc = int(random.uniform(0, h - bh)), int(random.uniform(0, w - bw)) # offset x, y + x1a, y1a, x2a, y2a = [xc, yc, xc + bw, yc + bh] + im[y1a:y2a, x1a:x2a] = im[y1b:y2b, x1b:x2b] # im4[ymin:ymax, xmin:xmax] + labels = np.append(labels, [[labels[i, 0], x1a, y1a, x2a, y2a]], axis=0) + + return im, labels + + +def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32): + # Resize and pad image while meeting stride-multiple constraints + shape = im.shape[:2] # current shape [height, width] + if isinstance(new_shape, int): + new_shape = (new_shape, new_shape) + + # Scale ratio (new / old) + r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) + if not scaleup: # only scale down, do not scale up (for better val mAP) + r = min(r, 1.0) + + # Compute padding + ratio = r, r # width, height ratios + new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) + dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding + if auto: # minimum rectangle + dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding + elif scaleFill: # stretch + dw, dh = 0.0, 0.0 + new_unpad = (new_shape[1], new_shape[0]) + ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios + + dw /= 2 # divide padding into 2 sides + dh /= 2 + + if shape[::-1] != new_unpad: # resize + im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) + top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) + left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) + im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border + return im, ratio, (dw, dh) + + +def random_perspective(im, targets=(), segments=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0, + border=(0, 0)): + # torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(0.1, 0.1), scale=(0.9, 1.1), shear=(-10, 10)) + # targets = [cls, xyxy] + + height = im.shape[0] + border[0] * 2 # shape(h,w,c) + width = im.shape[1] + border[1] * 2 + + # Center + C = np.eye(3) + C[0, 2] = -im.shape[1] / 2 # x translation (pixels) + C[1, 2] = -im.shape[0] / 2 # y translation (pixels) + + # Perspective + P = np.eye(3) + P[2, 0] = random.uniform(-perspective, perspective) # x perspective (about y) + P[2, 1] = random.uniform(-perspective, perspective) # y perspective (about x) + + # Rotation and Scale + R = np.eye(3) + a = random.uniform(-degrees, degrees) + # a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotations + s = random.uniform(1 - scale, 1 + scale) + # s = 2 ** random.uniform(-scale, scale) + R[:2] = cv2.getRotationMatrix2D(angle=a, center=(0, 0), scale=s) + + # Shear + S = np.eye(3) + S[0, 1] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # x shear (deg) + S[1, 0] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # y shear (deg) + + # Translation + T = np.eye(3) + T[0, 2] = random.uniform(0.5 - translate, 0.5 + translate) * width # x translation (pixels) + T[1, 2] = random.uniform(0.5 - translate, 0.5 + translate) * height # y translation (pixels) + + # Combined rotation matrix + M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT + if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed + if perspective: + im = cv2.warpPerspective(im, M, dsize=(width, height), borderValue=(114, 114, 114)) + else: # affine + im = cv2.warpAffine(im, M[:2], dsize=(width, height), borderValue=(114, 114, 114)) + + # Visualize + # import matplotlib.pyplot as plt + # ax = plt.subplots(1, 2, figsize=(12, 6))[1].ravel() + # ax[0].imshow(im[:, :, ::-1]) # base + # ax[1].imshow(im2[:, :, ::-1]) # warped + + # Transform label coordinates + n = len(targets) + if n: + use_segments = any(x.any() for x in segments) + new = np.zeros((n, 4)) + if use_segments: # warp segments + segments = resample_segments(segments) # upsample + for i, segment in enumerate(segments): + xy = np.ones((len(segment), 3)) + xy[:, :2] = segment + xy = xy @ M.T # transform + xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine + + # clip + new[i] = segment2box(xy, width, height) + + else: # warp boxes + xy = np.ones((n * 4, 3)) + xy[:, :2] = targets[:, [1, 2, 3, 4, 1, 4, 3, 2]].reshape(n * 4, 2) # x1y1, x2y2, x1y2, x2y1 + xy = xy @ M.T # transform + xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine + + # create new boxes + x = xy[:, [0, 2, 4, 6]] + y = xy[:, [1, 3, 5, 7]] + new = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T + + # clip + new[:, [0, 2]] = new[:, [0, 2]].clip(0, width) + new[:, [1, 3]] = new[:, [1, 3]].clip(0, height) + + # filter candidates + i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01 if use_segments else 0.10) + targets = targets[i] + targets[:, 1:5] = new[i] + + return im, targets + + +def copy_paste(im, labels, segments, p=0.5): + # Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy) + n = len(segments) + if p and n: + h, w, c = im.shape # height, width, channels + im_new = np.zeros(im.shape, np.uint8) + for j in random.sample(range(n), k=round(p * n)): + l, s = labels[j], segments[j] + box = w - l[3], l[2], w - l[1], l[4] + ioa = bbox_ioa(box, labels[:, 1:5]) # intersection over area + if (ioa < 0.30).all(): # allow 30% obscuration of existing labels + labels = np.concatenate((labels, [[l[0], *box]]), 0) + segments.append(np.concatenate((w - s[:, 0:1], s[:, 1:2]), 1)) + cv2.drawContours(im_new, [segments[j].astype(np.int32)], -1, (255, 255, 255), cv2.FILLED) + + result = cv2.bitwise_and(src1=im, src2=im_new) + result = cv2.flip(result, 1) # augment segments (flip left-right) + i = result > 0 # pixels to replace + # i[:, :] = result.max(2).reshape(h, w, 1) # act over ch + im[i] = result[i] # cv2.imwrite('debug.jpg', im) # debug + + return im, labels, segments + + +def cutout(im, labels, p=0.5): + # Applies image cutout augmentation https://arxiv.org/abs/1708.04552 + if random.random() < p: + h, w = im.shape[:2] + scales = [0.5] * 1 + [0.25] * 2 + [0.125] * 4 + [0.0625] * 8 + [0.03125] * 16 # image size fraction + for s in scales: + mask_h = random.randint(1, int(h * s)) # create random masks + mask_w = random.randint(1, int(w * s)) + + # box + xmin = max(0, random.randint(0, w) - mask_w // 2) + ymin = max(0, random.randint(0, h) - mask_h // 2) + xmax = min(w, xmin + mask_w) + ymax = min(h, ymin + mask_h) + + # apply random color mask + im[ymin:ymax, xmin:xmax] = [random.randint(64, 191) for _ in range(3)] + + # return unobscured labels + if len(labels) and s > 0.03: + box = np.array([xmin, ymin, xmax, ymax], dtype=np.float32) + ioa = bbox_ioa(box, labels[:, 1:5]) # intersection over area + labels = labels[ioa < 0.60] # remove >60% obscured labels + + return labels + + +def mixup(im, labels, im2, labels2): + # Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdf + r = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0 + im = (im * r + im2 * (1 - r)).astype(np.uint8) + labels = np.concatenate((labels, labels2), 0) + return im, labels + + +def box_candidates(box1, box2, wh_thr=2, ar_thr=20, area_thr=0.1, eps=1e-16): # box1(4,n), box2(4,n) + # Compute candidate boxes: box1 before augment, box2 after augment, wh_thr (pixels), aspect_ratio_thr, area_ratio + w1, h1 = box1[2] - box1[0], box1[3] - box1[1] + w2, h2 = box2[2] - box2[0], box2[3] - box2[1] + ar = np.maximum(w2 / (h2 + eps), h2 / (w2 + eps)) # aspect ratio + return (w2 > wh_thr) & (h2 > wh_thr) & (w2 * h2 / (w1 * h1 + eps) > area_thr) & (ar < ar_thr) # candidates diff --git a/utils/autoanchor.py b/utils/autoanchor.py index 51ed803455..0c202c4965 100644 --- a/utils/autoanchor.py +++ b/utils/autoanchor.py @@ -1,28 +1,32 @@ -# Auto-anchor utils +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Auto-anchor utils +""" + +import random import numpy as np import torch import yaml from tqdm import tqdm -from utils.general import colorstr +from utils.general import LOGGER, colorstr, emojis + +PREFIX = colorstr('AutoAnchor: ') def check_anchor_order(m): - # Check anchor order against stride order for YOLOv3 Detect() module m, and correct if necessary - a = m.anchor_grid.prod(-1).view(-1) # anchor area + # Check anchor order against stride order for Detect() module m, and correct if necessary + a = m.anchors.prod(-1).view(-1) # anchor area da = a[-1] - a[0] # delta a ds = m.stride[-1] - m.stride[0] # delta s if da.sign() != ds.sign(): # same order - print('Reversing anchor order') + LOGGER.info(f'{PREFIX}Reversing anchor order') m.anchors[:] = m.anchors.flip(0) - m.anchor_grid[:] = m.anchor_grid.flip(0) def check_anchors(dataset, model, thr=4.0, imgsz=640): # Check anchor fit to data, recompute if necessary - prefix = colorstr('autoanchor: ') - print(f'\n{prefix}Analyzing anchors... ', end='') m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect() shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True) scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale @@ -30,39 +34,39 @@ def check_anchors(dataset, model, thr=4.0, imgsz=640): def metric(k): # compute metric r = wh[:, None] / k[None] - x = torch.min(r, 1. / r).min(2)[0] # ratio metric + x = torch.min(r, 1 / r).min(2)[0] # ratio metric best = x.max(1)[0] # best_x - aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold - bpr = (best > 1. / thr).float().mean() # best possible recall + aat = (x > 1 / thr).float().sum(1).mean() # anchors above threshold + bpr = (best > 1 / thr).float().mean() # best possible recall return bpr, aat - anchors = m.anchor_grid.clone().cpu().view(-1, 2) # current anchors - bpr, aat = metric(anchors) - print(f'anchors/target = {aat:.2f}, Best Possible Recall (BPR) = {bpr:.4f}', end='') - if bpr < 0.98: # threshold to recompute - print('. Attempting to improve anchors, please wait...') - na = m.anchor_grid.numel() // 2 # number of anchors + anchors = m.anchors.clone() * m.stride.to(m.anchors.device).view(-1, 1, 1) # current anchors + bpr, aat = metric(anchors.cpu().view(-1, 2)) + s = f'\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). ' + if bpr > 0.98: # threshold to recompute + LOGGER.info(emojis(f'{s}Current anchors are a good fit to dataset βœ…')) + else: + LOGGER.info(emojis(f'{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...')) + na = m.anchors.numel() // 2 # number of anchors try: anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False) except Exception as e: - print(f'{prefix}ERROR: {e}') + LOGGER.info(f'{PREFIX}ERROR: {e}') new_bpr = metric(anchors)[0] if new_bpr > bpr: # replace anchors anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors) - m.anchor_grid[:] = anchors.clone().view_as(m.anchor_grid) # for inference m.anchors[:] = anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss check_anchor_order(m) - print(f'{prefix}New anchors saved to model. Update model *.yaml to use these anchors in the future.') + LOGGER.info(f'{PREFIX}New anchors saved to model. Update model *.yaml to use these anchors in the future.') else: - print(f'{prefix}Original anchors better than new anchors. Proceeding with original anchors.') - print('') # newline + LOGGER.info(f'{PREFIX}Original anchors better than new anchors. Proceeding with original anchors.') -def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True): +def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True): """ Creates kmeans-evolved anchors from training dataset Arguments: - path: path to dataset *.yaml, or a loaded dataset + dataset: path to data.yaml, or a loaded dataset n: number of anchors img_size: image size used for training thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0 @@ -77,12 +81,11 @@ def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=10 """ from scipy.cluster.vq import kmeans - thr = 1. / thr - prefix = colorstr('autoanchor: ') + thr = 1 / thr def metric(k, wh): # compute metrics r = wh[:, None] / k[None] - x = torch.min(r, 1. / r).min(2)[0] # ratio metric + x = torch.min(r, 1 / r).min(2)[0] # ratio metric # x = wh_iou(wh, torch.tensor(k)) # iou metric return x, x.max(1)[0] # x, best_x @@ -90,24 +93,24 @@ def anchor_fitness(k): # mutation fitness _, best = metric(torch.tensor(k, dtype=torch.float32), wh) return (best * (best > thr).float()).mean() # fitness - def print_results(k): + def print_results(k, verbose=True): k = k[np.argsort(k.prod(1))] # sort small to large x, best = metric(k, wh0) bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr - print(f'{prefix}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr') - print(f'{prefix}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' - f'past_thr={x[x > thr].mean():.3f}-mean: ', end='') + s = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \ + f'{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' \ + f'past_thr={x[x > thr].mean():.3f}-mean: ' for i, x in enumerate(k): - print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg + s += '%i,%i, ' % (round(x[0]), round(x[1])) + if verbose: + LOGGER.info(s[:-2]) return k - if isinstance(path, str): # *.yaml file - with open(path) as f: + if isinstance(dataset, str): # *.yaml file + with open(dataset, errors='ignore') as f: data_dict = yaml.safe_load(f) # model dict from utils.datasets import LoadImagesAndLabels dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True) - else: - dataset = path # dataset # Get label wh shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True) @@ -116,19 +119,19 @@ def print_results(k): # Filter i = (wh0 < 3.0).any(1).sum() if i: - print(f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.') + LOGGER.info(f'{PREFIX}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.') wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels # wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1 # Kmeans calculation - print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...') + LOGGER.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...') s = wh.std(0) # sigmas for whitening k, dist = kmeans(wh / s, n, iter=30) # points, mean distance - assert len(k) == n, print(f'{prefix}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}') + assert len(k) == n, f'{PREFIX}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}' k *= s wh = torch.tensor(wh, dtype=torch.float32) # filtered wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered - k = print_results(k) + k = print_results(k, verbose=False) # Plot # k, d = [None] * 20, [None] * 20 @@ -145,17 +148,17 @@ def print_results(k): # Evolve npr = np.random f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma - pbar = tqdm(range(gen), desc=f'{prefix}Evolving anchors with Genetic Algorithm:') # progress bar + pbar = tqdm(range(gen), desc=f'{PREFIX}Evolving anchors with Genetic Algorithm:') # progress bar for _ in pbar: v = np.ones(sh) while (v == 1).all(): # mutate until a change occurs (prevent duplicates) - v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0) + v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0) kg = (k.copy() * v).clip(min=2.0) fg = anchor_fitness(kg) if fg > f: f, k = fg, kg.copy() - pbar.desc = f'{prefix}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}' + pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}' if verbose: - print_results(k) + print_results(k, verbose) return print_results(k) diff --git a/utils/autobatch.py b/utils/autobatch.py new file mode 100644 index 0000000000..4fbf32bc9a --- /dev/null +++ b/utils/autobatch.py @@ -0,0 +1,57 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Auto-batch utils +""" + +from copy import deepcopy + +import numpy as np +import torch +from torch.cuda import amp + +from utils.general import LOGGER, colorstr +from utils.torch_utils import profile + + +def check_train_batch_size(model, imgsz=640): + # Check training batch size + with amp.autocast(): + return autobatch(deepcopy(model).train(), imgsz) # compute optimal batch size + + +def autobatch(model, imgsz=640, fraction=0.9, batch_size=16): + # Automatically estimate best batch size to use `fraction` of available CUDA memory + # Usage: + # import torch + # from utils.autobatch import autobatch + # model = torch.hub.load('ultralytics/yolov3', 'yolov3', autoshape=False) + # print(autobatch(model)) + + prefix = colorstr('AutoBatch: ') + LOGGER.info(f'{prefix}Computing optimal batch size for --imgsz {imgsz}') + device = next(model.parameters()).device # get model device + if device.type == 'cpu': + LOGGER.info(f'{prefix}CUDA not detected, using default CPU batch-size {batch_size}') + return batch_size + + d = str(device).upper() # 'CUDA:0' + properties = torch.cuda.get_device_properties(device) # device properties + t = properties.total_memory / 1024 ** 3 # (GiB) + r = torch.cuda.memory_reserved(device) / 1024 ** 3 # (GiB) + a = torch.cuda.memory_allocated(device) / 1024 ** 3 # (GiB) + f = t - (r + a) # free inside reserved + LOGGER.info(f'{prefix}{d} ({properties.name}) {t:.2f}G total, {r:.2f}G reserved, {a:.2f}G allocated, {f:.2f}G free') + + batch_sizes = [1, 2, 4, 8, 16] + try: + img = [torch.zeros(b, 3, imgsz, imgsz) for b in batch_sizes] + y = profile(img, model, n=3, device=device) + except Exception as e: + LOGGER.warning(f'{prefix}{e}') + + y = [x[2] for x in y if x] # memory [2] + batch_sizes = batch_sizes[:len(y)] + p = np.polyfit(batch_sizes, y, deg=1) # first degree polynomial fit + b = int((f * fraction - p[1]) / p[0]) # y intercept (optimal batch size) + LOGGER.info(f'{prefix}Using batch-size {b} for {d} {t * fraction:.2f}G/{t:.2f}G ({fraction * 100:.0f}%)') + return b diff --git a/utils/aws/mime.sh b/utils/aws/mime.sh deleted file mode 100644 index c319a83cfb..0000000000 --- a/utils/aws/mime.sh +++ /dev/null @@ -1,26 +0,0 @@ -# AWS EC2 instance startup 'MIME' script https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/ -# This script will run on every instance restart, not only on first start -# --- DO NOT COPY ABOVE COMMENTS WHEN PASTING INTO USERDATA --- - -Content-Type: multipart/mixed; boundary="//" -MIME-Version: 1.0 - ---// -Content-Type: text/cloud-config; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Content-Disposition: attachment; filename="cloud-config.txt" - -#cloud-config -cloud_final_modules: -- [scripts-user, always] - ---// -Content-Type: text/x-shellscript; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Content-Disposition: attachment; filename="userdata.txt" - -#!/bin/bash -# --- paste contents of userdata.sh here --- ---// diff --git a/utils/aws/resume.py b/utils/aws/resume.py deleted file mode 100644 index 4b0d4246b5..0000000000 --- a/utils/aws/resume.py +++ /dev/null @@ -1,37 +0,0 @@ -# Resume all interrupted trainings in yolov5/ dir including DDP trainings -# Usage: $ python utils/aws/resume.py - -import os -import sys -from pathlib import Path - -import torch -import yaml - -sys.path.append('./') # to run '$ python *.py' files in subdirectories - -port = 0 # --master_port -path = Path('').resolve() -for last in path.rglob('*/**/last.pt'): - ckpt = torch.load(last) - if ckpt['optimizer'] is None: - continue - - # Load opt.yaml - with open(last.parent.parent / 'opt.yaml') as f: - opt = yaml.safe_load(f) - - # Get device count - d = opt['device'].split(',') # devices - nd = len(d) # number of devices - ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel - - if ddp: # multi-GPU - port += 1 - cmd = f'python -m torch.distributed.launch --nproc_per_node {nd} --master_port {port} train.py --resume {last}' - else: # single-GPU - cmd = f'python train.py --resume {last}' - - cmd += ' > /dev/null 2>&1 &' # redirect output to dev/null and run in daemon thread - print(cmd) - os.system(cmd) diff --git a/utils/aws/userdata.sh b/utils/aws/userdata.sh deleted file mode 100644 index 5846fedb16..0000000000 --- a/utils/aws/userdata.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# AWS EC2 instance startup script https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html -# This script will run only once on first instance start (for a re-start script see mime.sh) -# /home/ubuntu (ubuntu) or /home/ec2-user (amazon-linux) is working dir -# Use >300 GB SSD - -cd home/ubuntu -if [ ! -d yolov5 ]; then - echo "Running first-time script." # install dependencies, download COCO, pull Docker - git clone https://github.com/ultralytics/yolov5 -b master && sudo chmod -R 777 yolov5 - cd yolov5 - bash data/scripts/get_coco.sh && echo "Data done." & - sudo docker pull ultralytics/yolov5:latest && echo "Docker done." & - python -m pip install --upgrade pip && pip install -r requirements.txt && python detect.py && echo "Requirements done." & - wait && echo "All tasks done." # finish background tasks -else - echo "Running re-start script." # resume interrupted runs - i=0 - list=$(sudo docker ps -qa) # container list i.e. $'one\ntwo\nthree\nfour' - while IFS= read -r id; do - ((i++)) - echo "restarting container $i: $id" - sudo docker start $id - # sudo docker exec -it $id python train.py --resume # single-GPU - sudo docker exec -d $id python utils/aws/resume.py # multi-scenario - done <<<"$list" -fi diff --git a/utils/callbacks.py b/utils/callbacks.py new file mode 100644 index 0000000000..43e81a7c51 --- /dev/null +++ b/utils/callbacks.py @@ -0,0 +1,76 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Callback utils +""" + + +class Callbacks: + """" + Handles all registered callbacks for Hooks + """ + + # Define the available callbacks + _callbacks = { + 'on_pretrain_routine_start': [], + 'on_pretrain_routine_end': [], + + 'on_train_start': [], + 'on_train_epoch_start': [], + 'on_train_batch_start': [], + 'optimizer_step': [], + 'on_before_zero_grad': [], + 'on_train_batch_end': [], + 'on_train_epoch_end': [], + + 'on_val_start': [], + 'on_val_batch_start': [], + 'on_val_image_end': [], + 'on_val_batch_end': [], + 'on_val_end': [], + + 'on_fit_epoch_end': [], # fit = train + val + 'on_model_save': [], + 'on_train_end': [], + + 'teardown': [], + } + + def register_action(self, hook, name='', callback=None): + """ + Register a new action to a callback hook + + Args: + hook The callback hook name to register the action to + name The name of the action for later reference + callback The callback to fire + """ + assert hook in self._callbacks, f"hook '{hook}' not found in callbacks {self._callbacks}" + assert callable(callback), f"callback '{callback}' is not callable" + self._callbacks[hook].append({'name': name, 'callback': callback}) + + def get_registered_actions(self, hook=None): + """" + Returns all the registered actions by callback hook + + Args: + hook The name of the hook to check, defaults to all + """ + if hook: + return self._callbacks[hook] + else: + return self._callbacks + + def run(self, hook, *args, **kwargs): + """ + Loop through the registered actions and fire all callbacks + + Args: + hook The name of the hook to check, defaults to all + args Arguments to receive from + kwargs Keyword Arguments to receive from + """ + + assert hook in self._callbacks, f"hook '{hook}' not found in callbacks {self._callbacks}" + + for logger in self._callbacks[hook]: + logger['callback'](*args, **kwargs) diff --git a/utils/datasets.py b/utils/datasets.py index 35aa430aaa..462d561a56 100755 --- a/utils/datasets.py +++ b/utils/datasets.py @@ -1,35 +1,41 @@ -# Dataset utils and dataloaders +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Dataloaders and dataset utils +""" import glob import hashlib -import logging -import math +import json import os import random import shutil import time from itertools import repeat -from multiprocessing.pool import ThreadPool +from multiprocessing.pool import Pool, ThreadPool from pathlib import Path from threading import Thread +from zipfile import ZipFile import cv2 import numpy as np import torch import torch.nn.functional as F -from PIL import Image, ExifTags -from torch.utils.data import Dataset +import yaml +from PIL import ExifTags, Image, ImageOps +from torch.utils.data import DataLoader, Dataset, dataloader, distributed from tqdm import tqdm -from utils.general import check_requirements, xyxy2xywh, xywh2xyxy, xywhn2xyxy, xyn2xy, segment2box, segments2boxes, \ - resample_segments, clean_str +from utils.augmentations import Albumentations, augment_hsv, copy_paste, letterbox, mixup, random_perspective +from utils.general import (LOGGER, check_dataset, check_requirements, check_yaml, clean_str, segments2boxes, xyn2xy, + xywh2xyxy, xywhn2xyxy, xyxy2xywhn) from utils.torch_utils import torch_distributed_zero_first # Parameters -help_url = 'https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' -img_formats = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo'] # acceptable image suffixes -vid_formats = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv'] # acceptable video suffixes -logger = logging.getLogger(__name__) +HELP_URL = 'https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' +IMG_FORMATS = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo'] # acceptable image suffixes +VID_FORMATS = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv'] # acceptable video suffixes +WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) # DPP +NUM_THREADS = min(8, os.cpu_count()) # number of multiprocessing threads # Get orientation exif tag for orientation in ExifTags.TAGS.keys(): @@ -60,36 +66,63 @@ def exif_size(img): return s -def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=False, cache=False, pad=0.0, rect=False, - rank=-1, world_size=1, workers=8, image_weights=False, quad=False, prefix=''): - # Make sure only the first process in DDP process the dataset first, and the following others can use the cache - with torch_distributed_zero_first(rank): +def exif_transpose(image): + """ + Transpose a PIL image accordingly if it has an EXIF Orientation tag. + Inplace version of https://github.com/python-pillow/Pillow/blob/master/src/PIL/ImageOps.py exif_transpose() + + :param image: The image to transpose. + :return: An image. + """ + exif = image.getexif() + orientation = exif.get(0x0112, 1) # default 1 + if orientation > 1: + method = {2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 4: Image.FLIP_TOP_BOTTOM, + 5: Image.TRANSPOSE, + 6: Image.ROTATE_270, + 7: Image.TRANSVERSE, + 8: Image.ROTATE_90, + }.get(orientation) + if method is not None: + image = image.transpose(method) + del exif[0x0112] + image.info["exif"] = exif.tobytes() + return image + + +def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=None, augment=False, cache=False, pad=0.0, + rect=False, rank=-1, workers=8, image_weights=False, quad=False, prefix='', shuffle=False): + if rect and shuffle: + LOGGER.warning('WARNING: --rect is incompatible with DataLoader shuffle, setting shuffle=False') + shuffle = False + with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP dataset = LoadImagesAndLabels(path, imgsz, batch_size, - augment=augment, # augment images - hyp=hyp, # augmentation hyperparameters - rect=rect, # rectangular training + augment=augment, # augmentation + hyp=hyp, # hyperparameters + rect=rect, # rectangular batches cache_images=cache, - single_cls=opt.single_cls, + single_cls=single_cls, stride=int(stride), pad=pad, image_weights=image_weights, prefix=prefix) batch_size = min(batch_size, len(dataset)) - nw = min([os.cpu_count() // world_size, batch_size if batch_size > 1 else 0, workers]) # number of workers - sampler = torch.utils.data.distributed.DistributedSampler(dataset) if rank != -1 else None - loader = torch.utils.data.DataLoader if image_weights else InfiniteDataLoader - # Use torch.utils.data.DataLoader() if dataset.properties will update during training else InfiniteDataLoader() - dataloader = loader(dataset, - batch_size=batch_size, - num_workers=nw, - sampler=sampler, - pin_memory=True, - collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn) - return dataloader, dataset - - -class InfiniteDataLoader(torch.utils.data.dataloader.DataLoader): + nw = min([os.cpu_count() // WORLD_SIZE, batch_size if batch_size > 1 else 0, workers]) # number of workers + sampler = None if rank == -1 else distributed.DistributedSampler(dataset, shuffle=shuffle) + loader = DataLoader if image_weights else InfiniteDataLoader # only DataLoader allows for attribute updates + return loader(dataset, + batch_size=batch_size, + shuffle=shuffle and sampler is None, + num_workers=nw, + sampler=sampler, + pin_memory=True, + collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn), dataset + + +class InfiniteDataLoader(dataloader.DataLoader): """ Dataloader that reuses workers Uses same syntax as vanilla DataLoader @@ -108,7 +141,7 @@ def __iter__(self): yield next(self.iterator) -class _RepeatSampler(object): +class _RepeatSampler: """ Sampler that repeats forever Args: @@ -123,9 +156,10 @@ def __iter__(self): yield from iter(self.sampler) -class LoadImages: # for inference - def __init__(self, path, img_size=640, stride=32): - p = str(Path(path).absolute()) # os-agnostic absolute path +class LoadImages: + # image/video dataloader, i.e. `python detect.py --source image.jpg/vid.mp4` + def __init__(self, path, img_size=640, stride=32, auto=True): + p = str(Path(path).resolve()) # os-agnostic absolute path if '*' in p: files = sorted(glob.glob(p, recursive=True)) # glob elif os.path.isdir(p): @@ -135,8 +169,8 @@ def __init__(self, path, img_size=640, stride=32): else: raise Exception(f'ERROR: {p} does not exist') - images = [x for x in files if x.split('.')[-1].lower() in img_formats] - videos = [x for x in files if x.split('.')[-1].lower() in vid_formats] + images = [x for x in files if x.split('.')[-1].lower() in IMG_FORMATS] + videos = [x for x in files if x.split('.')[-1].lower() in VID_FORMATS] ni, nv = len(images), len(videos) self.img_size = img_size @@ -145,12 +179,13 @@ def __init__(self, path, img_size=640, stride=32): self.nf = ni + nv # number of files self.video_flag = [False] * ni + [True] * nv self.mode = 'image' + self.auto = auto if any(videos): self.new_video(videos[0]) # new video else: self.cap = None assert self.nf > 0, f'No images or videos found in {p}. ' \ - f'Supported formats are:\nimages: {img_formats}\nvideos: {vid_formats}' + f'Supported formats are:\nimages: {IMG_FORMATS}\nvideos: {VID_FORMATS}' def __iter__(self): self.count = 0 @@ -176,23 +211,23 @@ def __next__(self): ret_val, img0 = self.cap.read() self.frame += 1 - print(f'video {self.count + 1}/{self.nf} ({self.frame}/{self.frames}) {path}: ', end='') + s = f'video {self.count + 1}/{self.nf} ({self.frame}/{self.frames}) {path}: ' else: # Read image self.count += 1 img0 = cv2.imread(path) # BGR - assert img0 is not None, 'Image Not Found ' + path - print(f'image {self.count}/{self.nf} {path}: ', end='') + assert img0 is not None, f'Image Not Found {path}' + s = f'image {self.count}/{self.nf} {path}: ' # Padded resize - img = letterbox(img0, self.img_size, stride=self.stride)[0] + img = letterbox(img0, self.img_size, stride=self.stride, auto=self.auto)[0] # Convert - img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 + img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB img = np.ascontiguousarray(img) - return path, img, img0, self.cap + return path, img, img0, self.cap, s def new_video(self, path): self.frame = 0 @@ -204,18 +239,12 @@ def __len__(self): class LoadWebcam: # for inference + # local webcam dataloader, i.e. `python detect.py --source 0` def __init__(self, pipe='0', img_size=640, stride=32): self.img_size = img_size self.stride = stride - - if pipe.isnumeric(): - pipe = eval(pipe) # local camera - # pipe = 'rtsp://192.168.1.64/1' # IP camera - # pipe = 'rtsp://username:password@192.168.1.64/1' # IP camera with login - # pipe = 'http://wmccpinetop.axiscam.net/mjpg/video.mjpg' # IP golf camera - - self.pipe = pipe - self.cap = cv2.VideoCapture(pipe) # video capture object + self.pipe = eval(pipe) if pipe.isnumeric() else pipe + self.cap = cv2.VideoCapture(self.pipe) # video capture object self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) # set buffer size def __iter__(self): @@ -230,45 +259,36 @@ def __next__(self): raise StopIteration # Read frame - if self.pipe == 0: # local camera - ret_val, img0 = self.cap.read() - img0 = cv2.flip(img0, 1) # flip left-right - else: # IP camera - n = 0 - while True: - n += 1 - self.cap.grab() - if n % 30 == 0: # skip frames - ret_val, img0 = self.cap.retrieve() - if ret_val: - break + ret_val, img0 = self.cap.read() + img0 = cv2.flip(img0, 1) # flip left-right # Print assert ret_val, f'Camera Error {self.pipe}' img_path = 'webcam.jpg' - print(f'webcam {self.count}: ', end='') + s = f'webcam {self.count}: ' # Padded resize img = letterbox(img0, self.img_size, stride=self.stride)[0] # Convert - img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 + img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB img = np.ascontiguousarray(img) - return img_path, img, img0, None + return img_path, img, img0, None, s def __len__(self): return 0 -class LoadStreams: # multiple IP or RTSP cameras - def __init__(self, sources='streams.txt', img_size=640, stride=32): +class LoadStreams: + # streamloader, i.e. `python detect.py --source 'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP streams` + def __init__(self, sources='streams.txt', img_size=640, stride=32, auto=True): self.mode = 'stream' self.img_size = img_size self.stride = stride if os.path.isfile(sources): - with open(sources, 'r') as f: + with open(sources) as f: sources = [x.strip() for x in f.read().strip().splitlines() if len(x.strip())] else: sources = [sources] @@ -276,43 +296,49 @@ def __init__(self, sources='streams.txt', img_size=640, stride=32): n = len(sources) self.imgs, self.fps, self.frames, self.threads = [None] * n, [0] * n, [0] * n, [None] * n self.sources = [clean_str(x) for x in sources] # clean source names for later + self.auto = auto for i, s in enumerate(sources): # index, source # Start thread to read frames from video stream - print(f'{i + 1}/{n}: {s}... ', end='') + st = f'{i + 1}/{n}: {s}... ' if 'youtube.com/' in s or 'youtu.be/' in s: # if source is YouTube video check_requirements(('pafy', 'youtube_dl')) import pafy s = pafy.new(s).getbest(preftype="mp4").url # YouTube URL s = eval(s) if s.isnumeric() else s # i.e. s = '0' local webcam cap = cv2.VideoCapture(s) - assert cap.isOpened(), f'Failed to open {s}' + assert cap.isOpened(), f'{st}Failed to open {s}' w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) self.fps[i] = max(cap.get(cv2.CAP_PROP_FPS) % 100, 0) or 30.0 # 30 FPS fallback self.frames[i] = max(int(cap.get(cv2.CAP_PROP_FRAME_COUNT)), 0) or float('inf') # infinite stream fallback _, self.imgs[i] = cap.read() # guarantee first frame - self.threads[i] = Thread(target=self.update, args=([i, cap]), daemon=True) - print(f" success ({self.frames[i]} frames {w}x{h} at {self.fps[i]:.2f} FPS)") + self.threads[i] = Thread(target=self.update, args=([i, cap, s]), daemon=True) + LOGGER.info(f"{st} Success ({self.frames[i]} frames {w}x{h} at {self.fps[i]:.2f} FPS)") self.threads[i].start() - print('') # newline + LOGGER.info('') # newline # check for common shapes - s = np.stack([letterbox(x, self.img_size, stride=self.stride)[0].shape for x in self.imgs], 0) # shapes + s = np.stack([letterbox(x, self.img_size, stride=self.stride, auto=self.auto)[0].shape for x in self.imgs]) self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal if not self.rect: - print('WARNING: Different stream shapes detected. For optimal performance supply similarly-shaped streams.') + LOGGER.warning('WARNING: Stream shapes differ. For optimal performance supply similarly-shaped streams.') - def update(self, i, cap): + def update(self, i, cap, stream): # Read stream `i` frames in daemon thread - n, f = 0, self.frames[i] + n, f, read = 0, self.frames[i], 1 # frame number, frame array, inference every 'read' frame while cap.isOpened() and n < f: n += 1 # _, self.imgs[index] = cap.read() cap.grab() - if n % 4: # read every 4th frame + if n % read == 0: success, im = cap.retrieve() - self.imgs[i] = im if success else self.imgs[i] * 0 + if success: + self.imgs[i] = im + else: + LOGGER.warning('WARNING: Video stream unresponsive, please check your IP camera connection.') + self.imgs[i] *= 0 + cap.open(stream) # re-open stream if signal was lost time.sleep(1 / self.fps[i]) # wait time def __iter__(self): @@ -327,28 +353,31 @@ def __next__(self): # Letterbox img0 = self.imgs.copy() - img = [letterbox(x, self.img_size, auto=self.rect, stride=self.stride)[0] for x in img0] + img = [letterbox(x, self.img_size, stride=self.stride, auto=self.rect and self.auto)[0] for x in img0] # Stack img = np.stack(img, 0) # Convert - img = img[:, :, :, ::-1].transpose(0, 3, 1, 2) # BGR to RGB, to bsx3x416x416 + img = img[..., ::-1].transpose((0, 3, 1, 2)) # BGR to RGB, BHWC to BCHW img = np.ascontiguousarray(img) - return self.sources, img, img0, None + return self.sources, img, img0, None, '' def __len__(self): - return 0 # 1E12 frames = 32 streams at 30 FPS for 30 years + return len(self.sources) # 1E12 frames = 32 streams at 30 FPS for 30 years def img2label_paths(img_paths): # Define label paths as a function of image paths sa, sb = os.sep + 'images' + os.sep, os.sep + 'labels' + os.sep # /images/, /labels/ substrings - return ['txt'.join(x.replace(sa, sb, 1).rsplit(x.split('.')[-1], 1)) for x in img_paths] + return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths] + +class LoadImagesAndLabels(Dataset): + # train_loader/val_loader, loads images and labels for training and validation + cache_version = 0.6 # dataset labels *.cache version -class LoadImagesAndLabels(Dataset): # for training/testing def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, rect=False, image_weights=False, cache_images=False, single_cls=False, stride=32, pad=0.0, prefix=''): self.img_size = img_size @@ -360,6 +389,7 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r self.mosaic_border = [-img_size // 2, -img_size // 2] self.stride = stride self.path = path + self.albumentations = Albumentations() if augment else None try: f = [] # image files @@ -367,29 +397,29 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r p = Path(p) # os-agnostic if p.is_dir(): # dir f += glob.glob(str(p / '**' / '*.*'), recursive=True) - # f = list(p.rglob('**/*.*')) # pathlib + # f = list(p.rglob('*.*')) # pathlib elif p.is_file(): # file - with open(p, 'r') as t: + with open(p) as t: t = t.read().strip().splitlines() parent = str(p.parent) + os.sep f += [x.replace('./', parent) if x.startswith('./') else x for x in t] # local to global path # f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib) else: raise Exception(f'{prefix}{p} does not exist') - self.img_files = sorted([x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in img_formats]) - # self.img_files = sorted([x for x in f if x.suffix[1:].lower() in img_formats]) # pathlib + self.img_files = sorted(x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS) + # self.img_files = sorted([x for x in f if x.suffix[1:].lower() in IMG_FORMATS]) # pathlib assert self.img_files, f'{prefix}No images found' except Exception as e: - raise Exception(f'{prefix}Error loading data from {path}: {e}\nSee {help_url}') + raise Exception(f'{prefix}Error loading data from {path}: {e}\nSee {HELP_URL}') # Check cache self.label_files = img2label_paths(self.img_files) # labels - cache_path = (p if p.is_file() else Path(self.label_files[0]).parent).with_suffix('.cache') # cached labels - if cache_path.is_file(): - cache, exists = torch.load(cache_path), True # load - if cache['hash'] != get_hash(self.label_files + self.img_files): # changed - cache, exists = self.cache_labels(cache_path, prefix), False # re-cache - else: + cache_path = (p if p.is_file() else Path(self.label_files[0]).parent).with_suffix('.cache') + try: + cache, exists = np.load(cache_path, allow_pickle=True).item(), True # load dict + assert cache['version'] == self.cache_version # same version + assert cache['hash'] == get_hash(self.label_files + self.img_files) # same hash + except: cache, exists = self.cache_labels(cache_path, prefix), False # cache # Display cache @@ -397,20 +427,17 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r if exists: d = f"Scanning '{cache_path}' images and labels... {nf} found, {nm} missing, {ne} empty, {nc} corrupted" tqdm(None, desc=prefix + d, total=n, initial=n) # display cache results - assert nf > 0 or not augment, f'{prefix}No labels in {cache_path}. Can not train without labels. See {help_url}' + if cache['msgs']: + LOGGER.info('\n'.join(cache['msgs'])) # display warnings + assert nf > 0 or not augment, f'{prefix}No labels in {cache_path}. Can not train without labels. See {HELP_URL}' # Read cache - cache.pop('hash') # remove hash - cache.pop('version') # remove version + [cache.pop(k) for k in ('hash', 'version', 'msgs')] # remove items labels, shapes, self.segments = zip(*cache.values()) self.labels = list(labels) self.shapes = np.array(shapes, dtype=np.float64) self.img_files = list(cache.keys()) # update self.label_files = img2label_paths(cache.keys()) # update - if single_cls: - for x in self.labels: - x[:, 0] = 0 - n = len(shapes) # number of images bi = np.floor(np.arange(n) / batch_size).astype(np.int) # batch index nb = bi[-1] + 1 # number of batches @@ -418,6 +445,20 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r self.n = n self.indices = range(n) + # Update labels + include_class = [] # filter labels to include only these classes (optional) + include_class_array = np.array(include_class).reshape(1, -1) + for i, (label, segment) in enumerate(zip(self.labels, self.segments)): + if include_class: + j = (label[:, 0:1] == include_class_array).any(1) + self.labels[i] = label[j] + if segment: + self.segments[i] = segment[j] + if single_cls: # single-class training, merge all classes into 0 + self.labels[i][:, 0] = 0 + if segment: + self.segments[i][:, 0] = 0 + # Rectangular Training if self.rect: # Sort by aspect ratio @@ -443,74 +484,61 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r self.batch_shapes = np.ceil(np.array(shapes) * img_size / stride + pad).astype(np.int) * stride # Cache images into memory for faster training (WARNING: large datasets may exceed system RAM) - self.imgs = [None] * n + self.imgs, self.img_npy = [None] * n, [None] * n if cache_images: + if cache_images == 'disk': + self.im_cache_dir = Path(Path(self.img_files[0]).parent.as_posix() + '_npy') + self.img_npy = [self.im_cache_dir / Path(f).with_suffix('.npy').name for f in self.img_files] + self.im_cache_dir.mkdir(parents=True, exist_ok=True) gb = 0 # Gigabytes of cached images self.img_hw0, self.img_hw = [None] * n, [None] * n - results = ThreadPool(8).imap(lambda x: load_image(*x), zip(repeat(self), range(n))) # 8 threads + results = ThreadPool(NUM_THREADS).imap(lambda x: load_image(*x), zip(repeat(self), range(n))) pbar = tqdm(enumerate(results), total=n) for i, x in pbar: - self.imgs[i], self.img_hw0[i], self.img_hw[i] = x # img, hw_original, hw_resized = load_image(self, i) - gb += self.imgs[i].nbytes - pbar.desc = f'{prefix}Caching images ({gb / 1E9:.1f}GB)' + if cache_images == 'disk': + if not self.img_npy[i].exists(): + np.save(self.img_npy[i].as_posix(), x[0]) + gb += self.img_npy[i].stat().st_size + else: + self.imgs[i], self.img_hw0[i], self.img_hw[i] = x # im, hw_orig, hw_resized = load_image(self, i) + gb += self.imgs[i].nbytes + pbar.desc = f'{prefix}Caching images ({gb / 1E9:.1f}GB {cache_images})' pbar.close() def cache_labels(self, path=Path('./labels.cache'), prefix=''): # Cache dataset labels, check images and read shapes x = {} # dict - nm, nf, ne, nc = 0, 0, 0, 0 # number missing, found, empty, duplicate - pbar = tqdm(zip(self.img_files, self.label_files), desc='Scanning images', total=len(self.img_files)) - for i, (im_file, lb_file) in enumerate(pbar): - try: - # verify images - im = Image.open(im_file) - im.verify() # PIL verify - shape = exif_size(im) # image size - segments = [] # instance segments - assert (shape[0] > 9) & (shape[1] > 9), f'image size {shape} <10 pixels' - assert im.format.lower() in img_formats, f'invalid image format {im.format}' - - # verify labels - if os.path.isfile(lb_file): - nf += 1 # label found - with open(lb_file, 'r') as f: - l = [x.split() for x in f.read().strip().splitlines() if len(x)] - if any([len(x) > 8 for x in l]): # is segment - classes = np.array([x[0] for x in l], dtype=np.float32) - segments = [np.array(x[1:], dtype=np.float32).reshape(-1, 2) for x in l] # (cls, xy1...) - l = np.concatenate((classes.reshape(-1, 1), segments2boxes(segments)), 1) # (cls, xywh) - l = np.array(l, dtype=np.float32) - if len(l): - assert l.shape[1] == 5, 'labels require 5 columns each' - assert (l >= 0).all(), 'negative labels' - assert (l[:, 1:] <= 1).all(), 'non-normalized or out of bounds coordinate labels' - assert np.unique(l, axis=0).shape[0] == l.shape[0], 'duplicate labels' - else: - ne += 1 # label empty - l = np.zeros((0, 5), dtype=np.float32) - else: - nm += 1 # label missing - l = np.zeros((0, 5), dtype=np.float32) - x[im_file] = [l, shape, segments] - except Exception as e: - nc += 1 - logging.info(f'{prefix}WARNING: Ignoring corrupted image and/or label {im_file}: {e}') - - pbar.desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels... " \ - f"{nf} found, {nm} missing, {ne} empty, {nc} corrupted" - pbar.close() + nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages + desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..." + with Pool(NUM_THREADS) as pool: + pbar = tqdm(pool.imap(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))), + desc=desc, total=len(self.img_files)) + for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar: + nm += nm_f + nf += nf_f + ne += ne_f + nc += nc_f + if im_file: + x[im_file] = [l, shape, segments] + if msg: + msgs.append(msg) + pbar.desc = f"{desc}{nf} found, {nm} missing, {ne} empty, {nc} corrupted" + pbar.close() + if msgs: + LOGGER.info('\n'.join(msgs)) if nf == 0: - logging.info(f'{prefix}WARNING: No labels found in {path}. See {help_url}') - + LOGGER.warning(f'{prefix}WARNING: No labels found in {path}. See {HELP_URL}') x['hash'] = get_hash(self.label_files + self.img_files) - x['results'] = nf, nm, ne, nc, i + 1 - x['version'] = 0.2 # cache version + x['results'] = nf, nm, ne, nc, len(self.img_files) + x['msgs'] = msgs # warnings + x['version'] = self.cache_version # cache version try: - torch.save(x, path) # save cache for next time - logging.info(f'{prefix}New cache created: {path}') + np.save(path, x) # save cache for next time + path.with_suffix('.cache.npy').rename(path) # remove .npy suffix + LOGGER.info(f'{prefix}New cache created: {path}') except Exception as e: - logging.info(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}') # path not writeable + LOGGER.warning(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}') # not writeable return x def __len__(self): @@ -532,12 +560,9 @@ def __getitem__(self, index): img, labels = load_mosaic(self, index) shapes = None - # MixUp https://arxiv.org/pdf/1710.09412.pdf + # MixUp augmentation if random.random() < hyp['mixup']: - img2, labels2 = load_mosaic(self, random.randint(0, self.n - 1)) - r = np.random.beta(8.0, 8.0) # mixup ratio, alpha=beta=8.0 - img = (img * r + img2 * (1 - r)).astype(np.uint8) - labels = np.concatenate((labels, labels2), 0) + img, labels = mixup(img, labels, *load_mosaic(self, random.randint(0, self.n - 1))) else: # Load image @@ -552,9 +577,7 @@ def __getitem__(self, index): if labels.size: # normalized xywh to pixel xyxy format labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1]) - if self.augment: - # Augment imagespace - if not mosaic: + if self.augment: img, labels = random_perspective(img, labels, degrees=hyp['degrees'], translate=hyp['translate'], @@ -562,38 +585,39 @@ def __getitem__(self, index): shear=hyp['shear'], perspective=hyp['perspective']) - # Augment colorspace - augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) + nl = len(labels) # number of labels + if nl: + labels[:, 1:5] = xyxy2xywhn(labels[:, 1:5], w=img.shape[1], h=img.shape[0], clip=True, eps=1E-3) - # Apply cutouts - # if random.random() < 0.9: - # labels = cutout(img, labels) + if self.augment: + # Albumentations + img, labels = self.albumentations(img, labels) + nl = len(labels) # update after albumentations - nL = len(labels) # number of labels - if nL: - labels[:, 1:5] = xyxy2xywh(labels[:, 1:5]) # convert xyxy to xywh - labels[:, [2, 4]] /= img.shape[0] # normalized height 0-1 - labels[:, [1, 3]] /= img.shape[1] # normalized width 0-1 + # HSV color-space + augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) - if self.augment: - # flip up-down + # Flip up-down if random.random() < hyp['flipud']: img = np.flipud(img) - if nL: + if nl: labels[:, 2] = 1 - labels[:, 2] - # flip left-right + # Flip left-right if random.random() < hyp['fliplr']: img = np.fliplr(img) - if nL: + if nl: labels[:, 1] = 1 - labels[:, 1] - labels_out = torch.zeros((nL, 6)) - if nL: + # Cutouts + # labels = cutout(img, labels, p=0.5) + + labels_out = torch.zeros((nl, 6)) + if nl: labels_out[:, 1:] = torch.from_numpy(labels) # Convert - img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 + img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB img = np.ascontiguousarray(img) return torch.from_numpy(img), labels_out, self.img_files[index], shapes @@ -611,13 +635,13 @@ def collate_fn4(batch): n = len(shapes) // 4 img4, label4, path4, shapes4 = [], [], path[:n], shapes[:n] - ho = torch.tensor([[0., 0, 0, 1, 0, 0]]) - wo = torch.tensor([[0., 0, 1, 0, 0, 0]]) - s = torch.tensor([[1, 1, .5, .5, .5, .5]]) # scale + ho = torch.tensor([[0.0, 0, 0, 1, 0, 0]]) + wo = torch.tensor([[0.0, 0, 1, 0, 0, 0]]) + s = torch.tensor([[1, 1, 0.5, 0.5, 0.5, 0.5]]) # scale for i in range(n): # zidane torch.zeros(16,3,720,1280) # BCHW i *= 4 if random.random() < 0.5: - im = F.interpolate(img[i].unsqueeze(0).float(), scale_factor=2., mode='bilinear', align_corners=False)[ + im = F.interpolate(img[i].unsqueeze(0).float(), scale_factor=2.0, mode='bilinear', align_corners=False)[ 0].type(img[i].type()) l = label[i] else: @@ -633,55 +657,34 @@ def collate_fn4(batch): # Ancillary functions -------------------------------------------------------------------------------------------------- -def load_image(self, index): - # loads 1 image from dataset, returns img, original hw, resized hw - img = self.imgs[index] - if img is None: # not cached - path = self.img_files[index] - img = cv2.imread(path) # BGR - assert img is not None, 'Image Not Found ' + path - h0, w0 = img.shape[:2] # orig hw +def load_image(self, i): + # loads 1 image from dataset index 'i', returns im, original hw, resized hw + im = self.imgs[i] + if im is None: # not cached in ram + npy = self.img_npy[i] + if npy and npy.exists(): # load npy + im = np.load(npy) + else: # read image + path = self.img_files[i] + im = cv2.imread(path) # BGR + assert im is not None, f'Image Not Found {path}' + h0, w0 = im.shape[:2] # orig hw r = self.img_size / max(h0, w0) # ratio if r != 1: # if sizes are not equal - img = cv2.resize(img, (int(w0 * r), int(h0 * r)), - interpolation=cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR) - return img, (h0, w0), img.shape[:2] # img, hw_original, hw_resized + im = cv2.resize(im, (int(w0 * r), int(h0 * r)), + interpolation=cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR) + return im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized else: - return self.imgs[index], self.img_hw0[index], self.img_hw[index] # img, hw_original, hw_resized - - -def augment_hsv(img, hgain=0.5, sgain=0.5, vgain=0.5): - r = np.random.uniform(-1, 1, 3) * [hgain, sgain, vgain] + 1 # random gains - hue, sat, val = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV)) - dtype = img.dtype # uint8 - - x = np.arange(0, 256, dtype=np.int16) - lut_hue = ((x * r[0]) % 180).astype(dtype) - lut_sat = np.clip(x * r[1], 0, 255).astype(dtype) - lut_val = np.clip(x * r[2], 0, 255).astype(dtype) - - img_hsv = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val))).astype(dtype) - cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR, dst=img) # no return needed - - -def hist_equalize(img, clahe=True, bgr=False): - # Equalize histogram on BGR image 'img' with img.shape(n,m,3) and range 0-255 - yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV if bgr else cv2.COLOR_RGB2YUV) - if clahe: - c = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) - yuv[:, :, 0] = c.apply(yuv[:, :, 0]) - else: - yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0]) # equalize Y channel histogram - return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR if bgr else cv2.COLOR_YUV2RGB) # convert YUV image to RGB + return self.imgs[i], self.img_hw0[i], self.img_hw[i] # im, hw_original, hw_resized def load_mosaic(self, index): - # loads images in a 4-mosaic - + # 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic labels4, segments4 = [], [] s = self.img_size - yc, xc = [int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border] # mosaic center x, y + yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border) # mosaic center x, y indices = [index] + random.choices(self.indices, k=3) # 3 additional image indices + random.shuffle(indices) for i, index in enumerate(indices): # Load image img, _, (h, w) = load_image(self, index) @@ -720,6 +723,7 @@ def load_mosaic(self, index): # img4, labels4 = replicate(img4, labels4) # replicate # Augment + img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp['copy_paste']) img4, labels4 = random_perspective(img4, labels4, segments4, degrees=self.hyp['degrees'], translate=self.hyp['translate'], @@ -732,11 +736,11 @@ def load_mosaic(self, index): def load_mosaic9(self, index): - # loads images in a 9-mosaic - + # 9-mosaic loader. Loads 1 image + 8 random images into a 9-image mosaic labels9, segments9 = [], [] s = self.img_size indices = [index] + random.choices(self.indices, k=8) # 8 additional image indices + random.shuffle(indices) for i, index in enumerate(indices): # Load image img, _, (h, w) = load_image(self, index) @@ -764,7 +768,7 @@ def load_mosaic9(self, index): c = s - w, s + h0 - hp - h, s, s + h0 - hp padx, pady = c[:2] - x1, y1, x2, y2 = [max(x, 0) for x in c] # allocate coords + x1, y1, x2, y2 = (max(x, 0) for x in c) # allocate coords # Labels labels, segments = self.labels[index].copy(), self.segments[index].copy() @@ -779,7 +783,7 @@ def load_mosaic9(self, index): hp, wp = h, w # height, width previous # Offset - yc, xc = [int(random.uniform(0, s)) for _ in self.mosaic_border] # mosaic center x, y + yc, xc = (int(random.uniform(0, s)) for _ in self.mosaic_border) # mosaic center x, y img9 = img9[yc:yc + 2 * s, xc:xc + 2 * s] # Concat/clip labels @@ -805,199 +809,6 @@ def load_mosaic9(self, index): return img9, labels9 -def replicate(img, labels): - # Replicate labels - h, w = img.shape[:2] - boxes = labels[:, 1:].astype(int) - x1, y1, x2, y2 = boxes.T - s = ((x2 - x1) + (y2 - y1)) / 2 # side length (pixels) - for i in s.argsort()[:round(s.size * 0.5)]: # smallest indices - x1b, y1b, x2b, y2b = boxes[i] - bh, bw = y2b - y1b, x2b - x1b - yc, xc = int(random.uniform(0, h - bh)), int(random.uniform(0, w - bw)) # offset x, y - x1a, y1a, x2a, y2a = [xc, yc, xc + bw, yc + bh] - img[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b] # img4[ymin:ymax, xmin:xmax] - labels = np.append(labels, [[labels[i, 0], x1a, y1a, x2a, y2a]], axis=0) - - return img, labels - - -def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32): - # Resize and pad image while meeting stride-multiple constraints - shape = img.shape[:2] # current shape [height, width] - if isinstance(new_shape, int): - new_shape = (new_shape, new_shape) - - # Scale ratio (new / old) - r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) - if not scaleup: # only scale down, do not scale up (for better test mAP) - r = min(r, 1.0) - - # Compute padding - ratio = r, r # width, height ratios - new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) - dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding - if auto: # minimum rectangle - dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding - elif scaleFill: # stretch - dw, dh = 0.0, 0.0 - new_unpad = (new_shape[1], new_shape[0]) - ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios - - dw /= 2 # divide padding into 2 sides - dh /= 2 - - if shape[::-1] != new_unpad: # resize - img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) - top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) - left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) - img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border - return img, ratio, (dw, dh) - - -def random_perspective(img, targets=(), segments=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0, - border=(0, 0)): - # torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(.1, .1), scale=(.9, 1.1), shear=(-10, 10)) - # targets = [cls, xyxy] - - height = img.shape[0] + border[0] * 2 # shape(h,w,c) - width = img.shape[1] + border[1] * 2 - - # Center - C = np.eye(3) - C[0, 2] = -img.shape[1] / 2 # x translation (pixels) - C[1, 2] = -img.shape[0] / 2 # y translation (pixels) - - # Perspective - P = np.eye(3) - P[2, 0] = random.uniform(-perspective, perspective) # x perspective (about y) - P[2, 1] = random.uniform(-perspective, perspective) # y perspective (about x) - - # Rotation and Scale - R = np.eye(3) - a = random.uniform(-degrees, degrees) - # a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotations - s = random.uniform(1 - scale, 1 + scale) - # s = 2 ** random.uniform(-scale, scale) - R[:2] = cv2.getRotationMatrix2D(angle=a, center=(0, 0), scale=s) - - # Shear - S = np.eye(3) - S[0, 1] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # x shear (deg) - S[1, 0] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # y shear (deg) - - # Translation - T = np.eye(3) - T[0, 2] = random.uniform(0.5 - translate, 0.5 + translate) * width # x translation (pixels) - T[1, 2] = random.uniform(0.5 - translate, 0.5 + translate) * height # y translation (pixels) - - # Combined rotation matrix - M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT - if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed - if perspective: - img = cv2.warpPerspective(img, M, dsize=(width, height), borderValue=(114, 114, 114)) - else: # affine - img = cv2.warpAffine(img, M[:2], dsize=(width, height), borderValue=(114, 114, 114)) - - # Visualize - # import matplotlib.pyplot as plt - # ax = plt.subplots(1, 2, figsize=(12, 6))[1].ravel() - # ax[0].imshow(img[:, :, ::-1]) # base - # ax[1].imshow(img2[:, :, ::-1]) # warped - - # Transform label coordinates - n = len(targets) - if n: - use_segments = any(x.any() for x in segments) - new = np.zeros((n, 4)) - if use_segments: # warp segments - segments = resample_segments(segments) # upsample - for i, segment in enumerate(segments): - xy = np.ones((len(segment), 3)) - xy[:, :2] = segment - xy = xy @ M.T # transform - xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine - - # clip - new[i] = segment2box(xy, width, height) - - else: # warp boxes - xy = np.ones((n * 4, 3)) - xy[:, :2] = targets[:, [1, 2, 3, 4, 1, 4, 3, 2]].reshape(n * 4, 2) # x1y1, x2y2, x1y2, x2y1 - xy = xy @ M.T # transform - xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine - - # create new boxes - x = xy[:, [0, 2, 4, 6]] - y = xy[:, [1, 3, 5, 7]] - new = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T - - # clip - new[:, [0, 2]] = new[:, [0, 2]].clip(0, width) - new[:, [1, 3]] = new[:, [1, 3]].clip(0, height) - - # filter candidates - i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01 if use_segments else 0.10) - targets = targets[i] - targets[:, 1:5] = new[i] - - return img, targets - - -def box_candidates(box1, box2, wh_thr=2, ar_thr=20, area_thr=0.1, eps=1e-16): # box1(4,n), box2(4,n) - # Compute candidate boxes: box1 before augment, box2 after augment, wh_thr (pixels), aspect_ratio_thr, area_ratio - w1, h1 = box1[2] - box1[0], box1[3] - box1[1] - w2, h2 = box2[2] - box2[0], box2[3] - box2[1] - ar = np.maximum(w2 / (h2 + eps), h2 / (w2 + eps)) # aspect ratio - return (w2 > wh_thr) & (h2 > wh_thr) & (w2 * h2 / (w1 * h1 + eps) > area_thr) & (ar < ar_thr) # candidates - - -def cutout(image, labels): - # Applies image cutout augmentation https://arxiv.org/abs/1708.04552 - h, w = image.shape[:2] - - def bbox_ioa(box1, box2): - # Returns the intersection over box2 area given box1, box2. box1 is 4, box2 is nx4. boxes are x1y1x2y2 - box2 = box2.transpose() - - # Get the coordinates of bounding boxes - b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3] - b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3] - - # Intersection area - inter_area = (np.minimum(b1_x2, b2_x2) - np.maximum(b1_x1, b2_x1)).clip(0) * \ - (np.minimum(b1_y2, b2_y2) - np.maximum(b1_y1, b2_y1)).clip(0) - - # box2 area - box2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1) + 1e-16 - - # Intersection over box2 area - return inter_area / box2_area - - # create random masks - scales = [0.5] * 1 + [0.25] * 2 + [0.125] * 4 + [0.0625] * 8 + [0.03125] * 16 # image size fraction - for s in scales: - mask_h = random.randint(1, int(h * s)) - mask_w = random.randint(1, int(w * s)) - - # box - xmin = max(0, random.randint(0, w) - mask_w // 2) - ymin = max(0, random.randint(0, h) - mask_h // 2) - xmax = min(w, xmin + mask_w) - ymax = min(h, ymin + mask_h) - - # apply random color mask - image[ymin:ymax, xmin:xmax] = [random.randint(64, 191) for _ in range(3)] - - # return unobscured labels - if len(labels) and s > 0.03: - box = np.array([xmin, ymin, xmax, ymax], dtype=np.float32) - ioa = bbox_ioa(box, labels[:, 1:5]) # intersection over area - labels = labels[ioa < 0.60] # remove >60% obscured labels - - return labels - - def create_folder(path='./new'): # Create folder if os.path.exists(path): @@ -1005,7 +816,7 @@ def create_folder(path='./new'): os.makedirs(path) # make new output folder -def flatten_recursive(path='../coco128'): +def flatten_recursive(path='../datasets/coco128'): # Flatten a recursive directory by bringing all files to top level new_path = Path(path + '_flat') create_folder(new_path) @@ -1013,15 +824,14 @@ def flatten_recursive(path='../coco128'): shutil.copyfile(file, new_path / Path(file).name) -def extract_boxes(path='../coco128/'): # from utils.datasets import *; extract_boxes('../coco128') +def extract_boxes(path='../datasets/coco128'): # from utils.datasets import *; extract_boxes() # Convert detection dataset into classification dataset, with one directory per class - path = Path(path) # images dir shutil.rmtree(path / 'classifier') if (path / 'classifier').is_dir() else None # remove existing files = list(path.rglob('*.*')) n = len(files) # number of files for im_file in tqdm(files, total=n): - if im_file.suffix[1:] in img_formats: + if im_file.suffix[1:] in IMG_FORMATS: # image im = cv2.imread(str(im_file))[..., ::-1] # BGR to RGB h, w = im.shape[:2] @@ -1029,7 +839,7 @@ def extract_boxes(path='../coco128/'): # from utils.datasets import *; extract_ # labels lb_file = Path(img2label_paths([str(im_file)])[0]) if Path(lb_file).exists(): - with open(lb_file, 'r') as f: + with open(lb_file) as f: lb = np.array([x.split() for x in f.read().strip().splitlines()], dtype=np.float32) # labels for j, x in enumerate(lb): @@ -1048,24 +858,179 @@ def extract_boxes(path='../coco128/'): # from utils.datasets import *; extract_ assert cv2.imwrite(str(f), im[b[1]:b[3], b[0]:b[2]]), f'box failure in {f}' -def autosplit(path='../coco128', weights=(0.9, 0.1, 0.0), annotated_only=False): +def autosplit(path='../datasets/coco128/images', weights=(0.9, 0.1, 0.0), annotated_only=False): """ Autosplit a dataset into train/val/test splits and save path/autosplit_*.txt files - Usage: from utils.datasets import *; autosplit('../coco128') + Usage: from utils.datasets import *; autosplit() Arguments - path: Path to images directory - weights: Train, val, test weights (list) - annotated_only: Only use images with an annotated txt file + path: Path to images directory + weights: Train, val, test weights (list, tuple) + annotated_only: Only use images with an annotated txt file """ path = Path(path) # images dir - files = sum([list(path.rglob(f"*.{img_ext}")) for img_ext in img_formats], []) # image files only + files = sorted(x for x in path.rglob('*.*') if x.suffix[1:].lower() in IMG_FORMATS) # image files only n = len(files) # number of files + random.seed(0) # for reproducibility indices = random.choices([0, 1, 2], weights=weights, k=n) # assign each image to a split txt = ['autosplit_train.txt', 'autosplit_val.txt', 'autosplit_test.txt'] # 3 txt files - [(path / x).unlink() for x in txt if (path / x).exists()] # remove existing + [(path.parent / x).unlink(missing_ok=True) for x in txt] # remove existing print(f'Autosplitting images from {path}' + ', using *.txt labeled images only' * annotated_only) for i, img in tqdm(zip(indices, files), total=n): if not annotated_only or Path(img2label_paths([str(img)])[0]).exists(): # check label - with open(path / txt[i], 'a') as f: - f.write(str(img) + '\n') # add image to txt file + with open(path.parent / txt[i], 'a') as f: + f.write('./' + img.relative_to(path.parent).as_posix() + '\n') # add image to txt file + + +def verify_image_label(args): + # Verify one image-label pair + im_file, lb_file, prefix = args + nm, nf, ne, nc, msg, segments = 0, 0, 0, 0, '', [] # number (missing, found, empty, corrupt), message, segments + try: + # verify images + im = Image.open(im_file) + im.verify() # PIL verify + shape = exif_size(im) # image size + assert (shape[0] > 9) & (shape[1] > 9), f'image size {shape} <10 pixels' + assert im.format.lower() in IMG_FORMATS, f'invalid image format {im.format}' + if im.format.lower() in ('jpg', 'jpeg'): + with open(im_file, 'rb') as f: + f.seek(-2, 2) + if f.read() != b'\xff\xd9': # corrupt JPEG + ImageOps.exif_transpose(Image.open(im_file)).save(im_file, 'JPEG', subsampling=0, quality=100) + msg = f'{prefix}WARNING: {im_file}: corrupt JPEG restored and saved' + + # verify labels + if os.path.isfile(lb_file): + nf = 1 # label found + with open(lb_file) as f: + l = [x.split() for x in f.read().strip().splitlines() if len(x)] + if any([len(x) > 8 for x in l]): # is segment + classes = np.array([x[0] for x in l], dtype=np.float32) + segments = [np.array(x[1:], dtype=np.float32).reshape(-1, 2) for x in l] # (cls, xy1...) + l = np.concatenate((classes.reshape(-1, 1), segments2boxes(segments)), 1) # (cls, xywh) + l = np.array(l, dtype=np.float32) + nl = len(l) + if nl: + assert l.shape[1] == 5, f'labels require 5 columns, {l.shape[1]} columns detected' + assert (l >= 0).all(), f'negative label values {l[l < 0]}' + assert (l[:, 1:] <= 1).all(), f'non-normalized or out of bounds coordinates {l[:, 1:][l[:, 1:] > 1]}' + _, i = np.unique(l, axis=0, return_index=True) + if len(i) < nl: # duplicate row check + l = l[i] # remove duplicates + if segments: + segments = segments[i] + msg = f'{prefix}WARNING: {im_file}: {nl - len(i)} duplicate labels removed' + else: + ne = 1 # label empty + l = np.zeros((0, 5), dtype=np.float32) + else: + nm = 1 # label missing + l = np.zeros((0, 5), dtype=np.float32) + return im_file, l, shape, segments, nm, nf, ne, nc, msg + except Exception as e: + nc = 1 + msg = f'{prefix}WARNING: {im_file}: ignoring corrupt image/label: {e}' + return [None, None, None, None, nm, nf, ne, nc, msg] + + +def dataset_stats(path='coco128.yaml', autodownload=False, verbose=False, profile=False, hub=False): + """ Return dataset statistics dictionary with images and instances counts per split per class + To run in parent directory: export PYTHONPATH="$PWD/yolov3" + Usage1: from utils.datasets import *; dataset_stats('coco128.yaml', autodownload=True) + Usage2: from utils.datasets import *; dataset_stats('../datasets/coco128_with_yaml.zip') + Arguments + path: Path to data.yaml or data.zip (with data.yaml inside data.zip) + autodownload: Attempt to download dataset if not found locally + verbose: Print stats dictionary + """ + + def round_labels(labels): + # Update labels to integer class and 6 decimal place floats + return [[int(c), *(round(x, 4) for x in points)] for c, *points in labels] + + def unzip(path): + # Unzip data.zip TODO: CONSTRAINT: path/to/abc.zip MUST unzip to 'path/to/abc/' + if str(path).endswith('.zip'): # path is data.zip + assert Path(path).is_file(), f'Error unzipping {path}, file not found' + ZipFile(path).extractall(path=path.parent) # unzip + dir = path.with_suffix('') # dataset directory == zip name + return True, str(dir), next(dir.rglob('*.yaml')) # zipped, data_dir, yaml_path + else: # path is data.yaml + return False, None, path + + def hub_ops(f, max_dim=1920): + # HUB ops for 1 image 'f': resize and save at reduced quality in /dataset-hub for web/app viewing + f_new = im_dir / Path(f).name # dataset-hub image filename + try: # use PIL + im = Image.open(f) + r = max_dim / max(im.height, im.width) # ratio + if r < 1.0: # image too large + im = im.resize((int(im.width * r), int(im.height * r))) + im.save(f_new, 'JPEG', quality=75, optimize=True) # save + except Exception as e: # use OpenCV + print(f'WARNING: HUB ops PIL failure {f}: {e}') + im = cv2.imread(f) + im_height, im_width = im.shape[:2] + r = max_dim / max(im_height, im_width) # ratio + if r < 1.0: # image too large + im = cv2.resize(im, (int(im_width * r), int(im_height * r)), interpolation=cv2.INTER_LINEAR) + cv2.imwrite(str(f_new), im) + + zipped, data_dir, yaml_path = unzip(Path(path)) + with open(check_yaml(yaml_path), errors='ignore') as f: + data = yaml.safe_load(f) # data dict + if zipped: + data['path'] = data_dir # TODO: should this be dir.resolve()? + check_dataset(data, autodownload) # download dataset if missing + hub_dir = Path(data['path'] + ('-hub' if hub else '')) + stats = {'nc': data['nc'], 'names': data['names']} # statistics dictionary + for split in 'train', 'val', 'test': + if data.get(split) is None: + stats[split] = None # i.e. no test set + continue + x = [] + dataset = LoadImagesAndLabels(data[split]) # load dataset + for label in tqdm(dataset.labels, total=dataset.n, desc='Statistics'): + x.append(np.bincount(label[:, 0].astype(int), minlength=data['nc'])) + x = np.array(x) # shape(128x80) + stats[split] = {'instance_stats': {'total': int(x.sum()), 'per_class': x.sum(0).tolist()}, + 'image_stats': {'total': dataset.n, 'unlabelled': int(np.all(x == 0, 1).sum()), + 'per_class': (x > 0).sum(0).tolist()}, + 'labels': [{str(Path(k).name): round_labels(v.tolist())} for k, v in + zip(dataset.img_files, dataset.labels)]} + + if hub: + im_dir = hub_dir / 'images' + im_dir.mkdir(parents=True, exist_ok=True) + for _ in tqdm(ThreadPool(NUM_THREADS).imap(hub_ops, dataset.img_files), total=dataset.n, desc='HUB Ops'): + pass + + # Profile + stats_path = hub_dir / 'stats.json' + if profile: + for _ in range(1): + file = stats_path.with_suffix('.npy') + t1 = time.time() + np.save(file, stats) + t2 = time.time() + x = np.load(file, allow_pickle=True) + print(f'stats.npy times: {time.time() - t2:.3f}s read, {t2 - t1:.3f}s write') + + file = stats_path.with_suffix('.json') + t1 = time.time() + with open(file, 'w') as f: + json.dump(stats, f) # save stats *.json + t2 = time.time() + with open(file) as f: + x = json.load(f) # load hyps dict + print(f'stats.json times: {time.time() - t2:.3f}s read, {t2 - t1:.3f}s write') + + # Save, print and return + if hub: + print(f'Saving {stats_path.resolve()}...') + with open(stats_path, 'w') as f: + json.dump(stats, f) # save stats.json + if verbose: + print(json.dumps(stats, indent=2, sort_keys=False)) + return stats diff --git a/utils/google_utils.py b/utils/downloads.py similarity index 83% rename from utils/google_utils.py rename to utils/downloads.py index 340fab1328..cd653078e9 100644 --- a/utils/google_utils.py +++ b/utils/downloads.py @@ -1,10 +1,15 @@ -# Google utils: https://cloud.google.com/storage/docs/reference/libraries +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Download utils +""" import os import platform import subprocess import time +import urllib from pathlib import Path +from zipfile import ZipFile import requests import torch @@ -19,30 +24,32 @@ def gsutil_getsize(url=''): def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''): # Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes file = Path(file) - try: # GitHub + assert_msg = f"Downloaded file '{file}' does not exist or size is < min_bytes={min_bytes}" + try: # url1 print(f'Downloading {url} to {file}...') torch.hub.download_url_to_file(url, str(file)) - assert file.exists() and file.stat().st_size > min_bytes # check - except Exception as e: # GCP + assert file.exists() and file.stat().st_size > min_bytes, assert_msg # check + except Exception as e: # url2 file.unlink(missing_ok=True) # remove partial downloads - print(f'Download error: {e}\nRe-attempting {url2 or url} to {file}...') + print(f'ERROR: {e}\nRe-attempting {url2 or url} to {file}...') os.system(f"curl -L '{url2 or url}' -o '{file}' --retry 3 -C -") # curl download, retry and resume on fail finally: if not file.exists() or file.stat().st_size < min_bytes: # check file.unlink(missing_ok=True) # remove partial downloads - print(f'ERROR: Download failure: {error_msg or url}') + print(f"ERROR: {assert_msg}\n{error_msg}") print('') -def attempt_download(file, repo='ultralytics/yolov3'): +def attempt_download(file, repo='ultralytics/yolov3'): # from utils.downloads import *; attempt_download() # Attempt file download if does not exist file = Path(str(file).strip().replace("'", '')) if not file.exists(): # URL specified - name = file.name + name = Path(urllib.parse.unquote(str(file))).name # decode '%2F' to '/' etc. if str(file).startswith(('http:/', 'https:/')): # download url = str(file).replace(':/', '://') # Pathlib turns :// -> :/ + name = name.split('?')[0] # parse authentication https://url.com/file.txt?auth... safe_download(file=name, url=url, min_bytes=1E5) return name @@ -50,7 +57,7 @@ def attempt_download(file, repo='ultralytics/yolov3'): file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required) try: response = requests.get(f'https://github.com/gitapi/repos/{repo}/releases/latest').json() # github api - assets = [x['name'] for x in response['assets']] # release assets, i.e. ['yolov5s.pt', 'yolov5m.pt', ...] + assets = [x['name'] for x in response['assets']] # release assets, i.e. ['yolov3.pt'...] tag = response['tag_name'] # i.e. 'v1.0' except: # fallback plan assets = ['yolov3.pt', 'yolov3-spp.pt', 'yolov3-tiny.pt'] @@ -70,7 +77,7 @@ def attempt_download(file, repo='ultralytics/yolov3'): def gdrive_download(id='16TiPfZj7htmTyhntwcZyEEAejOUxuT6m', file='tmp.zip'): - # Downloads a file from Google Drive. from yolov3.utils.google_utils import *; gdrive_download() + # Downloads a file from Google Drive. from yolov3.utils.downloads import *; gdrive_download() t = time.time() file = Path(file) cookie = Path('cookie') # gdrive cookie @@ -97,8 +104,8 @@ def gdrive_download(id='16TiPfZj7htmTyhntwcZyEEAejOUxuT6m', file='tmp.zip'): # Unzip if archive if file.suffix == '.zip': print('unzipping... ', end='') - os.system(f'unzip -q {file}') # unzip - file.unlink() # remove zip to free space + ZipFile(file).extractall(path=file.parent) # unzip + file.unlink() # remove zip print(f'Done ({time.time() - t:.1f}s)') return r @@ -111,6 +118,9 @@ def get_token(cookie="./cookie"): return line.split()[-1] return "" +# Google utils: https://cloud.google.com/storage/docs/reference/libraries ---------------------------------------------- +# +# # def upload_blob(bucket_name, source_file_name, destination_blob_name): # # Uploads a file to a bucket # # https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-python diff --git a/utils/flask_rest_api/README.md b/utils/flask_rest_api/README.md deleted file mode 100644 index 324c2416dc..0000000000 --- a/utils/flask_rest_api/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Flask REST API -[REST](https://en.wikipedia.org/wiki/Representational_state_transfer) [API](https://en.wikipedia.org/wiki/API)s are commonly used to expose Machine Learning (ML) models to other services. This folder contains an example REST API created using Flask to expose the YOLOv5s model from [PyTorch Hub](https://pytorch.org/hub/ultralytics_yolov5/). - -## Requirements - -[Flask](https://palletsprojects.com/p/flask/) is required. Install with: -```shell -$ pip install Flask -``` - -## Run - -After Flask installation run: - -```shell -$ python3 restapi.py --port 5000 -``` - -Then use [curl](https://curl.se/) to perform a request: - -```shell -$ curl -X POST -F image=@zidane.jpg 'http://localhost:5000/v1/object-detection/yolov5s'` -``` - -The model inference results are returned as a JSON response: - -```json -[ - { - "class": 0, - "confidence": 0.8900438547, - "height": 0.9318675399, - "name": "person", - "width": 0.3264600933, - "xcenter": 0.7438579798, - "ycenter": 0.5207948685 - }, - { - "class": 0, - "confidence": 0.8440024257, - "height": 0.7155083418, - "name": "person", - "width": 0.6546785235, - "xcenter": 0.427829951, - "ycenter": 0.6334488392 - }, - { - "class": 27, - "confidence": 0.3771208823, - "height": 0.3902671337, - "name": "tie", - "width": 0.0696444362, - "xcenter": 0.3675483763, - "ycenter": 0.7991207838 - }, - { - "class": 27, - "confidence": 0.3527112305, - "height": 0.1540903747, - "name": "tie", - "width": 0.0336618312, - "xcenter": 0.7814827561, - "ycenter": 0.5065554976 - } -] -``` - -An example python script to perform inference using [requests](https://docs.python-requests.org/en/master/) is given in `example_request.py` diff --git a/utils/flask_rest_api/example_request.py b/utils/flask_rest_api/example_request.py deleted file mode 100644 index ff21f30f93..0000000000 --- a/utils/flask_rest_api/example_request.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Perform test request""" -import pprint - -import requests - -DETECTION_URL = "http://localhost:5000/v1/object-detection/yolov5s" -TEST_IMAGE = "zidane.jpg" - -image_data = open(TEST_IMAGE, "rb").read() - -response = requests.post(DETECTION_URL, files={"image": image_data}).json() - -pprint.pprint(response) diff --git a/utils/flask_rest_api/restapi.py b/utils/flask_rest_api/restapi.py deleted file mode 100644 index b0df747220..0000000000 --- a/utils/flask_rest_api/restapi.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Run a rest API exposing the yolov5s object detection model -""" -import argparse -import io - -import torch -from PIL import Image -from flask import Flask, request - -app = Flask(__name__) - -DETECTION_URL = "/v1/object-detection/yolov5s" - - -@app.route(DETECTION_URL, methods=["POST"]) -def predict(): - if not request.method == "POST": - return - - if request.files.get("image"): - image_file = request.files["image"] - image_bytes = image_file.read() - - img = Image.open(io.BytesIO(image_bytes)) - - results = model(img, size=640) # reduce size=320 for faster inference - return results.pandas().xyxy[0].to_json(orient="records") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Flask API exposing YOLOv3 model") - parser.add_argument("--port", default=5000, type=int, help="port number") - args = parser.parse_args() - - model = torch.hub.load("ultralytics/yolov5", "yolov5s", force_reload=True) # force_reload to recache - app.run(host="0.0.0.0", port=args.port) # debug=True causes Restarting with stat diff --git a/utils/general.py b/utils/general.py index af33c63389..820e35d945 100755 --- a/utils/general.py +++ b/utils/general.py @@ -1,5 +1,9 @@ -# YOLOv3 general utils +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +General utils +""" +import contextlib import glob import logging import math @@ -7,11 +11,15 @@ import platform import random import re -import subprocess +import shutil +import signal import time +import urllib from itertools import repeat from multiprocessing.pool import ThreadPool from pathlib import Path +from subprocess import check_output +from zipfile import ZipFile import cv2 import numpy as np @@ -21,9 +29,8 @@ import torchvision import yaml -from utils.google_utils import gsutil_getsize -from utils.metrics import fitness -from utils.torch_utils import init_torch_seeds +from utils.downloads import gsutil_getsize +from utils.metrics import box_iou, fitness # Settings torch.set_printoptions(linewidth=320, precision=5, profile='long') @@ -32,18 +39,96 @@ cv2.setNumThreads(0) # prevent OpenCV from multithreading (incompatible with PyTorch DataLoader) os.environ['NUMEXPR_MAX_THREADS'] = str(min(os.cpu_count(), 8)) # NumExpr max threads +FILE = Path(__file__).resolve() +ROOT = FILE.parents[1] # root directory -def set_logging(rank=-1, verbose=True): - logging.basicConfig( - format="%(message)s", - level=logging.INFO if (verbose and rank in [-1, 0]) else logging.WARN) + +def set_logging(name=None, verbose=True): + # Sets level and returns logger + rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings + logging.basicConfig(format="%(message)s", level=logging.INFO if (verbose and rank in (-1, 0)) else logging.WARNING) + return logging.getLogger(name) + + +LOGGER = set_logging(__name__) # define globally (used in train.py, val.py, detect.py, etc.) + + +class Profile(contextlib.ContextDecorator): + # Usage: @Profile() decorator or 'with Profile():' context manager + def __enter__(self): + self.start = time.time() + + def __exit__(self, type, value, traceback): + print(f'Profile results: {time.time() - self.start:.5f}s') + + +class Timeout(contextlib.ContextDecorator): + # Usage: @Timeout(seconds) decorator or 'with Timeout(seconds):' context manager + def __init__(self, seconds, *, timeout_msg='', suppress_timeout_errors=True): + self.seconds = int(seconds) + self.timeout_message = timeout_msg + self.suppress = bool(suppress_timeout_errors) + + def _timeout_handler(self, signum, frame): + raise TimeoutError(self.timeout_message) + + def __enter__(self): + signal.signal(signal.SIGALRM, self._timeout_handler) # Set handler for SIGALRM + signal.alarm(self.seconds) # start countdown for SIGALRM to be raised + + def __exit__(self, exc_type, exc_val, exc_tb): + signal.alarm(0) # Cancel SIGALRM if it's scheduled + if self.suppress and exc_type is TimeoutError: # Suppress TimeoutError + return True + + +class WorkingDirectory(contextlib.ContextDecorator): + # Usage: @WorkingDirectory(dir) decorator or 'with WorkingDirectory(dir):' context manager + def __init__(self, new_dir): + self.dir = new_dir # new dir + self.cwd = Path.cwd().resolve() # current dir + + def __enter__(self): + os.chdir(self.dir) + + def __exit__(self, exc_type, exc_val, exc_tb): + os.chdir(self.cwd) + + +def try_except(func): + # try-except function. Usage: @try_except decorator + def handler(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception as e: + print(e) + + return handler + + +def methods(instance): + # Get class/instance methods + return [f for f in dir(instance) if callable(getattr(instance, f)) and not f.startswith("__")] + + +def print_args(name, opt): + # Print argparser arguments + LOGGER.info(colorstr(f'{name}: ') + ', '.join(f'{k}={v}' for k, v in vars(opt).items())) def init_seeds(seed=0): - # Initialize random number generator (RNG) seeds + # Initialize random number generator (RNG) seeds https://pytorch.org/docs/stable/notes/randomness.html + # cudnn seed 0 settings are slower and more reproducible, else faster and less reproducible + import torch.backends.cudnn as cudnn random.seed(seed) np.random.seed(seed) - init_torch_seeds(seed) + torch.manual_seed(seed) + cudnn.benchmark, cudnn.deterministic = (False, True) if seed == 0 else (True, False) + + +def intersect_dicts(da, db, exclude=()): + # Dictionary intersection of matching keys and shapes, omitting 'exclude' keys, using da values + return {k: v for k, v in da.items() if k in db and not any(x in k for x in exclude) and v.shape == db[k].shape} def get_latest_run(search_dir='.'): @@ -52,81 +137,136 @@ def get_latest_run(search_dir='.'): return max(last_list, key=os.path.getctime) if last_list else '' +def user_config_dir(dir='Ultralytics', env_var='YOLOV3_CONFIG_DIR'): + # Return path of user configuration directory. Prefer environment variable if exists. Make dir if required. + env = os.getenv(env_var) + if env: + path = Path(env) # use environment variable + else: + cfg = {'Windows': 'AppData/Roaming', 'Linux': '.config', 'Darwin': 'Library/Application Support'} # 3 OS dirs + path = Path.home() / cfg.get(platform.system(), '') # OS-specific config dir + path = (path if is_writeable(path) else Path('/tmp')) / dir # GCP and AWS lambda fix, only /tmp is writeable + path.mkdir(exist_ok=True) # make if required + return path + + +def is_writeable(dir, test=False): + # Return True if directory has write permissions, test opening a file with write permissions if test=True + if test: # method 1 + file = Path(dir) / 'tmp.txt' + try: + with open(file, 'w'): # open file with write permissions + pass + file.unlink() # remove file + return True + except OSError: + return False + else: # method 2 + return os.access(dir, os.R_OK) # possible issues on Windows + + def is_docker(): - # Is environment a Docker container + # Is environment a Docker container? return Path('/workspace').exists() # or Path('/.dockerenv').exists() def is_colab(): - # Is environment a Google Colab instance + # Is environment a Google Colab instance? try: import google.colab return True - except Exception as e: + except ImportError: return False +def is_pip(): + # Is file in a pip package? + return 'site-packages' in Path(__file__).resolve().parts + + +def is_ascii(s=''): + # Is string composed of all ASCII (no UTF) characters? (note str().isascii() introduced in python 3.7) + s = str(s) # convert list, tuple, None, etc. to str + return len(s.encode().decode('ascii', 'ignore')) == len(s) + + +def is_chinese(s='δΊΊε·₯智能'): + # Is string composed of any Chinese characters? + return re.search('[\u4e00-\u9fff]', s) + + def emojis(str=''): # Return platform-dependent emoji-safe version of string return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str -def file_size(file): - # Return file size in MB - return Path(file).stat().st_size / 1e6 +def file_size(path): + # Return file/dir size (MB) + path = Path(path) + if path.is_file(): + return path.stat().st_size / 1E6 + elif path.is_dir(): + return sum(f.stat().st_size for f in path.glob('**/*') if f.is_file()) / 1E6 + else: + return 0.0 def check_online(): # Check internet connectivity import socket try: - socket.create_connection(("1.1.1.1", 443), 5) # check host accesability + socket.create_connection(("1.1.1.1", 443), 5) # check host accessibility return True except OSError: return False +@try_except +@WorkingDirectory(ROOT) def check_git_status(): # Recommend 'git pull' if code is out of date + msg = ', for updates see https://github.com/ultralytics/yolov3' print(colorstr('github: '), end='') - try: - assert Path('.git').exists(), 'skipping check (not a git repository)' - assert not is_docker(), 'skipping check (Docker image)' - assert check_online(), 'skipping check (offline)' - - cmd = 'git fetch && git config --get remote.origin.url' - url = subprocess.check_output(cmd, shell=True).decode().strip().rstrip('.git') # github repo url - branch = subprocess.check_output('git rev-parse --abbrev-ref HEAD', shell=True).decode().strip() # checked out - n = int(subprocess.check_output(f'git rev-list {branch}..origin/master --count', shell=True)) # commits behind - if n > 0: - s = f"⚠️ WARNING: code is out of date by {n} commit{'s' * (n > 1)}. " \ - f"Use 'git pull' to update or 'git clone {url}' to download latest." - else: - s = f'up to date with {url} βœ…' - print(emojis(s)) # emoji-safe - except Exception as e: - print(e) + assert Path('.git').exists(), 'skipping check (not a git repository)' + msg + assert not is_docker(), 'skipping check (Docker image)' + msg + assert check_online(), 'skipping check (offline)' + msg + + cmd = 'git fetch && git config --get remote.origin.url' + url = check_output(cmd, shell=True, timeout=5).decode().strip().rstrip('.git') # git fetch + branch = check_output('git rev-parse --abbrev-ref HEAD', shell=True).decode().strip() # checked out + n = int(check_output(f'git rev-list {branch}..origin/master --count', shell=True)) # commits behind + if n > 0: + s = f"⚠️ YOLOv3 is out of date by {n} commit{'s' * (n > 1)}. Use `git pull` or `git clone {url}` to update." + else: + s = f'up to date with {url} βœ…' + print(emojis(s)) # emoji-safe -def check_python(minimum='3.7.0', required=True): +def check_python(minimum='3.6.2'): # Check current python version vs. required python version - current = platform.python_version() - result = pkg.parse_version(current) >= pkg.parse_version(minimum) - if required: - assert result, f'Python {minimum} required by YOLOv3, but Python {current} is currently installed' - return result + check_version(platform.python_version(), minimum, name='Python ', hard=True) + +def check_version(current='0.0.0', minimum='0.0.0', name='version ', pinned=False, hard=False): + # Check version vs. required version + current, minimum = (pkg.parse_version(x) for x in (current, minimum)) + result = (current == minimum) if pinned else (current >= minimum) # bool + if hard: # assert min requirements met + assert result, f'{name}{minimum} required by YOLOv3, but {name}{current} is currently installed' + else: + return result -def check_requirements(requirements='requirements.txt', exclude=()): + +@try_except +def check_requirements(requirements=ROOT / 'requirements.txt', exclude=(), install=True): # Check installed dependencies meet requirements (pass *.txt file or list of packages) prefix = colorstr('red', 'bold', 'requirements:') check_python() # check python version if isinstance(requirements, (str, Path)): # requirements.txt file file = Path(requirements) - if not file.exists(): - print(f"{prefix} {file.resolve()} not found, check failed.") - return - requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(file.open()) if x.name not in exclude] + assert file.exists(), f"{prefix} {file.resolve()} not found, check failed." + with file.open() as f: + requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(f) if x.name not in exclude] else: # list or tuple of packages requirements = [x for x in requirements if x not in exclude] @@ -135,25 +275,33 @@ def check_requirements(requirements='requirements.txt', exclude=()): try: pkg.require(r) except Exception as e: # DistributionNotFound or VersionConflict if requirements not met - n += 1 - print(f"{prefix} {r} not found and is required by YOLOv3, attempting auto-update...") - try: - print(subprocess.check_output(f"pip install '{r}'", shell=True).decode()) - except Exception as e: - print(f'{prefix} {e}') + s = f"{prefix} {r} not found and is required by YOLOv3" + if install: + print(f"{s}, attempting auto-update...") + try: + assert check_online(), f"'pip install {r}' skipped (offline)" + print(check_output(f"pip install '{r}'", shell=True).decode()) + n += 1 + except Exception as e: + print(f'{prefix} {e}') + else: + print(f'{s}. Please install and rerun your command.') if n: # if packages updated source = file.resolve() if 'file' in locals() else requirements s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \ f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n" - print(emojis(s)) # emoji-safe + print(emojis(s)) -def check_img_size(img_size, s=32): - # Verify img_size is a multiple of stride s - new_size = make_divisible(img_size, int(s)) # ceil gs-multiple - if new_size != img_size: - print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size)) +def check_img_size(imgsz, s=32, floor=0): + # Verify image size is a multiple of stride s in each dimension + if isinstance(imgsz, int): # integer i.e. img_size=640 + new_size = max(make_divisible(imgsz, int(s)), floor) + else: # list i.e. img_size=[640, 480] + new_size = [max(make_divisible(x, int(s)), floor) for x in imgsz] + if new_size != imgsz: + print(f'WARNING: --img-size {imgsz} must be multiple of max stride {s}, updating to {new_size}') return new_size @@ -172,53 +320,114 @@ def check_imshow(): return False -def check_file(file): +def check_suffix(file='yolov3.pt', suffix=('.pt',), msg=''): + # Check file(s) for acceptable suffix + if file and suffix: + if isinstance(suffix, str): + suffix = [suffix] + for f in file if isinstance(file, (list, tuple)) else [file]: + s = Path(f).suffix.lower() # file suffix + if len(s): + assert s in suffix, f"{msg}{f} acceptable suffix is {suffix}" + + +def check_yaml(file, suffix=('.yaml', '.yml')): + # Search/download YAML file (if necessary) and return path, checking suffix + return check_file(file, suffix) + + +def check_file(file, suffix=''): # Search/download file (if necessary) and return path + check_suffix(file, suffix) # optional file = str(file) # convert to str() if Path(file).is_file() or file == '': # exists return file - elif file.startswith(('http://', 'https://')): # download - url, file = file, Path(file).name - print(f'Downloading {url} to {file}...') - torch.hub.download_url_to_file(url, file) - assert Path(file).exists() and Path(file).stat().st_size > 0, f'File download failed: {url}' # check + elif file.startswith(('http:/', 'https:/')): # download + url = str(Path(file)).replace(':/', '://') # Pathlib turns :// -> :/ + file = Path(urllib.parse.unquote(file).split('?')[0]).name # '%2F' to '/', split https://url.com/file.txt?auth + if Path(file).is_file(): + print(f'Found {url} locally at {file}') # file already exists + else: + print(f'Downloading {url} to {file}...') + torch.hub.download_url_to_file(url, file) + assert Path(file).exists() and Path(file).stat().st_size > 0, f'File download failed: {url}' # check return file else: # search - files = glob.glob('./**/' + file, recursive=True) # find file + files = [] + for d in 'data', 'models', 'utils': # search directories + files.extend(glob.glob(str(ROOT / d / '**' / file), recursive=True)) # find file assert len(files), f'File not found: {file}' # assert file was found assert len(files) == 1, f"Multiple files match '{file}', specify exact path: {files}" # assert unique return files[0] # return file -def check_dataset(dict): - # Download dataset if not found locally - val, s = dict.get('val'), dict.get('download') - if val and len(val): +def check_dataset(data, autodownload=True): + # Download and/or unzip dataset if not found locally + # Usage: https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128_with_yaml.zip + + # Download (optional) + extract_dir = '' + if isinstance(data, (str, Path)) and str(data).endswith('.zip'): # i.e. gs://bucket/dir/coco128.zip + download(data, dir='../datasets', unzip=True, delete=False, curl=False, threads=1) + data = next((Path('../datasets') / Path(data).stem).rglob('*.yaml')) + extract_dir, autodownload = data.parent, False + + # Read yaml (optional) + if isinstance(data, (str, Path)): + with open(data, errors='ignore') as f: + data = yaml.safe_load(f) # dictionary + + # Parse yaml + path = extract_dir or Path(data.get('path') or '') # optional 'path' default to '.' + for k in 'train', 'val', 'test': + if data.get(k): # prepend path + data[k] = str(path / data[k]) if isinstance(data[k], str) else [str(path / x) for x in data[k]] + + assert 'nc' in data, "Dataset 'nc' key missing." + if 'names' not in data: + data['names'] = [f'class{i}' for i in range(data['nc'])] # assign class names if missing + train, val, test, s = (data.get(x) for x in ('train', 'val', 'test', 'download')) + if val: val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path if not all(x.exists() for x in val): print('\nWARNING: Dataset not found, nonexistent paths: %s' % [str(x) for x in val if not x.exists()]) - if s and len(s): # download script + if s and autodownload: # download script + root = path.parent if 'path' in data else '..' # unzip directory i.e. '../' if s.startswith('http') and s.endswith('.zip'): # URL f = Path(s).name # filename - print(f'Downloading {s} ...') + print(f'Downloading {s} to {f}...') torch.hub.download_url_to_file(s, f) - r = os.system(f'unzip -q {f} -d ../ && rm {f}') # unzip + Path(root).mkdir(parents=True, exist_ok=True) # create root + ZipFile(f).extractall(path=root) # unzip + Path(f).unlink() # remove zip + r = None # success elif s.startswith('bash '): # bash script print(f'Running {s} ...') r = os.system(s) else: # python script - r = exec(s) # return None - print('Dataset autodownload %s\n' % ('success' if r in (0, None) else 'failure')) # print result + r = exec(s, {'yaml': data}) # return None + print(f"Dataset autodownload {f'success, saved to {root}' if r in (0, None) else 'failure'}\n") else: raise Exception('Dataset not found.') + return data # dictionary + + +def url2file(url): + # Convert URL to filename, i.e. https://url.com/file.txt?auth -> file.txt + url = str(Path(url)).replace(':/', '://') # Pathlib turns :// -> :/ + file = Path(urllib.parse.unquote(url)).name.split('?')[0] # '%2F' to '/', split https://url.com/file.txt?auth + return file + def download(url, dir='.', unzip=True, delete=True, curl=False, threads=1): - # Multi-threaded file download and unzip function + # Multi-threaded file download and unzip function, used in data.yaml for autodownload def download_one(url, dir): # Download 1 file f = dir / Path(url).name # filename - if not f.exists(): + if Path(url).is_file(): # exists in current path + Path(url).rename(f) # move to dir + elif not f.exists(): print(f'Downloading {url} to {f}...') if curl: os.system(f"curl -L '{url}' -o '{f}' --retry 9 -C -") # curl download, retry and resume on fail @@ -227,12 +436,11 @@ def download_one(url, dir): if unzip and f.suffix in ('.zip', '.gz'): print(f'Unzipping {f}...') if f.suffix == '.zip': - s = f'unzip -qo {f} -d {dir} && rm {f}' # unzip -quiet -overwrite + ZipFile(f).extractall(path=dir) # unzip elif f.suffix == '.gz': - s = f'tar xfz {f} --directory {f.parent}' # unzip - if delete: # delete zip file after unzip - s += f' && rm {f}' - os.system(s) + os.system(f'tar xfz {f} --directory {f.parent}') # unzip + if delete: + f.unlink() # remove zip dir = Path(dir) dir.mkdir(parents=True, exist_ok=True) # make directory @@ -242,7 +450,7 @@ def download_one(url, dir): pool.close() pool.join() else: - for u in tuple(url) if isinstance(url, str) else url: + for u in [url] if isinstance(url, (str, Path)) else url: download_one(u, dir) @@ -257,7 +465,7 @@ def clean_str(s): def one_cycle(y1=0.0, y2=1.0, steps=100): - # lambda function for sinusoidal ramp from y1 to y2 + # lambda function for sinusoidal ramp from y1 to y2 https://arxiv.org/pdf/1812.01187.pdf return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1 @@ -355,6 +563,18 @@ def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0): return y +def xyxy2xywhn(x, w=640, h=640, clip=False, eps=0.0): + # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] normalized where xy1=top-left, xy2=bottom-right + if clip: + clip_coords(x, (h - eps, w - eps)) # warning: inplace clip + y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) + y[:, 0] = ((x[:, 0] + x[:, 2]) / 2) / w # x center + y[:, 1] = ((x[:, 1] + x[:, 3]) / 2) / h # y center + y[:, 2] = (x[:, 2] - x[:, 0]) / w # width + y[:, 3] = (x[:, 3] - x[:, 1]) / h # height + return y + + def xyn2xy(x, w=640, h=640, padw=0, padh=0): # Convert normalized segments into pixel segments, shape (n,2) y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) @@ -405,90 +625,16 @@ def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None): return coords -def clip_coords(boxes, img_shape): +def clip_coords(boxes, shape): # Clip bounding xyxy bounding boxes to image shape (height, width) - boxes[:, 0].clamp_(0, img_shape[1]) # x1 - boxes[:, 1].clamp_(0, img_shape[0]) # y1 - boxes[:, 2].clamp_(0, img_shape[1]) # x2 - boxes[:, 3].clamp_(0, img_shape[0]) # y2 - - -def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7): - # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4 - box2 = box2.T - - # Get the coordinates of bounding boxes - if x1y1x2y2: # x1, y1, x2, y2 = box1 - b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3] - b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3] - else: # transform from xywh to xyxy - b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2 - b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2 - b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2 - b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2 - - # Intersection area - inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \ - (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0) - - # Union Area - w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps - w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps - union = w1 * h1 + w2 * h2 - inter + eps - - iou = inter / union - if GIoU or DIoU or CIoU: - cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width - ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height - if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1 - c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared - rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + - (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared - if DIoU: - return iou - rho2 / c2 # DIoU - elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47 - v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2) - with torch.no_grad(): - alpha = v / (v - iou + (1 + eps)) - return iou - (rho2 / c2 + v * alpha) # CIoU - else: # GIoU https://arxiv.org/pdf/1902.09630.pdf - c_area = cw * ch + eps # convex area - return iou - (c_area - union) / c_area # GIoU - else: - return iou # IoU - - -def box_iou(box1, box2): - # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py - """ - Return intersection-over-union (Jaccard index) of boxes. - Both sets of boxes are expected to be in (x1, y1, x2, y2) format. - Arguments: - box1 (Tensor[N, 4]) - box2 (Tensor[M, 4]) - Returns: - iou (Tensor[N, M]): the NxM matrix containing the pairwise - IoU values for every element in boxes1 and boxes2 - """ - - def box_area(box): - # box = 4xn - return (box[2] - box[0]) * (box[3] - box[1]) - - area1 = box_area(box1.T) - area2 = box_area(box2.T) - - # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2) - inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2) - return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter) - - -def wh_iou(wh1, wh2): - # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2 - wh1 = wh1[:, None] # [N,1,2] - wh2 = wh2[None] # [1,M,2] - inter = torch.min(wh1, wh2).prod(2) # [N,M] - return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter) + if isinstance(boxes, torch.Tensor): # faster individually + boxes[:, 0].clamp_(0, shape[1]) # x1 + boxes[:, 1].clamp_(0, shape[0]) # y1 + boxes[:, 2].clamp_(0, shape[1]) # x2 + boxes[:, 3].clamp_(0, shape[0]) # y2 + else: # np.array (faster grouped) + boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, shape[1]) # x1, x2 + boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, shape[0]) # y1, y2 def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False, @@ -601,39 +747,48 @@ def strip_optimizer(f='best.pt', s=''): # from utils.general import *; strip_op print(f"Optimizer stripped from {f},{(' saved as %s,' % s) if s else ''} {mb:.1f}MB") -def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''): - # Print mutation results to evolve.txt (for use with train.py --evolve) - a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys - b = '%10.3g' * len(hyp) % tuple(hyp.values()) # hyperparam values - c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3) - print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c)) +def print_mutation(results, hyp, save_dir, bucket): + evolve_csv, results_csv, evolve_yaml = save_dir / 'evolve.csv', save_dir / 'results.csv', save_dir / 'hyp_evolve.yaml' + keys = ('metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', + 'val/box_loss', 'val/obj_loss', 'val/cls_loss') + tuple(hyp.keys()) # [results + hyps] + keys = tuple(x.strip() for x in keys) + vals = results + tuple(hyp.values()) + n = len(keys) + # Download (optional) if bucket: - url = 'gs://%s/evolve.txt' % bucket - if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0): - os.system('gsutil cp %s .' % url) # download evolve.txt if larger than local + url = f'gs://{bucket}/evolve.csv' + if gsutil_getsize(url) > (os.path.getsize(evolve_csv) if os.path.exists(evolve_csv) else 0): + os.system(f'gsutil cp {url} {save_dir}') # download evolve.csv if larger than local + + # Log to evolve.csv + s = '' if evolve_csv.exists() else (('%20s,' * n % keys).rstrip(',') + '\n') # add header + with open(evolve_csv, 'a') as f: + f.write(s + ('%20.5g,' * n % vals).rstrip(',') + '\n') - with open('evolve.txt', 'a') as f: # append result - f.write(c + b + '\n') - x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0) # load unique rows - x = x[np.argsort(-fitness(x))] # sort - np.savetxt('evolve.txt', x, '%10.3g') # save sort by fitness + # Print to screen + print(colorstr('evolve: ') + ', '.join(f'{x.strip():>20s}' for x in keys)) + print(colorstr('evolve: ') + ', '.join(f'{x:20.5g}' for x in vals), end='\n\n\n') # Save yaml - for i, k in enumerate(hyp.keys()): - hyp[k] = float(x[0, i + 7]) - with open(yaml_file, 'w') as f: - results = tuple(x[0, :7]) - c = '%10.4g' * len(results) % results # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3) - f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n') + with open(evolve_yaml, 'w') as f: + data = pd.read_csv(evolve_csv) + data = data.rename(columns=lambda x: x.strip()) # strip keys + i = np.argmax(fitness(data.values[:, :7])) # + f.write('# YOLOv3 Hyperparameter Evolution Results\n' + + f'# Best generation: {i}\n' + + f'# Last generation: {len(data)}\n' + + '# ' + ', '.join(f'{x.strip():>20s}' for x in keys[:7]) + '\n' + + '# ' + ', '.join(f'{x:>20.5g}' for x in data.values[i, :7]) + '\n\n') yaml.safe_dump(hyp, f, sort_keys=False) if bucket: - os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket)) # upload + os.system(f'gsutil cp {evolve_csv} {evolve_yaml} gs://{bucket}') # upload def apply_classifier(x, model, img, im0): - # Apply a second stage classifier to yolo outputs + # Apply a second stage classifier to YOLO outputs + # Example model = torchvision.models.__dict__['efficientnet_b0'](pretrained=True).to(device).eval() im0 = [im0] if isinstance(im0, np.ndarray) else im0 for i, d in enumerate(x): # per image if d is not None and len(d): @@ -654,11 +809,11 @@ def apply_classifier(x, model, img, im0): for j, a in enumerate(d): # per item cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])] im = cv2.resize(cutout, (224, 224)) # BGR - # cv2.imwrite('test%i.jpg' % j, cutout) + # cv2.imwrite('example%i.jpg' % j, cutout) im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32 - im /= 255.0 # 0 - 255 to 0.0 - 1.0 + im /= 255 # 0 - 255 to 0.0 - 1.0 ims.append(im) pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction @@ -667,33 +822,20 @@ def apply_classifier(x, model, img, im0): return x -def save_one_box(xyxy, im, file='image.jpg', gain=1.02, pad=10, square=False, BGR=False, save=True): - # Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop - xyxy = torch.tensor(xyxy).view(-1, 4) - b = xyxy2xywh(xyxy) # boxes - if square: - b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # attempt rectangle to square - b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad - xyxy = xywh2xyxy(b).long() - clip_coords(xyxy, im.shape) - crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)] - if save: - cv2.imwrite(str(increment_path(file, mkdir=True).with_suffix('.jpg')), crop) - return crop - - def increment_path(path, exist_ok=False, sep='', mkdir=False): # Increment file or directory path, i.e. runs/exp --> runs/exp{sep}2, runs/exp{sep}3, ... etc. path = Path(path) # os-agnostic if path.exists() and not exist_ok: - suffix = path.suffix - path = path.with_suffix('') + path, suffix = (path.with_suffix(''), path.suffix) if path.is_file() else (path, '') dirs = glob.glob(f"{path}{sep}*") # similar paths matches = [re.search(rf"%s{sep}(\d+)" % path.stem, d) for d in dirs] i = [int(m.groups()[0]) for m in matches if m] # indices n = max(i) + 1 if i else 2 # increment number - path = Path(f"{path}{sep}{n}{suffix}") # update path - dir = path if path.suffix == '' else path.parent # directory - if not dir.exists() and mkdir: - dir.mkdir(parents=True, exist_ok=True) # make directory + path = Path(f"{path}{sep}{n}{suffix}") # increment path + if mkdir: + path.mkdir(parents=True, exist_ok=True) # make directory return path + + +# Variables +NCOLS = 0 if is_docker() else shutil.get_terminal_size().columns # terminal window size diff --git a/utils/google_app_engine/Dockerfile b/utils/google_app_engine/Dockerfile deleted file mode 100644 index 0155618f47..0000000000 --- a/utils/google_app_engine/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM gcr.io/google-appengine/python - -# Create a virtualenv for dependencies. This isolates these packages from -# system-level packages. -# Use -p python3 or -p python3.7 to select python version. Default is version 2. -RUN virtualenv /env -p python3 - -# Setting these environment variables are the same as running -# source /env/bin/activate. -ENV VIRTUAL_ENV /env -ENV PATH /env/bin:$PATH - -RUN apt-get update && apt-get install -y python-opencv - -# Copy the application's requirements.txt and run pip to install all -# dependencies into the virtualenv. -ADD requirements.txt /app/requirements.txt -RUN pip install -r /app/requirements.txt - -# Add the application source code. -ADD . /app - -# Run a WSGI server to serve the application. gunicorn must be declared as -# a dependency in requirements.txt. -CMD gunicorn -b :$PORT main:app diff --git a/utils/google_app_engine/additional_requirements.txt b/utils/google_app_engine/additional_requirements.txt deleted file mode 100644 index 2f81c8b400..0000000000 --- a/utils/google_app_engine/additional_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -# add these requirements in your app on top of the existing ones -pip==19.2 -Flask==1.0.2 -gunicorn==19.9.0 diff --git a/utils/google_app_engine/app.yaml b/utils/google_app_engine/app.yaml deleted file mode 100644 index bd162e44dd..0000000000 --- a/utils/google_app_engine/app.yaml +++ /dev/null @@ -1,14 +0,0 @@ -runtime: custom -env: flex - -service: yolov3app - -liveness_check: - initial_delay_sec: 600 - -manual_scaling: - instances: 1 -resources: - cpu: 1 - memory_gb: 4 - disk_size_gb: 20 \ No newline at end of file diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py new file mode 100644 index 0000000000..bf55fec860 --- /dev/null +++ b/utils/loggers/__init__.py @@ -0,0 +1,156 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Logging utils +""" + +import os +import warnings +from threading import Thread + +import pkg_resources as pkg +import torch +from torch.utils.tensorboard import SummaryWriter + +from utils.general import colorstr, emojis +from utils.loggers.wandb.wandb_utils import WandbLogger +from utils.plots import plot_images, plot_results +from utils.torch_utils import de_parallel + +LOGGERS = ('csv', 'tb', 'wandb') # text-file, TensorBoard, Weights & Biases +RANK = int(os.getenv('RANK', -1)) + +try: + import wandb + + assert hasattr(wandb, '__version__') # verify package import not local dir + if pkg.parse_version(wandb.__version__) >= pkg.parse_version('0.12.2') and RANK in [0, -1]: + wandb_login_success = wandb.login(timeout=30) + if not wandb_login_success: + wandb = None +except (ImportError, AssertionError): + wandb = None + + +class Loggers(): + # Loggers class + def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, include=LOGGERS): + self.save_dir = save_dir + self.weights = weights + self.opt = opt + self.hyp = hyp + self.logger = logger # for printing results to console + self.include = include + self.keys = ['train/box_loss', 'train/obj_loss', 'train/cls_loss', # train loss + 'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', # metrics + 'val/box_loss', 'val/obj_loss', 'val/cls_loss', # val loss + 'x/lr0', 'x/lr1', 'x/lr2'] # params + for k in LOGGERS: + setattr(self, k, None) # init empty logger dictionary + self.csv = True # always log to csv + + # Message + if not wandb: + prefix = colorstr('Weights & Biases: ') + s = f"{prefix}run 'pip install wandb' to automatically track and visualize YOLOv3 πŸš€ runs (RECOMMENDED)" + print(emojis(s)) + + # TensorBoard + s = self.save_dir + if 'tb' in self.include and not self.opt.evolve: + prefix = colorstr('TensorBoard: ') + self.logger.info(f"{prefix}Start with 'tensorboard --logdir {s.parent}', view at http://localhost:6006/") + self.tb = SummaryWriter(str(s)) + + # W&B + if wandb and 'wandb' in self.include: + wandb_artifact_resume = isinstance(self.opt.resume, str) and self.opt.resume.startswith('wandb-artifact://') + run_id = torch.load(self.weights).get('wandb_id') if self.opt.resume and not wandb_artifact_resume else None + self.opt.hyp = self.hyp # add hyperparameters + self.wandb = WandbLogger(self.opt, run_id) + else: + self.wandb = None + + def on_pretrain_routine_end(self): + # Callback runs on pre-train routine end + paths = self.save_dir.glob('*labels*.jpg') # training labels + if self.wandb: + self.wandb.log({"Labels": [wandb.Image(str(x), caption=x.name) for x in paths]}) + + def on_train_batch_end(self, ni, model, imgs, targets, paths, plots, sync_bn): + # Callback runs on train batch end + if plots: + if ni == 0: + if not sync_bn: # tb.add_graph() --sync known issue https://github.com/ultralytics/yolov5/issues/3754 + with warnings.catch_warnings(): + warnings.simplefilter('ignore') # suppress jit trace warning + self.tb.add_graph(torch.jit.trace(de_parallel(model), imgs[0:1], strict=False), []) + if ni < 3: + f = self.save_dir / f'train_batch{ni}.jpg' # filename + Thread(target=plot_images, args=(imgs, targets, paths, f), daemon=True).start() + if self.wandb and ni == 10: + files = sorted(self.save_dir.glob('train*.jpg')) + self.wandb.log({'Mosaics': [wandb.Image(str(f), caption=f.name) for f in files if f.exists()]}) + + def on_train_epoch_end(self, epoch): + # Callback runs on train epoch end + if self.wandb: + self.wandb.current_epoch = epoch + 1 + + def on_val_image_end(self, pred, predn, path, names, im): + # Callback runs on val image end + if self.wandb: + self.wandb.val_one_image(pred, predn, path, names, im) + + def on_val_end(self): + # Callback runs on val end + if self.wandb: + files = sorted(self.save_dir.glob('val*.jpg')) + self.wandb.log({"Validation": [wandb.Image(str(f), caption=f.name) for f in files]}) + + def on_fit_epoch_end(self, vals, epoch, best_fitness, fi): + # Callback runs at the end of each fit (train+val) epoch + x = {k: v for k, v in zip(self.keys, vals)} # dict + if self.csv: + file = self.save_dir / 'results.csv' + n = len(x) + 1 # number of cols + s = '' if file.exists() else (('%20s,' * n % tuple(['epoch'] + self.keys)).rstrip(',') + '\n') # add header + with open(file, 'a') as f: + f.write(s + ('%20.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') + + if self.tb: + for k, v in x.items(): + self.tb.add_scalar(k, v, epoch) + + if self.wandb: + self.wandb.log(x) + self.wandb.end_epoch(best_result=best_fitness == fi) + + def on_model_save(self, last, epoch, final_epoch, best_fitness, fi): + # Callback runs on model save event + if self.wandb: + if ((epoch + 1) % self.opt.save_period == 0 and not final_epoch) and self.opt.save_period != -1: + self.wandb.log_model(last.parent, self.opt, epoch, fi, best_model=best_fitness == fi) + + def on_train_end(self, last, best, plots, epoch, results): + # Callback runs on training end + if plots: + plot_results(file=self.save_dir / 'results.csv') # save results.png + files = ['results.png', 'confusion_matrix.png', *(f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R'))] + files = [(self.save_dir / f) for f in files if (self.save_dir / f).exists()] # filter + + if self.tb: + import cv2 + for f in files: + self.tb.add_image(f.stem, cv2.imread(str(f))[..., ::-1], epoch, dataformats='HWC') + + if self.wandb: + self.wandb.log({"Results": [wandb.Image(str(f), caption=f.name) for f in files]}) + # Calling wandb.log. TODO: Refactor this into WandbLogger.log_model + if not self.opt.evolve: + wandb.log_artifact(str(best if best.exists() else last), type='model', + name='run_' + self.wandb.wandb_run.id + '_model', + aliases=['latest', 'best', 'stripped']) + self.wandb.finish_run() + else: + self.wandb.finish_run() + self.wandb = WandbLogger(self.opt) diff --git a/utils/loggers/wandb/README.md b/utils/loggers/wandb/README.md new file mode 100644 index 0000000000..bae57bdabf --- /dev/null +++ b/utils/loggers/wandb/README.md @@ -0,0 +1,147 @@ +πŸ“š This guide explains how to use **Weights & Biases** (W&B) with YOLOv3 πŸš€. UPDATED 29 September 2021. +* [About Weights & Biases](#about-weights-&-biases) +* [First-Time Setup](#first-time-setup) +* [Viewing runs](#viewing-runs) +* [Advanced Usage: Dataset Versioning and Evaluation](#advanced-usage) +* [Reports: Share your work with the world!](#reports) + +## About Weights & Biases +Think of [W&B](https://wandb.ai/site?utm_campaign=repo_yolo_wandbtutorial) like GitHub for machine learning models. With a few lines of code, save everything you need to debug, compare and reproduce your models β€” architecture, hyperparameters, git commits, model weights, GPU usage, and even datasets and predictions. + +Used by top researchers including teams at OpenAI, Lyft, Github, and MILA, W&B is part of the new standard of best practices for machine learning. How W&B can help you optimize your machine learning workflows: + + * [Debug](https://wandb.ai/wandb/getting-started/reports/Visualize-Debug-Machine-Learning-Models--VmlldzoyNzY5MDk#Free-2) model performance in real time + * [GPU usage](https://wandb.ai/wandb/getting-started/reports/Visualize-Debug-Machine-Learning-Models--VmlldzoyNzY5MDk#System-4) visualized automatically + * [Custom charts](https://wandb.ai/wandb/customizable-charts/reports/Powerful-Custom-Charts-To-Debug-Model-Peformance--VmlldzoyNzY4ODI) for powerful, extensible visualization + * [Share insights](https://wandb.ai/wandb/getting-started/reports/Visualize-Debug-Machine-Learning-Models--VmlldzoyNzY5MDk#Share-8) interactively with collaborators + * [Optimize hyperparameters](https://docs.wandb.com/sweeps) efficiently + * [Track](https://docs.wandb.com/artifacts) datasets, pipelines, and production models + +## First-Time Setup +
+ Toggle Details +When you first train, W&B will prompt you to create a new account and will generate an **API key** for you. If you are an existing user you can retrieve your key from https://wandb.ai/authorize. This key is used to tell W&B where to log your data. You only need to supply your key once, and then it is remembered on the same device. + +W&B will create a cloud **project** (default is 'YOLOv3') for your training runs, and each new training run will be provided a unique run **name** within that project as project/name. You can also manually set your project and run name as: + + ```shell + $ python train.py --project ... --name ... + ``` + +YOLOv3 notebook example: Open In Colab Open In Kaggle +Screen Shot 2021-09-29 at 10 23 13 PM + + +
+ +## Viewing Runs +
+ Toggle Details +Run information streams from your environment to the W&B cloud console as you train. This allows you to monitor and even cancel runs in realtime . All important information is logged: + + * Training & Validation losses + * Metrics: Precision, Recall, mAP@0.5, mAP@0.5:0.95 + * Learning Rate over time + * A bounding box debugging panel, showing the training progress over time + * GPU: Type, **GPU Utilization**, power, temperature, **CUDA memory usage** + * System: Disk I/0, CPU utilization, RAM memory usage + * Your trained model as W&B Artifact + * Environment: OS and Python types, Git repository and state, **training command** + +

Weights & Biases dashboard

+ + +
+ +## Advanced Usage +You can leverage W&B artifacts and Tables integration to easily visualize and manage your datasets, models and training evaluations. Here are some quick examples to get you started. +
+

1. Visualize and Version Datasets

+ Log, visualize, dynamically query, and understand your data with W&B Tables. You can use the following command to log your dataset as a W&B Table. This will generate a {dataset}_wandb.yaml file which can be used to train from dataset artifact. +
+ Usage + Code $ python utils/logger/wandb/log_dataset.py --project ... --name ... --data .. + + ![Screenshot (64)](https://user-images.githubusercontent.com/15766192/128486078-d8433890-98a3-4d12-8986-b6c0e3fc64b9.png) +
+ +

2: Train and Log Evaluation simultaneousy

+ This is an extension of the previous section, but it'll also training after uploading the dataset. This also evaluation Table + Evaluation table compares your predictions and ground truths across the validation set for each epoch. It uses the references to the already uploaded datasets, + so no images will be uploaded from your system more than once. +
+ Usage + Code $ python utils/logger/wandb/log_dataset.py --data .. --upload_data + +![Screenshot (72)](https://user-images.githubusercontent.com/15766192/128979739-4cf63aeb-a76f-483f-8861-1c0100b938a5.png) +
+ +

3: Train using dataset artifact

+ When you upload a dataset as described in the first section, you get a new config file with an added `_wandb` to its name. This file contains the information that + can be used to train a model directly from the dataset artifact. This also logs evaluation +
+ Usage + Code $ python utils/logger/wandb/log_dataset.py --data {data}_wandb.yaml + +![Screenshot (72)](https://user-images.githubusercontent.com/15766192/128979739-4cf63aeb-a76f-483f-8861-1c0100b938a5.png) +
+ +

4: Save model checkpoints as artifacts

+ To enable saving and versioning checkpoints of your experiment, pass `--save_period n` with the base cammand, where `n` represents checkpoint interval. + You can also log both the dataset and model checkpoints simultaneously. If not passed, only the final model will be logged + +
+ Usage + Code $ python train.py --save_period 1 + +![Screenshot (68)](https://user-images.githubusercontent.com/15766192/128726138-ec6c1f60-639d-437d-b4ee-3acd9de47ef3.png) +
+ +
+ +

5: Resume runs from checkpoint artifacts.

+Any run can be resumed using artifacts if the --resume argument starts withΒ wandb-artifact://Β prefix followed by the run path, i.e,Β wandb-artifact://username/project/runid . This doesn't require the model checkpoint to be present on the local system. + +
+ Usage + Code $ python train.py --resume wandb-artifact://{run_path} + +![Screenshot (70)](https://user-images.githubusercontent.com/15766192/128728988-4e84b355-6c87-41ae-a591-14aecf45343e.png) +
+ +

6: Resume runs from dataset artifact & checkpoint artifacts.

+ Local dataset or model checkpoints are not required. This can be used to resume runs directly on a different device + The syntax is same as the previous section, but you'll need to lof both the dataset and model checkpoints as artifacts, i.e, set bot --upload_dataset or + train from _wandb.yaml file and set --save_period + +
+ Usage + Code $ python train.py --resume wandb-artifact://{run_path} + +![Screenshot (70)](https://user-images.githubusercontent.com/15766192/128728988-4e84b355-6c87-41ae-a591-14aecf45343e.png) +
+ +
+ + +

Reports

+W&B Reports can be created from your saved runs for sharing online. Once a report is created you will receive a link you can use to publically share your results. Here is an example report created from the COCO128 tutorial trainings of all YOLOv5 models ([link](https://wandb.ai/glenn-jocher/yolov5_tutorial/reports/YOLOv5-COCO128-Tutorial-Results--VmlldzozMDI5OTY)). + +Weights & Biases Reports + + +## Environments + +YOLOv3 may be run in any of the following up-to-date verified environments (with all dependencies including [CUDA](https://developer.nvidia.com/cuda)/[CUDNN](https://developer.nvidia.com/cudnn), [Python](https://www.python.org/) and [PyTorch](https://pytorch.org/) preinstalled): + +- **Google Colab and Kaggle** notebooks with free GPU: Open In Colab Open In Kaggle +- **Google Cloud** Deep Learning VM. See [GCP Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/GCP-Quickstart) +- **Amazon** Deep Learning AMI. See [AWS Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/AWS-Quickstart) +- **Docker Image**. See [Docker Quickstart Guide](https://github.com/ultralytics/yolov3/wiki/Docker-Quickstart) Docker Pulls + + +## Status + +![CI CPU testing](https://github.com/ultralytics/yolov3/workflows/CI%20CPU%20testing/badge.svg) + +If this badge is green, all [YOLOv3 GitHub Actions](https://github.com/ultralytics/yolov3/actions) Continuous Integration (CI) tests are currently passing. CI tests verify correct operation of YOLOv3 training ([train.py](https://github.com/ultralytics/yolov3/blob/master/train.py)), validation ([val.py](https://github.com/ultralytics/yolov3/blob/master/val.py)), inference ([detect.py](https://github.com/ultralytics/yolov3/blob/master/detect.py)) and export ([export.py](https://github.com/ultralytics/yolov3/blob/master/export.py)) on MacOS, Windows, and Ubuntu every 24 hours and on every commit. diff --git a/utils/aws/__init__.py b/utils/loggers/wandb/__init__.py similarity index 100% rename from utils/aws/__init__.py rename to utils/loggers/wandb/__init__.py diff --git a/utils/wandb_logging/log_dataset.py b/utils/loggers/wandb/log_dataset.py similarity index 61% rename from utils/wandb_logging/log_dataset.py rename to utils/loggers/wandb/log_dataset.py index fae76b048f..d3c77430ff 100644 --- a/utils/wandb_logging/log_dataset.py +++ b/utils/loggers/wandb/log_dataset.py @@ -1,16 +1,16 @@ import argparse -import yaml - from wandb_utils import WandbLogger +from utils.general import LOGGER + WANDB_ARTIFACT_PREFIX = 'wandb-artifact://' def create_dataset_artifact(opt): - with open(opt.data) as f: - data = yaml.safe_load(f) # data dict - logger = WandbLogger(opt, '', None, data, job_type='Dataset Creation') + logger = WandbLogger(opt, None, job_type='Dataset Creation') # TODO: return value unused + if not logger.wandb: + LOGGER.info("install wandb using `pip install wandb` to log the dataset") if __name__ == '__main__': @@ -18,6 +18,9 @@ def create_dataset_artifact(opt): parser.add_argument('--data', type=str, default='data/coco128.yaml', help='data.yaml path') parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset') parser.add_argument('--project', type=str, default='YOLOv3', help='name of W&B Project') + parser.add_argument('--entity', default=None, help='W&B entity') + parser.add_argument('--name', type=str, default='log dataset', help='name of W&B run') + opt = parser.parse_args() opt.resume = False # Explicitly disallow resume check for dataset upload job diff --git a/utils/loggers/wandb/sweep.py b/utils/loggers/wandb/sweep.py new file mode 100644 index 0000000000..5e24f96e13 --- /dev/null +++ b/utils/loggers/wandb/sweep.py @@ -0,0 +1,41 @@ +import sys +from pathlib import Path + +import wandb + +FILE = Path(__file__).resolve() +ROOT = FILE.parents[3] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH + +from train import parse_opt, train +from utils.callbacks import Callbacks +from utils.general import increment_path +from utils.torch_utils import select_device + + +def sweep(): + wandb.init() + # Get hyp dict from sweep agent + hyp_dict = vars(wandb.config).get("_items") + + # Workaround: get necessary opt args + opt = parse_opt(known=True) + opt.batch_size = hyp_dict.get("batch_size") + opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve)) + opt.epochs = hyp_dict.get("epochs") + opt.nosave = True + opt.data = hyp_dict.get("data") + opt.weights = str(opt.weights) + opt.cfg = str(opt.cfg) + opt.data = str(opt.data) + opt.hyp = str(opt.hyp) + opt.project = str(opt.project) + device = select_device(opt.device, batch_size=opt.batch_size) + + # train + train(hyp_dict, opt, device, callbacks=Callbacks()) + + +if __name__ == "__main__": + sweep() diff --git a/utils/loggers/wandb/sweep.yaml b/utils/loggers/wandb/sweep.yaml new file mode 100644 index 0000000000..c7790d75f6 --- /dev/null +++ b/utils/loggers/wandb/sweep.yaml @@ -0,0 +1,143 @@ +# Hyperparameters for training +# To set range- +# Provide min and max values as: +# parameter: +# +# min: scalar +# max: scalar +# OR +# +# Set a specific list of search space- +# parameter: +# values: [scalar1, scalar2, scalar3...] +# +# You can use grid, bayesian and hyperopt search strategy +# For more info on configuring sweeps visit - https://docs.wandb.ai/guides/sweeps/configuration + +program: utils/loggers/wandb/sweep.py +method: random +metric: + name: metrics/mAP_0.5 + goal: maximize + +parameters: + # hyperparameters: set either min, max range or values list + data: + value: "data/coco128.yaml" + batch_size: + values: [64] + epochs: + values: [10] + + lr0: + distribution: uniform + min: 1e-5 + max: 1e-1 + lrf: + distribution: uniform + min: 0.01 + max: 1.0 + momentum: + distribution: uniform + min: 0.6 + max: 0.98 + weight_decay: + distribution: uniform + min: 0.0 + max: 0.001 + warmup_epochs: + distribution: uniform + min: 0.0 + max: 5.0 + warmup_momentum: + distribution: uniform + min: 0.0 + max: 0.95 + warmup_bias_lr: + distribution: uniform + min: 0.0 + max: 0.2 + box: + distribution: uniform + min: 0.02 + max: 0.2 + cls: + distribution: uniform + min: 0.2 + max: 4.0 + cls_pw: + distribution: uniform + min: 0.5 + max: 2.0 + obj: + distribution: uniform + min: 0.2 + max: 4.0 + obj_pw: + distribution: uniform + min: 0.5 + max: 2.0 + iou_t: + distribution: uniform + min: 0.1 + max: 0.7 + anchor_t: + distribution: uniform + min: 2.0 + max: 8.0 + fl_gamma: + distribution: uniform + min: 0.0 + max: 0.1 + hsv_h: + distribution: uniform + min: 0.0 + max: 0.1 + hsv_s: + distribution: uniform + min: 0.0 + max: 0.9 + hsv_v: + distribution: uniform + min: 0.0 + max: 0.9 + degrees: + distribution: uniform + min: 0.0 + max: 45.0 + translate: + distribution: uniform + min: 0.0 + max: 0.9 + scale: + distribution: uniform + min: 0.0 + max: 0.9 + shear: + distribution: uniform + min: 0.0 + max: 10.0 + perspective: + distribution: uniform + min: 0.0 + max: 0.001 + flipud: + distribution: uniform + min: 0.0 + max: 1.0 + fliplr: + distribution: uniform + min: 0.0 + max: 1.0 + mosaic: + distribution: uniform + min: 0.0 + max: 1.0 + mixup: + distribution: uniform + min: 0.0 + max: 1.0 + copy_paste: + distribution: uniform + min: 0.0 + max: 1.0 diff --git a/utils/loggers/wandb/wandb_utils.py b/utils/loggers/wandb/wandb_utils.py new file mode 100644 index 0000000000..7087e4e95e --- /dev/null +++ b/utils/loggers/wandb/wandb_utils.py @@ -0,0 +1,532 @@ +"""Utilities and tools for tracking runs with Weights & Biases.""" + +import logging +import os +import sys +from contextlib import contextmanager +from pathlib import Path +from typing import Dict + +import pkg_resources as pkg +import yaml +from tqdm import tqdm + +FILE = Path(__file__).resolve() +ROOT = FILE.parents[3] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH + +from utils.datasets import LoadImagesAndLabels, img2label_paths +from utils.general import LOGGER, check_dataset, check_file + +try: + import wandb + + assert hasattr(wandb, '__version__') # verify package import not local dir +except (ImportError, AssertionError): + wandb = None + +RANK = int(os.getenv('RANK', -1)) +WANDB_ARTIFACT_PREFIX = 'wandb-artifact://' + + +def remove_prefix(from_string, prefix=WANDB_ARTIFACT_PREFIX): + return from_string[len(prefix):] + + +def check_wandb_config_file(data_config_file): + wandb_config = '_wandb.'.join(data_config_file.rsplit('.', 1)) # updated data.yaml path + if Path(wandb_config).is_file(): + return wandb_config + return data_config_file + + +def check_wandb_dataset(data_file): + is_trainset_wandb_artifact = False + is_valset_wandb_artifact = False + if check_file(data_file) and data_file.endswith('.yaml'): + with open(data_file, errors='ignore') as f: + data_dict = yaml.safe_load(f) + is_trainset_wandb_artifact = (isinstance(data_dict['train'], str) and + data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX)) + is_valset_wandb_artifact = (isinstance(data_dict['val'], str) and + data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX)) + if is_trainset_wandb_artifact or is_valset_wandb_artifact: + return data_dict + else: + return check_dataset(data_file) + + +def get_run_info(run_path): + run_path = Path(remove_prefix(run_path, WANDB_ARTIFACT_PREFIX)) + run_id = run_path.stem + project = run_path.parent.stem + entity = run_path.parent.parent.stem + model_artifact_name = 'run_' + run_id + '_model' + return entity, project, run_id, model_artifact_name + + +def check_wandb_resume(opt): + process_wandb_config_ddp_mode(opt) if RANK not in [-1, 0] else None + if isinstance(opt.resume, str): + if opt.resume.startswith(WANDB_ARTIFACT_PREFIX): + if RANK not in [-1, 0]: # For resuming DDP runs + entity, project, run_id, model_artifact_name = get_run_info(opt.resume) + api = wandb.Api() + artifact = api.artifact(entity + '/' + project + '/' + model_artifact_name + ':latest') + modeldir = artifact.download() + opt.weights = str(Path(modeldir) / "last.pt") + return True + return None + + +def process_wandb_config_ddp_mode(opt): + with open(check_file(opt.data), errors='ignore') as f: + data_dict = yaml.safe_load(f) # data dict + train_dir, val_dir = None, None + if isinstance(data_dict['train'], str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX): + api = wandb.Api() + train_artifact = api.artifact(remove_prefix(data_dict['train']) + ':' + opt.artifact_alias) + train_dir = train_artifact.download() + train_path = Path(train_dir) / 'data/images/' + data_dict['train'] = str(train_path) + + if isinstance(data_dict['val'], str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX): + api = wandb.Api() + val_artifact = api.artifact(remove_prefix(data_dict['val']) + ':' + opt.artifact_alias) + val_dir = val_artifact.download() + val_path = Path(val_dir) / 'data/images/' + data_dict['val'] = str(val_path) + if train_dir or val_dir: + ddp_data_path = str(Path(val_dir) / 'wandb_local_data.yaml') + with open(ddp_data_path, 'w') as f: + yaml.safe_dump(data_dict, f) + opt.data = ddp_data_path + + +class WandbLogger(): + """Log training runs, datasets, models, and predictions to Weights & Biases. + + This logger sends information to W&B at wandb.ai. By default, this information + includes hyperparameters, system configuration and metrics, model metrics, + and basic data metrics and analyses. + + By providing additional command line arguments to train.py, datasets, + models and predictions can also be logged. + + For more on how this logger is used, see the Weights & Biases documentation: + https://docs.wandb.com/guides/integrations/yolov5 + """ + + def __init__(self, opt, run_id=None, job_type='Training'): + """ + - Initialize WandbLogger instance + - Upload dataset if opt.upload_dataset is True + - Setup trainig processes if job_type is 'Training' + + arguments: + opt (namespace) -- Commandline arguments for this run + run_id (str) -- Run ID of W&B run to be resumed + job_type (str) -- To set the job_type for this run + + """ + # Pre-training routine -- + self.job_type = job_type + self.wandb, self.wandb_run = wandb, None if not wandb else wandb.run + self.val_artifact, self.train_artifact = None, None + self.train_artifact_path, self.val_artifact_path = None, None + self.result_artifact = None + self.val_table, self.result_table = None, None + self.bbox_media_panel_images = [] + self.val_table_path_map = None + self.max_imgs_to_log = 16 + self.wandb_artifact_data_dict = None + self.data_dict = None + # It's more elegant to stick to 1 wandb.init call, + # but useful config data is overwritten in the WandbLogger's wandb.init call + if isinstance(opt.resume, str): # checks resume from artifact + if opt.resume.startswith(WANDB_ARTIFACT_PREFIX): + entity, project, run_id, model_artifact_name = get_run_info(opt.resume) + model_artifact_name = WANDB_ARTIFACT_PREFIX + model_artifact_name + assert wandb, 'install wandb to resume wandb runs' + # Resume wandb-artifact:// runs here| workaround for not overwriting wandb.config + self.wandb_run = wandb.init(id=run_id, + project=project, + entity=entity, + resume='allow', + allow_val_change=True) + opt.resume = model_artifact_name + elif self.wandb: + self.wandb_run = wandb.init(config=opt, + resume="allow", + project='YOLOv3' if opt.project == 'runs/train' else Path(opt.project).stem, + entity=opt.entity, + name=opt.name if opt.name != 'exp' else None, + job_type=job_type, + id=run_id, + allow_val_change=True) if not wandb.run else wandb.run + if self.wandb_run: + if self.job_type == 'Training': + if opt.upload_dataset: + if not opt.resume: + self.wandb_artifact_data_dict = self.check_and_upload_dataset(opt) + + if opt.resume: + # resume from artifact + if isinstance(opt.resume, str) and opt.resume.startswith(WANDB_ARTIFACT_PREFIX): + self.data_dict = dict(self.wandb_run.config.data_dict) + else: # local resume + self.data_dict = check_wandb_dataset(opt.data) + else: + self.data_dict = check_wandb_dataset(opt.data) + self.wandb_artifact_data_dict = self.wandb_artifact_data_dict or self.data_dict + + # write data_dict to config. useful for resuming from artifacts. Do this only when not resuming. + self.wandb_run.config.update({'data_dict': self.wandb_artifact_data_dict}, + allow_val_change=True) + self.setup_training(opt) + + if self.job_type == 'Dataset Creation': + self.data_dict = self.check_and_upload_dataset(opt) + + def check_and_upload_dataset(self, opt): + """ + Check if the dataset format is compatible and upload it as W&B artifact + + arguments: + opt (namespace)-- Commandline arguments for current run + + returns: + Updated dataset info dictionary where local dataset paths are replaced by WAND_ARFACT_PREFIX links. + """ + assert wandb, 'Install wandb to upload dataset' + config_path = self.log_dataset_artifact(opt.data, + opt.single_cls, + 'YOLOv3' if opt.project == 'runs/train' else Path(opt.project).stem) + LOGGER.info(f"Created dataset config file {config_path}") + with open(config_path, errors='ignore') as f: + wandb_data_dict = yaml.safe_load(f) + return wandb_data_dict + + def setup_training(self, opt): + """ + Setup the necessary processes for training YOLO models: + - Attempt to download model checkpoint and dataset artifacts if opt.resume stats with WANDB_ARTIFACT_PREFIX + - Update data_dict, to contain info of previous run if resumed and the paths of dataset artifact if downloaded + - Setup log_dict, initialize bbox_interval + + arguments: + opt (namespace) -- commandline arguments for this run + + """ + self.log_dict, self.current_epoch = {}, 0 + self.bbox_interval = opt.bbox_interval + if isinstance(opt.resume, str): + modeldir, _ = self.download_model_artifact(opt) + if modeldir: + self.weights = Path(modeldir) / "last.pt" + config = self.wandb_run.config + opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp = str( + self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs, \ + config.hyp + data_dict = self.data_dict + if self.val_artifact is None: # If --upload_dataset is set, use the existing artifact, don't download + self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(data_dict.get('train'), + opt.artifact_alias) + self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(data_dict.get('val'), + opt.artifact_alias) + + if self.train_artifact_path is not None: + train_path = Path(self.train_artifact_path) / 'data/images/' + data_dict['train'] = str(train_path) + if self.val_artifact_path is not None: + val_path = Path(self.val_artifact_path) / 'data/images/' + data_dict['val'] = str(val_path) + + if self.val_artifact is not None: + self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation") + self.result_table = wandb.Table(["epoch", "id", "ground truth", "prediction", "avg_confidence"]) + self.val_table = self.val_artifact.get("val") + if self.val_table_path_map is None: + self.map_val_table_path() + if opt.bbox_interval == -1: + self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1 + train_from_artifact = self.train_artifact_path is not None and self.val_artifact_path is not None + # Update the the data_dict to point to local artifacts dir + if train_from_artifact: + self.data_dict = data_dict + + def download_dataset_artifact(self, path, alias): + """ + download the model checkpoint artifact if the path starts with WANDB_ARTIFACT_PREFIX + + arguments: + path -- path of the dataset to be used for training + alias (str)-- alias of the artifact to be download/used for training + + returns: + (str, wandb.Artifact) -- path of the downladed dataset and it's corresponding artifact object if dataset + is found otherwise returns (None, None) + """ + if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX): + artifact_path = Path(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias) + dataset_artifact = wandb.use_artifact(artifact_path.as_posix().replace("\\", "/")) + assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'" + datadir = dataset_artifact.download() + return datadir, dataset_artifact + return None, None + + def download_model_artifact(self, opt): + """ + download the model checkpoint artifact if the resume path starts with WANDB_ARTIFACT_PREFIX + + arguments: + opt (namespace) -- Commandline arguments for this run + """ + if opt.resume.startswith(WANDB_ARTIFACT_PREFIX): + model_artifact = wandb.use_artifact(remove_prefix(opt.resume, WANDB_ARTIFACT_PREFIX) + ":latest") + assert model_artifact is not None, 'Error: W&B model artifact doesn\'t exist' + modeldir = model_artifact.download() + epochs_trained = model_artifact.metadata.get('epochs_trained') + total_epochs = model_artifact.metadata.get('total_epochs') + is_finished = total_epochs is None + assert not is_finished, 'training is finished, can only resume incomplete runs.' + return modeldir, model_artifact + return None, None + + def log_model(self, path, opt, epoch, fitness_score, best_model=False): + """ + Log the model checkpoint as W&B artifact + + arguments: + path (Path) -- Path of directory containing the checkpoints + opt (namespace) -- Command line arguments for this run + epoch (int) -- Current epoch number + fitness_score (float) -- fitness score for current epoch + best_model (boolean) -- Boolean representing if the current checkpoint is the best yet. + """ + model_artifact = wandb.Artifact('run_' + wandb.run.id + '_model', type='model', metadata={ + 'original_url': str(path), + 'epochs_trained': epoch + 1, + 'save period': opt.save_period, + 'project': opt.project, + 'total_epochs': opt.epochs, + 'fitness_score': fitness_score + }) + model_artifact.add_file(str(path / 'last.pt'), name='last.pt') + wandb.log_artifact(model_artifact, + aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else '']) + LOGGER.info(f"Saving model artifact on epoch {epoch + 1}") + + def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False): + """ + Log the dataset as W&B artifact and return the new data file with W&B links + + arguments: + data_file (str) -- the .yaml file with information about the dataset like - path, classes etc. + single_class (boolean) -- train multi-class data as single-class + project (str) -- project name. Used to construct the artifact path + overwrite_config (boolean) -- overwrites the data.yaml file if set to true otherwise creates a new + file with _wandb postfix. Eg -> data_wandb.yaml + + returns: + the new .yaml file with artifact links. it can be used to start training directly from artifacts + """ + self.data_dict = check_dataset(data_file) # parse and check + data = dict(self.data_dict) + nc, names = (1, ['item']) if single_cls else (int(data['nc']), data['names']) + names = {k: v for k, v in enumerate(names)} # to index dictionary + self.train_artifact = self.create_dataset_table(LoadImagesAndLabels( + data['train'], rect=True, batch_size=1), names, name='train') if data.get('train') else None + self.val_artifact = self.create_dataset_table(LoadImagesAndLabels( + data['val'], rect=True, batch_size=1), names, name='val') if data.get('val') else None + if data.get('train'): + data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train') + if data.get('val'): + data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val') + path = Path(data_file).stem + path = (path if overwrite_config else path + '_wandb') + '.yaml' # updated data.yaml path + data.pop('download', None) + data.pop('path', None) + with open(path, 'w') as f: + yaml.safe_dump(data, f) + + if self.job_type == 'Training': # builds correct artifact pipeline graph + self.wandb_run.use_artifact(self.val_artifact) + self.wandb_run.use_artifact(self.train_artifact) + self.val_artifact.wait() + self.val_table = self.val_artifact.get('val') + self.map_val_table_path() + else: + self.wandb_run.log_artifact(self.train_artifact) + self.wandb_run.log_artifact(self.val_artifact) + return path + + def map_val_table_path(self): + """ + Map the validation dataset Table like name of file -> it's id in the W&B Table. + Useful for - referencing artifacts for evaluation. + """ + self.val_table_path_map = {} + LOGGER.info("Mapping dataset") + for i, data in enumerate(tqdm(self.val_table.data)): + self.val_table_path_map[data[3]] = data[0] + + def create_dataset_table(self, dataset: LoadImagesAndLabels, class_to_id: Dict[int,str], name: str = 'dataset'): + """ + Create and return W&B artifact containing W&B Table of the dataset. + + arguments: + dataset -- instance of LoadImagesAndLabels class used to iterate over the data to build Table + class_to_id -- hash map that maps class ids to labels + name -- name of the artifact + + returns: + dataset artifact to be logged or used + """ + # TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging + artifact = wandb.Artifact(name=name, type="dataset") + img_files = tqdm([dataset.path]) if isinstance(dataset.path, str) and Path(dataset.path).is_dir() else None + img_files = tqdm(dataset.img_files) if not img_files else img_files + for img_file in img_files: + if Path(img_file).is_dir(): + artifact.add_dir(img_file, name='data/images') + labels_path = 'labels'.join(dataset.path.rsplit('images', 1)) + artifact.add_dir(labels_path, name='data/labels') + else: + artifact.add_file(img_file, name='data/images/' + Path(img_file).name) + label_file = Path(img2label_paths([img_file])[0]) + artifact.add_file(str(label_file), + name='data/labels/' + label_file.name) if label_file.exists() else None + table = wandb.Table(columns=["id", "train_image", "Classes", "name"]) + class_set = wandb.Classes([{'id': id, 'name': name} for id, name in class_to_id.items()]) + for si, (img, labels, paths, shapes) in enumerate(tqdm(dataset)): + box_data, img_classes = [], {} + for cls, *xywh in labels[:, 1:].tolist(): + cls = int(cls) + box_data.append({"position": {"middle": [xywh[0], xywh[1]], "width": xywh[2], "height": xywh[3]}, + "class_id": cls, + "box_caption": "%s" % (class_to_id[cls])}) + img_classes[cls] = class_to_id[cls] + boxes = {"ground_truth": {"box_data": box_data, "class_labels": class_to_id}} # inference-space + table.add_data(si, wandb.Image(paths, classes=class_set, boxes=boxes), list(img_classes.values()), + Path(paths).name) + artifact.add(table, name) + return artifact + + def log_training_progress(self, predn, path, names): + """ + Build evaluation Table. Uses reference from validation dataset table. + + arguments: + predn (list): list of predictions in the native space in the format - [xmin, ymin, xmax, ymax, confidence, class] + path (str): local path of the current evaluation image + names (dict(int, str)): hash map that maps class ids to labels + """ + class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()]) + box_data = [] + total_conf = 0 + for *xyxy, conf, cls in predn.tolist(): + if conf >= 0.25: + box_data.append( + {"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]}, + "class_id": int(cls), + "box_caption": f"{names[cls]} {conf:.3f}", + "scores": {"class_score": conf}, + "domain": "pixel"}) + total_conf += conf + boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space + id = self.val_table_path_map[Path(path).name] + self.result_table.add_data(self.current_epoch, + id, + self.val_table.data[id][1], + wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set), + total_conf / max(1, len(box_data)) + ) + + def val_one_image(self, pred, predn, path, names, im): + """ + Log validation data for one image. updates the result Table if validation dataset is uploaded and log bbox media panel + + arguments: + pred (list): list of scaled predictions in the format - [xmin, ymin, xmax, ymax, confidence, class] + predn (list): list of predictions in the native space - [xmin, ymin, xmax, ymax, confidence, class] + path (str): local path of the current evaluation image + """ + if self.val_table and self.result_table: # Log Table if Val dataset is uploaded as artifact + self.log_training_progress(predn, path, names) + + if len(self.bbox_media_panel_images) < self.max_imgs_to_log and self.current_epoch > 0: + if self.current_epoch % self.bbox_interval == 0: + box_data = [{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]}, + "class_id": int(cls), + "box_caption": f"{names[cls]} {conf:.3f}", + "scores": {"class_score": conf}, + "domain": "pixel"} for *xyxy, conf, cls in pred.tolist()] + boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space + self.bbox_media_panel_images.append(wandb.Image(im, boxes=boxes, caption=path.name)) + + def log(self, log_dict): + """ + save the metrics to the logging dictionary + + arguments: + log_dict (Dict) -- metrics/media to be logged in current step + """ + if self.wandb_run: + for key, value in log_dict.items(): + self.log_dict[key] = value + + def end_epoch(self, best_result=False): + """ + commit the log_dict, model artifacts and Tables to W&B and flush the log_dict. + + arguments: + best_result (boolean): Boolean representing if the result of this evaluation is best or not + """ + if self.wandb_run: + with all_logging_disabled(): + if self.bbox_media_panel_images: + self.log_dict["BoundingBoxDebugger"] = self.bbox_media_panel_images + try: + wandb.log(self.log_dict) + except BaseException as e: + LOGGER.info(f"An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}") + self.wandb_run.finish() + self.wandb_run = None + + self.log_dict = {} + self.bbox_media_panel_images = [] + if self.result_artifact: + self.result_artifact.add(self.result_table, 'result') + wandb.log_artifact(self.result_artifact, aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), + ('best' if best_result else '')]) + + wandb.log({"evaluation": self.result_table}) + self.result_table = wandb.Table(["epoch", "id", "ground truth", "prediction", "avg_confidence"]) + self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation") + + def finish_run(self): + """ + Log metrics if any and finish the current W&B run + """ + if self.wandb_run: + if self.log_dict: + with all_logging_disabled(): + wandb.log(self.log_dict) + wandb.run.finish() + + +@contextmanager +def all_logging_disabled(highest_level=logging.CRITICAL): + """ source - https://gist.github.com/simon-weber/7853144 + A context manager that will prevent any logging messages triggered during the body from being processed. + :param highest_level: the maximum logging level in use. + This would only need to be changed if a custom level greater than CRITICAL is defined. + """ + previous_level = logging.root.manager.disable + logging.disable(highest_level) + try: + yield + finally: + logging.disable(previous_level) diff --git a/utils/loss.py b/utils/loss.py index a2c5cce795..dfde60adfd 100644 --- a/utils/loss.py +++ b/utils/loss.py @@ -1,9 +1,12 @@ -# Loss functions +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Loss functions +""" import torch import torch.nn as nn -from utils.general import bbox_iou +from utils.metrics import bbox_iou from utils.torch_utils import is_parallel @@ -15,7 +18,7 @@ def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#iss class BCEBlurWithLogitsLoss(nn.Module): # BCEwithLogitLoss() with reduced missing label effects. def __init__(self, alpha=0.05): - super(BCEBlurWithLogitsLoss, self).__init__() + super().__init__() self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss() self.alpha = alpha @@ -32,7 +35,7 @@ def forward(self, pred, true): class FocalLoss(nn.Module): # Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5) def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): - super(FocalLoss, self).__init__() + super().__init__() self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss() self.gamma = gamma self.alpha = alpha @@ -62,7 +65,7 @@ def forward(self, pred, true): class QFocalLoss(nn.Module): # Wraps Quality focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5) def __init__(self, loss_fcn, gamma=1.5, alpha=0.25): - super(QFocalLoss, self).__init__() + super().__init__() self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss() self.gamma = gamma self.alpha = alpha @@ -88,7 +91,7 @@ def forward(self, pred, true): class ComputeLoss: # Compute losses def __init__(self, model, autobalance=False): - super(ComputeLoss, self).__init__() + self.sort_obj_iou = False device = next(model.parameters()).device # get model device h = model.hyp # hyperparameters @@ -105,9 +108,9 @@ def __init__(self, model, autobalance=False): BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g) det = model.module.model[-1] if is_parallel(model) else model.model[-1] # Detect() module - self.balance = {3: [4.0, 1.0, 0.4]}.get(det.nl, [4.0, 1.0, 0.25, 0.06, .02]) # P3-P7 + self.balance = {3: [4.0, 1.0, 0.4]}.get(det.nl, [4.0, 1.0, 0.25, 0.06, 0.02]) # P3-P7 self.ssi = list(det.stride).index(16) if autobalance else 0 # stride 16 index - self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, model.gr, h, autobalance + self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance for k in 'na', 'nc', 'nl', 'anchors': setattr(self, k, getattr(det, k)) @@ -126,14 +129,18 @@ def __call__(self, p, targets): # predictions, targets, model ps = pi[b, a, gj, gi] # prediction subset corresponding to targets # Regression - pxy = ps[:, :2].sigmoid() * 2. - 0.5 + pxy = ps[:, :2].sigmoid() * 2 - 0.5 pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i] pbox = torch.cat((pxy, pwh), 1) # predicted box iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target) lbox += (1.0 - iou).mean() # iou loss # Objectness - tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio + score_iou = iou.detach().clamp(0).type(tobj.dtype) + if self.sort_obj_iou: + sort_id = torch.argsort(score_iou) + b, a, gj, gi, score_iou = b[sort_id], a[sort_id], gj[sort_id], gi[sort_id], score_iou[sort_id] + tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * score_iou # iou ratio # Classification if self.nc > 1: # cls loss (only if multiple classes) @@ -157,8 +164,7 @@ def __call__(self, p, targets): # predictions, targets, model lcls *= self.hyp['cls'] bs = tobj.shape[0] # batch size - loss = lbox + lobj + lcls - return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach() + return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach() def build_targets(self, p, targets): # Build targets for compute_loss(), input targets(image,class,x,y,w,h) @@ -170,7 +176,7 @@ def build_targets(self, p, targets): g = 0.5 # bias off = torch.tensor([[0, 0], - # [1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m + [1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m # [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm ], device=targets.device).float() * g # offsets @@ -183,17 +189,17 @@ def build_targets(self, p, targets): if nt: # Matches r = t[:, :, 4:6] / anchors[:, None] # wh ratio - j = torch.max(r, 1. / r).max(2)[0] < self.hyp['anchor_t'] # compare + j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2)) t = t[j] # filter # Offsets gxy = t[:, 2:4] # grid xy gxi = gain[[2, 3]] - gxy # inverse - j, k = ((gxy % 1. < g) & (gxy > 1.)).T - l, m = ((gxi % 1. < g) & (gxi > 1.)).T - j = torch.stack((torch.ones_like(j),)) - t = t.repeat((off.shape[0], 1, 1))[j] + j, k = ((gxy % 1 < g) & (gxy > 1)).T + l, m = ((gxi % 1 < g) & (gxi > 1)).T + j = torch.stack((torch.ones_like(j), j, k, l, m)) + t = t.repeat((5, 1, 1))[j] offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j] else: t = targets[0] diff --git a/utils/metrics.py b/utils/metrics.py index 323c84b6c8..c8fcac5f0c 100644 --- a/utils/metrics.py +++ b/utils/metrics.py @@ -1,13 +1,16 @@ -# Model validation metrics +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Model validation metrics +""" +import math +import warnings from pathlib import Path import matplotlib.pyplot as plt import numpy as np import torch -from . import general - def fitness(x): # Model fitness as a weighted combination of metrics @@ -68,6 +71,8 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names # Compute F1 (harmonic mean of precision and recall) f1 = 2 * p * r / (p + r + 1e-16) + names = [v for k, v in names.items() if k in unique_classes] # list: only classes that have data + names = {i: v for i, v in enumerate(names)} # to dict if plot: plot_pr_curve(px, py, ap, Path(save_dir) / 'PR_curve.png', names) plot_mc_curve(px, f1, Path(save_dir) / 'F1_curve.png', names, ylabel='F1') @@ -88,8 +93,8 @@ def compute_ap(recall, precision): """ # Append sentinel values to beginning and end - mrec = np.concatenate(([0.], recall, [recall[-1] + 0.01])) - mpre = np.concatenate(([1.], precision, [0.])) + mrec = np.concatenate(([0.0], recall, [1.0])) + mpre = np.concatenate(([1.0], precision, [0.0])) # Compute the precision envelope mpre = np.flip(np.maximum.accumulate(np.flip(mpre))) @@ -127,7 +132,7 @@ def process_batch(self, detections, labels): detections = detections[detections[:, 4] > self.conf] gt_classes = labels[:, 0].int() detection_classes = detections[:, 5].int() - iou = general.box_iou(labels[:, 1:], detections[:, :4]) + iou = box_iou(labels[:, 1:], detections[:, :4]) x = torch.where(iou > self.iou_thres) if x[0].shape[0]: @@ -157,30 +162,135 @@ def process_batch(self, detections, labels): def matrix(self): return self.matrix - def plot(self, save_dir='', names=()): + def plot(self, normalize=True, save_dir='', names=()): try: import seaborn as sn - array = self.matrix / (self.matrix.sum(0).reshape(1, self.nc + 1) + 1E-6) # normalize + array = self.matrix / ((self.matrix.sum(0).reshape(1, -1) + 1E-6) if normalize else 1) # normalize columns array[array < 0.005] = np.nan # don't annotate (would appear as 0.00) fig = plt.figure(figsize=(12, 9), tight_layout=True) sn.set(font_scale=1.0 if self.nc < 50 else 0.8) # for label size labels = (0 < len(names) < 99) and len(names) == self.nc # apply names to ticklabels - sn.heatmap(array, annot=self.nc < 30, annot_kws={"size": 8}, cmap='Blues', fmt='.2f', square=True, - xticklabels=names + ['background FP'] if labels else "auto", - yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1)) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') # suppress empty matrix RuntimeWarning: All-NaN slice encountered + sn.heatmap(array, annot=self.nc < 30, annot_kws={"size": 8}, cmap='Blues', fmt='.2f', square=True, + xticklabels=names + ['background FP'] if labels else "auto", + yticklabels=names + ['background FN'] if labels else "auto").set_facecolor((1, 1, 1)) fig.axes[0].set_xlabel('True') fig.axes[0].set_ylabel('Predicted') fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250) + plt.close() except Exception as e: - pass + print(f'WARNING: ConfusionMatrix plot failure: {e}') def print(self): for i in range(self.nc + 1): print(' '.join(map(str, self.matrix[i]))) +def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7): + # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4 + box2 = box2.T + + # Get the coordinates of bounding boxes + if x1y1x2y2: # x1, y1, x2, y2 = box1 + b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3] + b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3] + else: # transform from xywh to xyxy + b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2 + b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2 + b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2 + b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2 + + # Intersection area + inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \ + (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0) + + # Union Area + w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps + w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps + union = w1 * h1 + w2 * h2 - inter + eps + + iou = inter / union + if GIoU or DIoU or CIoU: + cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width + ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height + if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1 + c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared + rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + + (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared + if DIoU: + return iou - rho2 / c2 # DIoU + elif CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47 + v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2) + with torch.no_grad(): + alpha = v / (v - iou + (1 + eps)) + return iou - (rho2 / c2 + v * alpha) # CIoU + else: # GIoU https://arxiv.org/pdf/1902.09630.pdf + c_area = cw * ch + eps # convex area + return iou - (c_area - union) / c_area # GIoU + else: + return iou # IoU + + +def box_iou(box1, box2): + # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py + """ + Return intersection-over-union (Jaccard index) of boxes. + Both sets of boxes are expected to be in (x1, y1, x2, y2) format. + Arguments: + box1 (Tensor[N, 4]) + box2 (Tensor[M, 4]) + Returns: + iou (Tensor[N, M]): the NxM matrix containing the pairwise + IoU values for every element in boxes1 and boxes2 + """ + + def box_area(box): + # box = 4xn + return (box[2] - box[0]) * (box[3] - box[1]) + + area1 = box_area(box1.T) + area2 = box_area(box2.T) + + # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2) + inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2) + return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter) + + +def bbox_ioa(box1, box2, eps=1E-7): + """ Returns the intersection over box2 area given box1, box2. Boxes are x1y1x2y2 + box1: np.array of shape(4) + box2: np.array of shape(nx4) + returns: np.array of shape(n) + """ + + box2 = box2.transpose() + + # Get the coordinates of bounding boxes + b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3] + b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3] + + # Intersection area + inter_area = (np.minimum(b1_x2, b2_x2) - np.maximum(b1_x1, b2_x1)).clip(0) * \ + (np.minimum(b1_y2, b2_y2) - np.maximum(b1_y1, b2_y1)).clip(0) + + # box2 area + box2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1) + eps + + # Intersection over box2 area + return inter_area / box2_area + + +def wh_iou(wh1, wh2): + # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2 + wh1 = wh1[:, None] # [N,1,2] + wh2 = wh2[None] # [1,M,2] + inter = torch.min(wh1, wh2).prod(2) # [N,M] + return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter) + + # Plots ---------------------------------------------------------------------------------------------------------------- def plot_pr_curve(px, py, ap, save_dir='pr_curve.png', names=()): @@ -201,6 +311,7 @@ def plot_pr_curve(px, py, ap, save_dir='pr_curve.png', names=()): ax.set_ylim(0, 1) plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left") fig.savefig(Path(save_dir), dpi=250) + plt.close() def plot_mc_curve(px, py, save_dir='mc_curve.png', names=(), xlabel='Confidence', ylabel='Metric'): @@ -221,3 +332,4 @@ def plot_mc_curve(px, py, save_dir='mc_curve.png', names=(), xlabel='Confidence' ax.set_ylim(0, 1) plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left") fig.savefig(Path(save_dir), dpi=250) + plt.close() diff --git a/utils/plots.py b/utils/plots.py index 2ae36523f3..16ae44a7e1 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -1,9 +1,10 @@ -# Plotting utils +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Plotting utils +""" -import glob import math import os -import random from copy import copy from pathlib import Path @@ -12,15 +13,17 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -import seaborn as sns +import seaborn as sn import torch -import yaml from PIL import Image, ImageDraw, ImageFont -from utils.general import xywh2xyxy, xyxy2xywh +from utils.general import (LOGGER, Timeout, check_requirements, clip_coords, increment_path, is_ascii, is_chinese, + try_except, user_config_dir, xywh2xyxy, xyxy2xywh) from utils.metrics import fitness # Settings +CONFIG_DIR = user_config_dir() # Ultralytics settings dir +RANK = int(os.getenv('RANK', -1)) matplotlib.rc('font', **{'size': 11}) matplotlib.use('Agg') # for writing to files only @@ -46,6 +49,105 @@ def hex2rgb(h): # rgb order (PIL) colors = Colors() # create instance for 'from utils.plots import colors' +def check_font(font='Arial.ttf', size=10): + # Return a PIL TrueType Font, downloading to CONFIG_DIR if necessary + font = Path(font) + font = font if font.exists() else (CONFIG_DIR / font.name) + try: + return ImageFont.truetype(str(font) if font.exists() else font.name, size) + except Exception as e: # download if missing + url = "https://ultralytics.com/assets/" + font.name + print(f'Downloading {url} to {font}...') + torch.hub.download_url_to_file(url, str(font), progress=False) + try: + return ImageFont.truetype(str(font), size) + except TypeError: + check_requirements('Pillow>=8.4.0') # known issue https://github.com/ultralytics/yolov5/issues/5374 + + +class Annotator: + if RANK in (-1, 0): + check_font() # download TTF if necessary + + # Annotator for train/val mosaics and jpgs and detect/hub inference annotations + def __init__(self, im, line_width=None, font_size=None, font='Arial.ttf', pil=False, example='abc'): + assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to Annotator() input images.' + self.pil = pil or not is_ascii(example) or is_chinese(example) + if self.pil: # use PIL + self.im = im if isinstance(im, Image.Image) else Image.fromarray(im) + self.draw = ImageDraw.Draw(self.im) + self.font = check_font(font='Arial.Unicode.ttf' if is_chinese(example) else font, + size=font_size or max(round(sum(self.im.size) / 2 * 0.035), 12)) + else: # use cv2 + self.im = im + self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) # line width + + def box_label(self, box, label='', color=(128, 128, 128), txt_color=(255, 255, 255)): + # Add one xyxy box to image with label + if self.pil or not is_ascii(label): + self.draw.rectangle(box, width=self.lw, outline=color) # box + if label: + w, h = self.font.getsize(label) # text width, height + outside = box[1] - h >= 0 # label fits outside box + self.draw.rectangle([box[0], + box[1] - h if outside else box[1], + box[0] + w + 1, + box[1] + 1 if outside else box[1] + h + 1], fill=color) + # self.draw.text((box[0], box[1]), label, fill=txt_color, font=self.font, anchor='ls') # for PIL>8.0 + self.draw.text((box[0], box[1] - h if outside else box[1]), label, fill=txt_color, font=self.font) + else: # cv2 + p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3])) + cv2.rectangle(self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA) + if label: + tf = max(self.lw - 1, 1) # font thickness + w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[0] # text width, height + outside = p1[1] - h - 3 >= 0 # label fits outside box + p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3 + cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA) # filled + cv2.putText(self.im, label, (p1[0], p1[1] - 2 if outside else p1[1] + h + 2), 0, self.lw / 3, txt_color, + thickness=tf, lineType=cv2.LINE_AA) + + def rectangle(self, xy, fill=None, outline=None, width=1): + # Add rectangle to image (PIL-only) + self.draw.rectangle(xy, fill, outline, width) + + def text(self, xy, text, txt_color=(255, 255, 255)): + # Add text to image (PIL-only) + w, h = self.font.getsize(text) # text width, height + self.draw.text((xy[0], xy[1] - h + 1), text, fill=txt_color, font=self.font) + + def result(self): + # Return annotated image as array + return np.asarray(self.im) + + +def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')): + """ + x: Features to be visualized + module_type: Module type + stage: Module stage within model + n: Maximum number of feature maps to plot + save_dir: Directory to save results + """ + if 'Detect' not in module_type: + batch, channels, height, width = x.shape # batch, channels, height, width + if height > 1 and width > 1: + f = f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename + + blocks = torch.chunk(x[0].cpu(), channels, dim=0) # select batch index 0, block by channels + n = min(n, channels) # number of plots + fig, ax = plt.subplots(math.ceil(n / 8), 8, tight_layout=True) # 8 rows x n/8 cols + ax = ax.ravel() + plt.subplots_adjust(wspace=0.05, hspace=0.05) + for i in range(n): + ax[i].imshow(blocks[i].squeeze()) # cmap='gray' + ax[i].axis('off') + + print(f'Saving {save_dir / f}... ({n}/{channels})') + plt.savefig(save_dir / f, dpi=300, bbox_inches='tight') + plt.close() + + def hist2d(x, y, n=100): # 2d histogram used in labels.png and evolve.png xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n) @@ -68,54 +170,6 @@ def butter_lowpass(cutoff, fs, order): return filtfilt(b, a, data) # forward-backward filter -def plot_one_box(x, im, color=(128, 128, 128), label=None, line_thickness=3): - # Plots one bounding box on image 'im' using OpenCV - assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to plot_on_box() input image.' - tl = line_thickness or round(0.002 * (im.shape[0] + im.shape[1]) / 2) + 1 # line/font thickness - c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3])) - cv2.rectangle(im, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA) - if label: - tf = max(tl - 1, 1) # font thickness - t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] - c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3 - cv2.rectangle(im, c1, c2, color, -1, cv2.LINE_AA) # filled - cv2.putText(im, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA) - - -def plot_one_box_PIL(box, im, color=(128, 128, 128), label=None, line_thickness=None): - # Plots one bounding box on image 'im' using PIL - im = Image.fromarray(im) - draw = ImageDraw.Draw(im) - line_thickness = line_thickness or max(int(min(im.size) / 200), 2) - draw.rectangle(box, width=line_thickness, outline=color) # plot - if label: - font = ImageFont.truetype("Arial.ttf", size=max(round(max(im.size) / 40), 12)) - txt_width, txt_height = font.getsize(label) - draw.rectangle([box[0], box[1] - txt_height + 4, box[0] + txt_width, box[1]], fill=color) - draw.text((box[0], box[1] - txt_height + 1), label, fill=(255, 255, 255), font=font) - return np.asarray(im) - - -def plot_wh_methods(): # from utils.plots import *; plot_wh_methods() - # Compares the two methods for width-height anchor multiplication - # https://github.com/ultralytics/yolov3/issues/168 - x = np.arange(-4.0, 4.0, .1) - ya = np.exp(x) - yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2 - - fig = plt.figure(figsize=(6, 3), tight_layout=True) - plt.plot(x, ya, '.-', label='YOLOv3') - plt.plot(x, yb ** 2, '.-', label='YOLOv5 ^2') - plt.plot(x, yb ** 1.6, '.-', label='YOLOv5 ^1.6') - plt.xlim(left=-4, right=4) - plt.ylim(bottom=0, top=6) - plt.xlabel('input') - plt.ylabel('output') - plt.grid() - plt.legend() - fig.savefig('comparison.png', dpi=200) - - def output_to_target(output): # Convert model output to target format [batch_id, class_id, x, y, w, h, conf] targets = [] @@ -125,82 +179,65 @@ def output_to_target(output): return np.array(targets) -def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16): +def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=1920, max_subplots=16): # Plot image grid with labels - if isinstance(images, torch.Tensor): images = images.cpu().float().numpy() if isinstance(targets, torch.Tensor): targets = targets.cpu().numpy() - - # un-normalise if np.max(images[0]) <= 1: - images *= 255 - - tl = 3 # line thickness - tf = max(tl - 1, 1) # font thickness + images *= 255 # de-normalise (optional) bs, _, h, w = images.shape # batch size, _, height, width bs = min(bs, max_subplots) # limit plot images ns = np.ceil(bs ** 0.5) # number of subplots (square) - # Check if we should resize - scale_factor = max_size / max(h, w) - if scale_factor < 1: - h = math.ceil(scale_factor * h) - w = math.ceil(scale_factor * w) - + # Build Image mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init - for i, img in enumerate(images): + for i, im in enumerate(images): if i == max_subplots: # if last batch has fewer images than we expect break - - block_x = int(w * (i // ns)) - block_y = int(h * (i % ns)) - - img = img.transpose(1, 2, 0) - if scale_factor < 1: - img = cv2.resize(img, (w, h)) - - mosaic[block_y:block_y + h, block_x:block_x + w, :] = img + x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin + im = im.transpose(1, 2, 0) + mosaic[y:y + h, x:x + w, :] = im + + # Resize (optional) + scale = max_size / ns / max(h, w) + if scale < 1: + h = math.ceil(scale * h) + w = math.ceil(scale * w) + mosaic = cv2.resize(mosaic, tuple(int(x * ns) for x in (w, h))) + + # Annotate + fs = int((h + w) * ns * 0.01) # font size + annotator = Annotator(mosaic, line_width=round(fs / 10), font_size=fs, pil=True) + for i in range(i + 1): + x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin + annotator.rectangle([x, y, x + w, y + h], None, (255, 255, 255), width=2) # borders + if paths: + annotator.text((x + 5, y + 5 + h), text=Path(paths[i]).name[:40], txt_color=(220, 220, 220)) # filenames if len(targets) > 0: - image_targets = targets[targets[:, 0] == i] - boxes = xywh2xyxy(image_targets[:, 2:6]).T - classes = image_targets[:, 1].astype('int') - labels = image_targets.shape[1] == 6 # labels if no conf column - conf = None if labels else image_targets[:, 6] # check for confidence presence (label vs pred) + ti = targets[targets[:, 0] == i] # image targets + boxes = xywh2xyxy(ti[:, 2:6]).T + classes = ti[:, 1].astype('int') + labels = ti.shape[1] == 6 # labels if no conf column + conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred) if boxes.shape[1]: if boxes.max() <= 1.01: # if normalized with tolerance 0.01 boxes[[0, 2]] *= w # scale to pixels boxes[[1, 3]] *= h - elif scale_factor < 1: # absolute coords need scale if image scales - boxes *= scale_factor - boxes[[0, 2]] += block_x - boxes[[1, 3]] += block_y - for j, box in enumerate(boxes.T): - cls = int(classes[j]) + elif scale < 1: # absolute coords need scale if image scales + boxes *= scale + boxes[[0, 2]] += x + boxes[[1, 3]] += y + for j, box in enumerate(boxes.T.tolist()): + cls = classes[j] color = colors(cls) cls = names[cls] if names else cls if labels or conf[j] > 0.25: # 0.25 conf thresh - label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j]) - plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl) - - # Draw image filename labels - if paths: - label = Path(paths[i]).name[:40] # trim to 40 char - t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] - cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf, - lineType=cv2.LINE_AA) - - # Image border - cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3) - - if fname: - r = min(1280. / max(h, w) / ns, 1.0) # ratio to limit image size - mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA) - # cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save - Image.fromarray(mosaic).save(fname) # PIL save - return mosaic + label = f'{cls}' if labels else f'{cls} {conf[j]:.1f}' + annotator.box_label(box, label, color=color) + annotator.im.save(fname) # save def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''): @@ -220,9 +257,9 @@ def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''): plt.close() -def plot_test_txt(): # from utils.plots import *; plot_test() - # Plot test.txt histograms - x = np.loadtxt('test.txt', dtype=np.float32) +def plot_val_txt(): # from utils.plots import *; plot_val() + # Plot val.txt histograms + x = np.loadtxt('val.txt', dtype=np.float32) box = xyxy2xywh(x[:, :4]) cx, cy = box[:, 0], box[:, 1] @@ -244,29 +281,32 @@ def plot_targets_txt(): # from utils.plots import *; plot_targets_txt() fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True) ax = ax.ravel() for i in range(4): - ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std())) + ax[i].hist(x[i], bins=100, label=f'{x[i].mean():.3g} +/- {x[i].std():.3g}') ax[i].legend() ax[i].set_title(s[i]) plt.savefig('targets.jpg', dpi=200) -def plot_study_txt(path='', x=None): # from utils.plots import *; plot_study_txt() - # Plot study.txt generated by test.py - fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True) - # ax = ax.ravel() +def plot_val_study(file='', dir='', x=None): # from utils.plots import *; plot_val_study() + # Plot file=study.txt generated by val.py (or plot all study*.txt in dir) + save_dir = Path(file).parent if file else Path(dir) + plot2 = False # plot additional results + if plot2: + ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)[1].ravel() fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True) - # for f in [Path(path) / f'study_coco_{x}.txt' for x in ['yolov3-tiny', 'yolov3', 'yolov3-spp', 'yolov5l']]: - for f in sorted(Path(path).glob('study*.txt')): + # for f in [save_dir / f'study_coco_{x}.txt' for x in ['yolov3', 'yolov3-spp', 'yolov3-tiny']]: + for f in sorted(save_dir.glob('study*.txt')): y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T x = np.arange(y.shape[1]) if x is None else np.array(x) - s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)'] - # for i in range(7): - # ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8) - # ax[i].set_title(s[i]) + if plot2: + s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_preprocess (ms/img)', 't_inference (ms/img)', 't_NMS (ms/img)'] + for i in range(7): + ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8) + ax[i].set_title(s[i]) j = y[3].argmax() + 1 - ax2.plot(y[6, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8, + ax2.plot(y[5, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8, label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO')) ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5], @@ -275,22 +315,26 @@ def plot_study_txt(path='', x=None): # from utils.plots import *; plot_study_tx ax2.grid(alpha=0.2) ax2.set_yticks(np.arange(20, 60, 5)) ax2.set_xlim(0, 57) - ax2.set_ylim(15, 55) + ax2.set_ylim(25, 55) ax2.set_xlabel('GPU Speed (ms/img)') ax2.set_ylabel('COCO AP val') ax2.legend(loc='lower right') - plt.savefig(str(Path(path).name) + '.png', dpi=300) + f = save_dir / 'study.png' + print(f'Saving {f}...') + plt.savefig(f, dpi=300) -def plot_labels(labels, names=(), save_dir=Path(''), loggers=None): +@try_except # known issue https://github.com/ultralytics/yolov5/issues/5395 +@Timeout(30) # known issue https://github.com/ultralytics/yolov5/issues/5611 +def plot_labels(labels, names=(), save_dir=Path('')): # plot dataset labels - print('Plotting labels... ') + LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ") c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes nc = int(c.max() + 1) # number of classes x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height']) # seaborn correlogram - sns.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9)) + sn.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9)) plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200) plt.close() @@ -298,15 +342,15 @@ def plot_labels(labels, names=(), save_dir=Path(''), loggers=None): matplotlib.use('svg') # faster ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel() y = ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8) - # [y[2].patches[i].set_color([x / 255 for x in colors(i)]) for i in range(nc)] # update colors bug #3195 + # [y[2].patches[i].set_color([x / 255 for x in colors(i)]) for i in range(nc)] # update colors bug #3195 ax[0].set_ylabel('instances') if 0 < len(names) < 30: ax[0].set_xticks(range(len(names))) ax[0].set_xticklabels(names, rotation=90, fontsize=10) else: ax[0].set_xlabel('classes') - sns.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9) - sns.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9) + sn.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9) + sn.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9) # rectangles labels[:, 1:3] = 0.5 # center @@ -325,34 +369,57 @@ def plot_labels(labels, names=(), save_dir=Path(''), loggers=None): matplotlib.use('Agg') plt.close() - # loggers - for k, v in loggers.items() or {}: - if k == 'wandb' and v: - v.log({"Labels": [v.Image(str(x), caption=x.name) for x in save_dir.glob('*labels*.jpg')]}, commit=False) - -def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.plots import *; plot_evolution() - # Plot hyperparameter evolution results in evolve.txt - with open(yaml_file) as f: - hyp = yaml.safe_load(f) - x = np.loadtxt('evolve.txt', ndmin=2) +def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *; plot_evolve() + # Plot evolve.csv hyp evolution results + evolve_csv = Path(evolve_csv) + data = pd.read_csv(evolve_csv) + keys = [x.strip() for x in data.columns] + x = data.values f = fitness(x) - # weights = (f - f.min()) ** 2 # for weighted results + j = np.argmax(f) # max fitness index plt.figure(figsize=(10, 12), tight_layout=True) matplotlib.rc('font', **{'size': 8}) - for i, (k, v) in enumerate(hyp.items()): - y = x[:, i + 7] - # mu = (y * weights).sum() / weights.sum() # best weighted result - mu = y[f.argmax()] # best single result + for i, k in enumerate(keys[7:]): + v = x[:, 7 + i] + mu = v[j] # best single result plt.subplot(6, 5, i + 1) - plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none') + plt.scatter(v, f, c=hist2d(v, f, 20), cmap='viridis', alpha=.8, edgecolors='none') plt.plot(mu, f.max(), 'k+', markersize=15) - plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters + plt.title(f'{k} = {mu:.3g}', fontdict={'size': 9}) # limit to 40 characters if i % 5 != 0: plt.yticks([]) - print('%15s: %.3g' % (k, mu)) - plt.savefig('evolve.png', dpi=200) - print('\nPlot saved as evolve.png') + print(f'{k:>15}: {mu:.3g}') + f = evolve_csv.with_suffix('.png') # filename + plt.savefig(f, dpi=200) + plt.close() + print(f'Saved {f}') + + +def plot_results(file='path/to/results.csv', dir=''): + # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') + save_dir = Path(file).parent if file else Path(dir) + fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) + ax = ax.ravel() + files = list(save_dir.glob('results*.csv')) + assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.' + for fi, f in enumerate(files): + try: + data = pd.read_csv(f) + s = [x.strip() for x in data.columns] + x = data.values[:, 0] + for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]): + y = data.values[:, j] + # y[y == 0] = np.nan # don't show zero values + ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=8) + ax[i].set_title(s[j], fontsize=12) + # if j in [8, 9, 10]: # share train and val loss y axes + # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5]) + except Exception as e: + print(f'Warning: Plotting error for {f}: {e}') + ax[1].legend() + fig.savefig(save_dir / 'results.png', dpi=200) + plt.close() def profile_idetection(start=0, stop=0, labels=(), save_dir=''): @@ -381,66 +448,22 @@ def profile_idetection(start=0, stop=0, labels=(), save_dir=''): else: a.remove() except Exception as e: - print('Warning: Plotting error for %s; %s' % (f, e)) - + print(f'Warning: Plotting error for {f}; {e}') ax[1].legend() plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200) -def plot_results_overlay(start=0, stop=0): # from utils.plots import *; plot_results_overlay() - # Plot training 'results*.txt', overlaying train and val losses - s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends - t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles - for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')): - results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T - n = results.shape[1] # number of rows - x = range(start, min(stop, n) if stop else n) - fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True) - ax = ax.ravel() - for i in range(5): - for j in [i, i + 5]: - y = results[j, x] - ax[i].plot(x, y, marker='.', label=s[j]) - # y_smooth = butter_lowpass_filtfilt(y) - # ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j]) - - ax[i].set_title(t[i]) - ax[i].legend() - ax[i].set_ylabel(f) if i == 0 else None # add filename - fig.savefig(f.replace('.txt', '.png'), dpi=200) - - -def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''): - # Plot training 'results*.txt'. from utils.plots import *; plot_results(save_dir='runs/train/exp') - fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) - ax = ax.ravel() - s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall', - 'val Box', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95'] - if bucket: - # files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id] - files = ['results%g.txt' % x for x in id] - c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id) - os.system(c) - else: - files = list(Path(save_dir).glob('results*.txt')) - assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir) - for fi, f in enumerate(files): - try: - results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T - n = results.shape[1] # number of rows - x = range(start, min(stop, n) if stop else n) - for i in range(10): - y = results[i, x] - if i in [0, 1, 2, 5, 6, 7]: - y[y == 0] = np.nan # don't show zero loss values - # y /= y[0] # normalize - label = labels[fi] if len(labels) else f.stem - ax[i].plot(x, y, marker='.', label=label, linewidth=2, markersize=8) - ax[i].set_title(s[i]) - # if i in [5, 6, 7]: # share train and val loss y axes - # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5]) - except Exception as e: - print('Warning: Plotting error for %s; %s' % (f, e)) - - ax[1].legend() - fig.savefig(Path(save_dir) / 'results.png', dpi=200) +def save_one_box(xyxy, im, file='image.jpg', gain=1.02, pad=10, square=False, BGR=False, save=True): + # Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop + xyxy = torch.tensor(xyxy).view(-1, 4) + b = xyxy2xywh(xyxy) # boxes + if square: + b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # attempt rectangle to square + b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad + xyxy = xywh2xyxy(b).long() + clip_coords(xyxy, im.shape) + crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)] + if save: + file.parent.mkdir(parents=True, exist_ok=True) # make directory + cv2.imwrite(str(increment_path(file).with_suffix('.jpg')), crop) + return crop diff --git a/utils/torch_utils.py b/utils/torch_utils.py index 9114112a7b..d3692297aa 100644 --- a/utils/torch_utils.py +++ b/utils/torch_utils.py @@ -1,7 +1,9 @@ -# YOLOv3 PyTorch utils +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +PyTorch utils +""" import datetime -import logging import math import os import platform @@ -12,16 +14,16 @@ from pathlib import Path import torch -import torch.backends.cudnn as cudnn +import torch.distributed as dist import torch.nn as nn import torch.nn.functional as F -import torchvision + +from utils.general import LOGGER try: - import thop # for FLOPS computation + import thop # for FLOPs computation except ImportError: thop = None -logger = logging.getLogger(__name__) @contextmanager @@ -30,19 +32,10 @@ def torch_distributed_zero_first(local_rank: int): Decorator to make all processes in distributed training wait for each local_master to do something. """ if local_rank not in [-1, 0]: - torch.distributed.barrier() + dist.barrier(device_ids=[local_rank]) yield if local_rank == 0: - torch.distributed.barrier() - - -def init_torch_seeds(seed=0): - # Speed-reproducibility tradeoff https://pytorch.org/docs/stable/notes/randomness.html - torch.manual_seed(seed) - if seed == 0: # slower, more reproducible - cudnn.benchmark, cudnn.deterministic = False, True - else: # faster, less reproducible - cudnn.benchmark, cudnn.deterministic = True, False + dist.barrier(device_ids=[0]) def date_modified(path=__file__): @@ -60,10 +53,11 @@ def git_describe(path=Path(__file__).parent): # path must be a directory return '' # not a git repository -def select_device(device='', batch_size=None): +def select_device(device='', batch_size=None, newline=True): # device = 'cpu' or '0' or '0,1,2,3' s = f'YOLOv3 πŸš€ {git_describe() or date_modified()} torch {torch.__version__} ' # string - cpu = device.lower() == 'cpu' + device = str(device).strip().lower().replace('cuda:', '') # to string, 'cuda:0' to '0' + cpu = device == 'cpu' if cpu: os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False elif device: # non-cpu device requested @@ -72,65 +66,80 @@ def select_device(device='', batch_size=None): cuda = not cpu and torch.cuda.is_available() if cuda: - devices = device.split(',') if device else range(torch.cuda.device_count()) # i.e. 0,1,6,7 + devices = device.split(',') if device else '0' # range(torch.cuda.device_count()) # i.e. 0,1,6,7 n = len(devices) # device count if n > 1 and batch_size: # check batch_size is divisible by device_count assert batch_size % n == 0, f'batch-size {batch_size} not multiple of GPU count {n}' - space = ' ' * len(s) + space = ' ' * (len(s) + 1) for i, d in enumerate(devices): p = torch.cuda.get_device_properties(i) - s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / 1024 ** 2}MB)\n" # bytes to MB + s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / 1024 ** 2:.0f}MiB)\n" # bytes to MB else: s += 'CPU\n' - logger.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe + if not newline: + s = s.rstrip() + LOGGER.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe return torch.device('cuda:0' if cuda else 'cpu') -def time_synchronized(): +def time_sync(): # pytorch-accurate time if torch.cuda.is_available(): torch.cuda.synchronize() return time.time() -def profile(x, ops, n=100, device=None): - # profile a pytorch module or list of modules. Example usage: - # x = torch.randn(16, 3, 640, 640) # input +def profile(input, ops, n=10, device=None): + # speed/memory/FLOPs profiler + # + # Usage: + # input = torch.randn(16, 3, 640, 640) # m1 = lambda x: x * torch.sigmoid(x) # m2 = nn.SiLU() - # profile(x, [m1, m2], n=100) # profile speed over 100 iterations - - device = device or torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') - x = x.to(device) - x.requires_grad = True - print(torch.__version__, device.type, torch.cuda.get_device_properties(0) if device.type == 'cuda' else '') - print(f"\n{'Params':>12s}{'GFLOPS':>12s}{'forward (ms)':>16s}{'backward (ms)':>16s}{'input':>24s}{'output':>24s}") - for m in ops if isinstance(ops, list) else [ops]: - m = m.to(device) if hasattr(m, 'to') else m # device - m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m # type - dtf, dtb, t = 0., 0., [0., 0., 0.] # dt forward, backward - try: - flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPS - except: - flops = 0 - - for _ in range(n): - t[0] = time_synchronized() - y = m(x) - t[1] = time_synchronized() + # profile(input, [m1, m2], n=100) # profile over 100 iterations + + results = [] + device = device or select_device() + print(f"{'Params':>12s}{'GFLOPs':>12s}{'GPU_mem (GB)':>14s}{'forward (ms)':>14s}{'backward (ms)':>14s}" + f"{'input':>24s}{'output':>24s}") + + for x in input if isinstance(input, list) else [input]: + x = x.to(device) + x.requires_grad = True + for m in ops if isinstance(ops, list) else [ops]: + m = m.to(device) if hasattr(m, 'to') else m # device + m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m + tf, tb, t = 0, 0, [0, 0, 0] # dt forward, backward try: - _ = y.sum().backward() - t[2] = time_synchronized() - except: # no backward method - t[2] = float('nan') - dtf += (t[1] - t[0]) * 1000 / n # ms per op forward - dtb += (t[2] - t[1]) * 1000 / n # ms per op backward + flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPs + except: + flops = 0 - s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list' - s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list' - p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters - print(f'{p:12}{flops:12.4g}{dtf:16.4g}{dtb:16.4g}{str(s_in):>24s}{str(s_out):>24s}') + try: + for _ in range(n): + t[0] = time_sync() + y = m(x) + t[1] = time_sync() + try: + _ = (sum(yi.sum() for yi in y) if isinstance(y, list) else y).sum().backward() + t[2] = time_sync() + except Exception as e: # no backward method + # print(e) # for debug + t[2] = float('nan') + tf += (t[1] - t[0]) * 1000 / n # ms per op forward + tb += (t[2] - t[1]) * 1000 / n # ms per op backward + mem = torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0 # (GB) + s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list' + s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list' + p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters + print(f'{p:12}{flops:12.4g}{mem:>14.3f}{tf:14.4g}{tb:14.4g}{str(s_in):>24s}{str(s_out):>24s}') + results.append([p, flops, mem, tf, tb, s_in, s_out]) + except Exception as e: + print(e) + results.append(None) + torch.cuda.empty_cache() + return results def is_parallel(model): @@ -143,11 +152,6 @@ def de_parallel(model): return model.module if is_parallel(model) else model -def intersect_dicts(da, db, exclude=()): - # Dictionary intersection of matching keys and shapes, omitting 'exclude' keys, using da values - return {k: v for k, v in da.items() if k in db and not any(x in k for x in exclude) and v.shape == db[k].shape} - - def initialize_weights(model): for m in model.modules(): t = type(m) @@ -156,7 +160,7 @@ def initialize_weights(model): elif t is nn.BatchNorm2d: m.eps = 1e-3 m.momentum = 0.03 - elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6]: + elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]: m.inplace = True @@ -167,7 +171,7 @@ def find_modules(model, mclass=nn.Conv2d): def sparsity(model): # Return global model sparsity - a, b = 0., 0. + a, b = 0, 0 for p in model.parameters(): a += p.numel() b += (p == 0).sum() @@ -213,42 +217,23 @@ def model_info(model, verbose=False, img_size=640): n_p = sum(x.numel() for x in model.parameters()) # number parameters n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients if verbose: - print('%5s %40s %9s %12s %20s %10s %10s' % ('layer', 'name', 'gradient', 'parameters', 'shape', 'mu', 'sigma')) + print(f"{'layer':>5} {'name':>40} {'gradient':>9} {'parameters':>12} {'shape':>20} {'mu':>10} {'sigma':>10}") for i, (name, p) in enumerate(model.named_parameters()): name = name.replace('module_list.', '') print('%5g %40s %9s %12g %20s %10.3g %10.3g' % (i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std())) - try: # FLOPS + try: # FLOPs from thop import profile stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32 img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device) # input - flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPS + flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPs img_size = img_size if isinstance(img_size, list) else [img_size, img_size] # expand if int/float - fs = ', %.1f GFLOPS' % (flops * img_size[0] / stride * img_size[1] / stride) # 640x640 GFLOPS + fs = ', %.1f GFLOPs' % (flops * img_size[0] / stride * img_size[1] / stride) # 640x640 GFLOPs except (ImportError, Exception): fs = '' - logger.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}") - - -def load_classifier(name='resnet101', n=2): - # Loads a pretrained model reshaped to n-class output - model = torchvision.models.__dict__[name](pretrained=True) - - # ResNet model properties - # input_size = [3, 224, 224] - # input_space = 'RGB' - # input_range = [0, 1] - # mean = [0.485, 0.456, 0.406] - # std = [0.229, 0.224, 0.225] - - # Reshape output to n classes - filters = model.fc.weight.shape[1] - model.fc.bias = nn.Parameter(torch.zeros(n), requires_grad=True) - model.fc.weight = nn.Parameter(torch.zeros(n, filters), requires_grad=True) - model.fc.out_features = n - return model + LOGGER.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}") def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416) @@ -260,7 +245,7 @@ def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416) s = (int(h * ratio), int(w * ratio)) # new size img = F.interpolate(img, size=s, mode='bilinear', align_corners=False) # resize if not same_shape: # pad/crop img - h, w = [math.ceil(x * ratio / gs) * gs for x in (h, w)] + h, w = (math.ceil(x * ratio / gs) * gs for x in (h, w)) return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean @@ -273,6 +258,29 @@ def copy_attr(a, b, include=(), exclude=()): setattr(a, k, v) +class EarlyStopping: + # simple early stopper + def __init__(self, patience=30): + self.best_fitness = 0.0 # i.e. mAP + self.best_epoch = 0 + self.patience = patience or float('inf') # epochs to wait after fitness stops improving to stop + self.possible_stop = False # possible stop may occur next epoch + + def __call__(self, epoch, fitness): + if fitness >= self.best_fitness: # >= 0 to allow for early zero-fitness stage of training + self.best_epoch = epoch + self.best_fitness = fitness + delta = epoch - self.best_epoch # epochs without improvement + self.possible_stop = delta >= (self.patience - 1) # possible stop may occur next epoch + stop = delta >= self.patience # stop training if patience exceeded + if stop: + LOGGER.info(f'Stopping training early as no improvement observed in last {self.patience} epochs. ' + f'Best results observed at epoch {self.best_epoch}, best model saved as best.pt.\n' + f'To update EarlyStopping(patience={self.patience}) pass a new patience value, ' + f'i.e. `python train.py --patience 300` or use `--patience 0` to disable EarlyStopping.') + return stop + + class ModelEMA: """ Model Exponential Moving Average from https://github.com/rwightman/pytorch-image-models Keep a moving average of everything in the model state_dict (parameters and buffers). @@ -303,7 +311,7 @@ def update(self, model): for k, v in self.ema.state_dict().items(): if v.dtype.is_floating_point: v *= d - v += (1. - d) * msd[k].detach() + v += (1 - d) * msd[k].detach() def update_attr(self, model, include=(), exclude=('process_group', 'reducer')): # Update EMA attributes diff --git a/utils/wandb_logging/__init__.py b/utils/wandb_logging/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/utils/wandb_logging/wandb_utils.py b/utils/wandb_logging/wandb_utils.py deleted file mode 100644 index 12bd320cc3..0000000000 --- a/utils/wandb_logging/wandb_utils.py +++ /dev/null @@ -1,318 +0,0 @@ -"""Utilities and tools for tracking runs with Weights & Biases.""" -import json -import sys -from pathlib import Path - -import torch -import yaml -from tqdm import tqdm - -sys.path.append(str(Path(__file__).parent.parent.parent)) # add utils/ to path -from utils.datasets import LoadImagesAndLabels -from utils.datasets import img2label_paths -from utils.general import colorstr, xywh2xyxy, check_dataset, check_file - -try: - import wandb - from wandb import init, finish -except ImportError: - wandb = None - -WANDB_ARTIFACT_PREFIX = 'wandb-artifact://' - - -def remove_prefix(from_string, prefix=WANDB_ARTIFACT_PREFIX): - return from_string[len(prefix):] - - -def check_wandb_config_file(data_config_file): - wandb_config = '_wandb.'.join(data_config_file.rsplit('.', 1)) # updated data.yaml path - if Path(wandb_config).is_file(): - return wandb_config - return data_config_file - - -def get_run_info(run_path): - run_path = Path(remove_prefix(run_path, WANDB_ARTIFACT_PREFIX)) - run_id = run_path.stem - project = run_path.parent.stem - entity = run_path.parent.parent.stem - model_artifact_name = 'run_' + run_id + '_model' - return entity, project, run_id, model_artifact_name - - -def check_wandb_resume(opt): - process_wandb_config_ddp_mode(opt) if opt.global_rank not in [-1, 0] else None - if isinstance(opt.resume, str): - if opt.resume.startswith(WANDB_ARTIFACT_PREFIX): - if opt.global_rank not in [-1, 0]: # For resuming DDP runs - entity, project, run_id, model_artifact_name = get_run_info(opt.resume) - api = wandb.Api() - artifact = api.artifact(entity + '/' + project + '/' + model_artifact_name + ':latest') - modeldir = artifact.download() - opt.weights = str(Path(modeldir) / "last.pt") - return True - return None - - -def process_wandb_config_ddp_mode(opt): - with open(check_file(opt.data)) as f: - data_dict = yaml.safe_load(f) # data dict - train_dir, val_dir = None, None - if isinstance(data_dict['train'], str) and data_dict['train'].startswith(WANDB_ARTIFACT_PREFIX): - api = wandb.Api() - train_artifact = api.artifact(remove_prefix(data_dict['train']) + ':' + opt.artifact_alias) - train_dir = train_artifact.download() - train_path = Path(train_dir) / 'data/images/' - data_dict['train'] = str(train_path) - - if isinstance(data_dict['val'], str) and data_dict['val'].startswith(WANDB_ARTIFACT_PREFIX): - api = wandb.Api() - val_artifact = api.artifact(remove_prefix(data_dict['val']) + ':' + opt.artifact_alias) - val_dir = val_artifact.download() - val_path = Path(val_dir) / 'data/images/' - data_dict['val'] = str(val_path) - if train_dir or val_dir: - ddp_data_path = str(Path(val_dir) / 'wandb_local_data.yaml') - with open(ddp_data_path, 'w') as f: - yaml.safe_dump(data_dict, f) - opt.data = ddp_data_path - - -class WandbLogger(): - """Log training runs, datasets, models, and predictions to Weights & Biases. - - This logger sends information to W&B at wandb.ai. By default, this information - includes hyperparameters, system configuration and metrics, model metrics, - and basic data metrics and analyses. - - By providing additional command line arguments to train.py, datasets, - models and predictions can also be logged. - - For more on how this logger is used, see the Weights & Biases documentation: - https://docs.wandb.com/guides/integrations/yolov5 - """ - def __init__(self, opt, name, run_id, data_dict, job_type='Training'): - # Pre-training routine -- - self.job_type = job_type - self.wandb, self.wandb_run, self.data_dict = wandb, None if not wandb else wandb.run, data_dict - # It's more elegant to stick to 1 wandb.init call, but useful config data is overwritten in the WandbLogger's wandb.init call - if isinstance(opt.resume, str): # checks resume from artifact - if opt.resume.startswith(WANDB_ARTIFACT_PREFIX): - entity, project, run_id, model_artifact_name = get_run_info(opt.resume) - model_artifact_name = WANDB_ARTIFACT_PREFIX + model_artifact_name - assert wandb, 'install wandb to resume wandb runs' - # Resume wandb-artifact:// runs here| workaround for not overwriting wandb.config - self.wandb_run = wandb.init(id=run_id, project=project, entity=entity, resume='allow') - opt.resume = model_artifact_name - elif self.wandb: - self.wandb_run = wandb.init(config=opt, - resume="allow", - project='YOLOv3' if opt.project == 'runs/train' else Path(opt.project).stem, - entity=opt.entity, - name=name, - job_type=job_type, - id=run_id) if not wandb.run else wandb.run - if self.wandb_run: - if self.job_type == 'Training': - if not opt.resume: - wandb_data_dict = self.check_and_upload_dataset(opt) if opt.upload_dataset else data_dict - # Info useful for resuming from artifacts - self.wandb_run.config.opt = vars(opt) - self.wandb_run.config.data_dict = wandb_data_dict - self.data_dict = self.setup_training(opt, data_dict) - if self.job_type == 'Dataset Creation': - self.data_dict = self.check_and_upload_dataset(opt) - else: - prefix = colorstr('wandb: ') - print(f"{prefix}Install Weights & Biases for YOLOv3 logging with 'pip install wandb' (recommended)") - - def check_and_upload_dataset(self, opt): - assert wandb, 'Install wandb to upload dataset' - check_dataset(self.data_dict) - config_path = self.log_dataset_artifact(check_file(opt.data), - opt.single_cls, - 'YOLOv3' if opt.project == 'runs/train' else Path(opt.project).stem) - print("Created dataset config file ", config_path) - with open(config_path) as f: - wandb_data_dict = yaml.safe_load(f) - return wandb_data_dict - - def setup_training(self, opt, data_dict): - self.log_dict, self.current_epoch, self.log_imgs = {}, 0, 16 # Logging Constants - self.bbox_interval = opt.bbox_interval - if isinstance(opt.resume, str): - modeldir, _ = self.download_model_artifact(opt) - if modeldir: - self.weights = Path(modeldir) / "last.pt" - config = self.wandb_run.config - opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp = str( - self.weights), config.save_period, config.total_batch_size, config.bbox_interval, config.epochs, \ - config.opt['hyp'] - data_dict = dict(self.wandb_run.config.data_dict) # eliminates the need for config file to resume - if 'val_artifact' not in self.__dict__: # If --upload_dataset is set, use the existing artifact, don't download - self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(data_dict.get('train'), - opt.artifact_alias) - self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(data_dict.get('val'), - opt.artifact_alias) - self.result_artifact, self.result_table, self.val_table, self.weights = None, None, None, None - if self.train_artifact_path is not None: - train_path = Path(self.train_artifact_path) / 'data/images/' - data_dict['train'] = str(train_path) - if self.val_artifact_path is not None: - val_path = Path(self.val_artifact_path) / 'data/images/' - data_dict['val'] = str(val_path) - self.val_table = self.val_artifact.get("val") - self.map_val_table_path() - if self.val_artifact is not None: - self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation") - self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"]) - if opt.bbox_interval == -1: - self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1 - return data_dict - - def download_dataset_artifact(self, path, alias): - if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX): - artifact_path = Path(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias) - dataset_artifact = wandb.use_artifact(artifact_path.as_posix()) - assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'" - datadir = dataset_artifact.download() - return datadir, dataset_artifact - return None, None - - def download_model_artifact(self, opt): - if opt.resume.startswith(WANDB_ARTIFACT_PREFIX): - model_artifact = wandb.use_artifact(remove_prefix(opt.resume, WANDB_ARTIFACT_PREFIX) + ":latest") - assert model_artifact is not None, 'Error: W&B model artifact doesn\'t exist' - modeldir = model_artifact.download() - epochs_trained = model_artifact.metadata.get('epochs_trained') - total_epochs = model_artifact.metadata.get('total_epochs') - is_finished = total_epochs is None - assert not is_finished, 'training is finished, can only resume incomplete runs.' - return modeldir, model_artifact - return None, None - - def log_model(self, path, opt, epoch, fitness_score, best_model=False): - model_artifact = wandb.Artifact('run_' + wandb.run.id + '_model', type='model', metadata={ - 'original_url': str(path), - 'epochs_trained': epoch + 1, - 'save period': opt.save_period, - 'project': opt.project, - 'total_epochs': opt.epochs, - 'fitness_score': fitness_score - }) - model_artifact.add_file(str(path / 'last.pt'), name='last.pt') - wandb.log_artifact(model_artifact, - aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else '']) - print("Saving model artifact on epoch ", epoch + 1) - - def log_dataset_artifact(self, data_file, single_cls, project, overwrite_config=False): - with open(data_file) as f: - data = yaml.safe_load(f) # data dict - nc, names = (1, ['item']) if single_cls else (int(data['nc']), data['names']) - names = {k: v for k, v in enumerate(names)} # to index dictionary - self.train_artifact = self.create_dataset_table(LoadImagesAndLabels( - data['train'], rect=True, batch_size=1), names, name='train') if data.get('train') else None - self.val_artifact = self.create_dataset_table(LoadImagesAndLabels( - data['val'], rect=True, batch_size=1), names, name='val') if data.get('val') else None - if data.get('train'): - data['train'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'train') - if data.get('val'): - data['val'] = WANDB_ARTIFACT_PREFIX + str(Path(project) / 'val') - path = data_file if overwrite_config else '_wandb.'.join(data_file.rsplit('.', 1)) # updated data.yaml path - data.pop('download', None) - with open(path, 'w') as f: - yaml.safe_dump(data, f) - - if self.job_type == 'Training': # builds correct artifact pipeline graph - self.wandb_run.use_artifact(self.val_artifact) - self.wandb_run.use_artifact(self.train_artifact) - self.val_artifact.wait() - self.val_table = self.val_artifact.get('val') - self.map_val_table_path() - else: - self.wandb_run.log_artifact(self.train_artifact) - self.wandb_run.log_artifact(self.val_artifact) - return path - - def map_val_table_path(self): - self.val_table_map = {} - print("Mapping dataset") - for i, data in enumerate(tqdm(self.val_table.data)): - self.val_table_map[data[3]] = data[0] - - def create_dataset_table(self, dataset, class_to_id, name='dataset'): - # TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging - artifact = wandb.Artifact(name=name, type="dataset") - img_files = tqdm([dataset.path]) if isinstance(dataset.path, str) and Path(dataset.path).is_dir() else None - img_files = tqdm(dataset.img_files) if not img_files else img_files - for img_file in img_files: - if Path(img_file).is_dir(): - artifact.add_dir(img_file, name='data/images') - labels_path = 'labels'.join(dataset.path.rsplit('images', 1)) - artifact.add_dir(labels_path, name='data/labels') - else: - artifact.add_file(img_file, name='data/images/' + Path(img_file).name) - label_file = Path(img2label_paths([img_file])[0]) - artifact.add_file(str(label_file), - name='data/labels/' + label_file.name) if label_file.exists() else None - table = wandb.Table(columns=["id", "train_image", "Classes", "name"]) - class_set = wandb.Classes([{'id': id, 'name': name} for id, name in class_to_id.items()]) - for si, (img, labels, paths, shapes) in enumerate(tqdm(dataset)): - box_data, img_classes = [], {} - for cls, *xywh in labels[:, 1:].tolist(): - cls = int(cls) - box_data.append({"position": {"middle": [xywh[0], xywh[1]], "width": xywh[2], "height": xywh[3]}, - "class_id": cls, - "box_caption": "%s" % (class_to_id[cls])}) - img_classes[cls] = class_to_id[cls] - boxes = {"ground_truth": {"box_data": box_data, "class_labels": class_to_id}} # inference-space - table.add_data(si, wandb.Image(paths, classes=class_set, boxes=boxes), json.dumps(img_classes), - Path(paths).name) - artifact.add(table, name) - return artifact - - def log_training_progress(self, predn, path, names): - if self.val_table and self.result_table: - class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()]) - box_data = [] - total_conf = 0 - for *xyxy, conf, cls in predn.tolist(): - if conf >= 0.25: - box_data.append( - {"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]}, - "class_id": int(cls), - "box_caption": "%s %.3f" % (names[cls], conf), - "scores": {"class_score": conf}, - "domain": "pixel"}) - total_conf = total_conf + conf - boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space - id = self.val_table_map[Path(path).name] - self.result_table.add_data(self.current_epoch, - id, - wandb.Image(self.val_table.data[id][1], boxes=boxes, classes=class_set), - total_conf / max(1, len(box_data)) - ) - - def log(self, log_dict): - if self.wandb_run: - for key, value in log_dict.items(): - self.log_dict[key] = value - - def end_epoch(self, best_result=False): - if self.wandb_run: - wandb.log(self.log_dict) - self.log_dict = {} - if self.result_artifact: - train_results = wandb.JoinedTable(self.val_table, self.result_table, "id") - self.result_artifact.add(train_results, 'result') - wandb.log_artifact(self.result_artifact, aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), - ('best' if best_result else '')]) - self.result_table = wandb.Table(["epoch", "id", "prediction", "avg_confidence"]) - self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation") - - def finish_run(self): - if self.wandb_run: - if self.log_dict: - wandb.log(self.log_dict) - wandb.run.finish() diff --git a/val.py b/val.py new file mode 100644 index 0000000000..24b28ad186 --- /dev/null +++ b/val.py @@ -0,0 +1,367 @@ +# YOLOv3 πŸš€ by Ultralytics, GPL-3.0 license +""" +Validate a trained model accuracy on a custom dataset + +Usage: + $ python path/to/val.py --data coco128.yaml --weights yolov3.pt --img 640 +""" + +import argparse +import json +import os +import sys +from pathlib import Path +from threading import Thread + +import numpy as np +import torch +from tqdm import tqdm + +FILE = Path(__file__).resolve() +ROOT = FILE.parents[0] # root directory +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) # add ROOT to PATH +ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative + +from models.common import DetectMultiBackend +from utils.callbacks import Callbacks +from utils.datasets import create_dataloader +from utils.general import (LOGGER, NCOLS, box_iou, check_dataset, check_img_size, check_requirements, check_yaml, + coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args, + scale_coords, xywh2xyxy, xyxy2xywh) +from utils.metrics import ConfusionMatrix, ap_per_class +from utils.plots import output_to_target, plot_images, plot_val_study +from utils.torch_utils import select_device, time_sync + + +def save_one_txt(predn, save_conf, shape, file): + # Save one txt result + gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh + for *xyxy, conf, cls in predn.tolist(): + xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh + line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format + with open(file, 'a') as f: + f.write(('%g ' * len(line)).rstrip() % line + '\n') + + +def save_one_json(predn, jdict, path, class_map): + # Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236} + image_id = int(path.stem) if path.stem.isnumeric() else path.stem + box = xyxy2xywh(predn[:, :4]) # xywh + box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner + for p, b in zip(predn.tolist(), box.tolist()): + jdict.append({'image_id': image_id, + 'category_id': class_map[int(p[5])], + 'bbox': [round(x, 3) for x in b], + 'score': round(p[4], 5)}) + + +def process_batch(detections, labels, iouv): + """ + Return correct predictions matrix. Both sets of boxes are in (x1, y1, x2, y2) format. + Arguments: + detections (Array[N, 6]), x1, y1, x2, y2, conf, class + labels (Array[M, 5]), class, x1, y1, x2, y2 + Returns: + correct (Array[N, 10]), for 10 IoU levels + """ + correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device) + iou = box_iou(labels[:, 1:], detections[:, :4]) + x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5])) # IoU above threshold and classes match + if x[0].shape[0]: + matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detection, iou] + if x[0].shape[0] > 1: + matches = matches[matches[:, 2].argsort()[::-1]] + matches = matches[np.unique(matches[:, 1], return_index=True)[1]] + # matches = matches[matches[:, 2].argsort()[::-1]] + matches = matches[np.unique(matches[:, 0], return_index=True)[1]] + matches = torch.Tensor(matches).to(iouv.device) + correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv + return correct + + +@torch.no_grad() +def run(data, + weights=None, # model.pt path(s) + batch_size=32, # batch size + imgsz=640, # inference size (pixels) + conf_thres=0.001, # confidence threshold + iou_thres=0.6, # NMS IoU threshold + task='val', # train, val, test, speed or study + device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu + single_cls=False, # treat as single-class dataset + augment=False, # augmented inference + verbose=False, # verbose output + save_txt=False, # save results to *.txt + save_hybrid=False, # save label+prediction hybrid results to *.txt + save_conf=False, # save confidences in --save-txt labels + save_json=False, # save a COCO-JSON results file + project=ROOT / 'runs/val', # save to project/name + name='exp', # save to project/name + exist_ok=False, # existing project/name ok, do not increment + half=True, # use FP16 half-precision inference + dnn=False, # use OpenCV DNN for ONNX inference + model=None, + dataloader=None, + save_dir=Path(''), + plots=True, + callbacks=Callbacks(), + compute_loss=None, + ): + # Initialize/load model and set device + training = model is not None + if training: # called by train.py + device, pt = next(model.parameters()).device, True # get model device, PyTorch model + + half &= device.type != 'cpu' # half precision only supported on CUDA + model.half() if half else model.float() + else: # called directly + device = select_device(device, batch_size=batch_size) + + # Directories + save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run + (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir + + # Load model + model = DetectMultiBackend(weights, device=device, dnn=dnn) + stride, pt = model.stride, model.pt + imgsz = check_img_size(imgsz, s=stride) # check image size + half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA + if pt: + model.model.half() if half else model.model.float() + else: + half = False + batch_size = 1 # export.py models default to batch-size 1 + device = torch.device('cpu') + LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends') + + # Data + data = check_dataset(data) # check + + # Configure + model.eval() + is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt') # COCO dataset + nc = 1 if single_cls else int(data['nc']) # number of classes + iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95 + niou = iouv.numel() + + # Dataloader + if not training: + if pt and device.type != 'cpu': + model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters()))) # warmup + pad = 0.0 if task == 'speed' else 0.5 + task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images + dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt, + prefix=colorstr(f'{task}: '))[0] + + seen = 0 + confusion_matrix = ConfusionMatrix(nc=nc) + names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)} + class_map = coco80_to_coco91_class() if is_coco else list(range(1000)) + s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95') + dt, p, r, f1, mp, mr, map50, map = [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 + loss = torch.zeros(3, device=device) + jdict, stats, ap, ap_class = [], [], [], [] + pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar + for batch_i, (im, targets, paths, shapes) in enumerate(pbar): + t1 = time_sync() + if pt: + im = im.to(device, non_blocking=True) + targets = targets.to(device) + im = im.half() if half else im.float() # uint8 to fp16/32 + im /= 255 # 0 - 255 to 0.0 - 1.0 + nb, _, height, width = im.shape # batch size, channels, height, width + t2 = time_sync() + dt[0] += t2 - t1 + + # Inference + out, train_out = model(im) if training else model(im, augment=augment, val=True) # inference, loss outputs + dt[1] += time_sync() - t2 + + # Loss + if compute_loss: + loss += compute_loss([x.float() for x in train_out], targets)[1] # box, obj, cls + + # NMS + targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels + lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling + t3 = time_sync() + out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls) + dt[2] += time_sync() - t3 + + # Metrics + for si, pred in enumerate(out): + labels = targets[targets[:, 0] == si, 1:] + nl = len(labels) + tcls = labels[:, 0].tolist() if nl else [] # target class + path, shape = Path(paths[si]), shapes[si][0] + seen += 1 + + if len(pred) == 0: + if nl: + stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls)) + continue + + # Predictions + if single_cls: + pred[:, 5] = 0 + predn = pred.clone() + scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred + + # Evaluate + if nl: + tbox = xywh2xyxy(labels[:, 1:5]) # target boxes + scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels + labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels + correct = process_batch(predn, labelsn, iouv) + if plots: + confusion_matrix.process_batch(predn, labelsn) + else: + correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool) + stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) # (correct, conf, pcls, tcls) + + # Save/log + if save_txt: + save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt')) + if save_json: + save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary + callbacks.run('on_val_image_end', pred, predn, path, names, im[si]) + + # Plot images + if plots and batch_i < 3: + f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels + Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start() + f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions + Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start() + + # Compute metrics + stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy + if len(stats) and stats[0].any(): + p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names) + ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95 + mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean() + nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class + else: + nt = torch.zeros(1) + + # Print results + pf = '%20s' + '%11i' * 2 + '%11.3g' * 4 # print format + LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map)) + + # Print results per class + if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): + for i, c in enumerate(ap_class): + LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i])) + + # Print speeds + t = tuple(x / seen * 1E3 for x in dt) # speeds per image + if not training: + shape = (batch_size, 3, imgsz, imgsz) + LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t) + + # Plots + if plots: + confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) + callbacks.run('on_val_end') + + # Save JSON + if save_json and len(jdict): + w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights + anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json + pred_json = str(save_dir / f"{w}_predictions.json") # predictions json + LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') + with open(pred_json, 'w') as f: + json.dump(jdict, f) + + try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb + check_requirements(['pycocotools']) + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + anno = COCO(anno_json) # init annotations api + pred = anno.loadRes(pred_json) # init predictions api + eval = COCOeval(anno, pred, 'bbox') + if is_coco: + eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files] # image IDs to evaluate + eval.evaluate() + eval.accumulate() + eval.summarize() + map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5) + except Exception as e: + LOGGER.info(f'pycocotools unable to run: {e}') + + # Return results + model.float() # for training + if not training: + s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' + LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") + maps = np.zeros(nc) + map + for i, c in enumerate(ap_class): + maps[c] = ap[i] + return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t + + +def parse_opt(): + parser = argparse.ArgumentParser() + parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') + parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov3.pt', help='model.pt path(s)') + parser.add_argument('--batch-size', type=int, default=32, help='batch size') + parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)') + parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold') + parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold') + parser.add_argument('--task', default='val', help='train, val, test, speed or study') + parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') + parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset') + parser.add_argument('--augment', action='store_true', help='augmented inference') + parser.add_argument('--verbose', action='store_true', help='report mAP by class') + parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') + parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt') + parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') + parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file') + parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name') + parser.add_argument('--name', default='exp', help='save to project/name') + parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') + parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') + parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') + opt = parser.parse_args() + opt.data = check_yaml(opt.data) # check YAML + opt.save_json |= opt.data.endswith('coco.yaml') + opt.save_txt |= opt.save_hybrid + print_args(FILE.stem, opt) + return opt + + +def main(opt): + check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) + + if opt.task in ('train', 'val', 'test'): # run normally + if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466 + LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.') + run(**vars(opt)) + + else: + weights = opt.weights if isinstance(opt.weights, list) else [opt.weights] + opt.half = True # FP16 for fastest results + if opt.task == 'speed': # speed benchmarks + # python val.py --task speed --data coco.yaml --batch 1 --weights yolov3.pt yolov3-spp.pt... + opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False + for opt.weights in weights: + run(**vars(opt), plots=False) + + elif opt.task == 'study': # speed vs mAP benchmarks + # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov3.pt yolov3-spp.pt... + for opt.weights in weights: + f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to + x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis + for opt.imgsz in x: # img-size + LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...') + r, _, t = run(**vars(opt), plots=False) + y.append(r + t) # results and times + np.savetxt(f, y, fmt='%10.4g') # save + os.system('zip -r study.zip study_*.txt') + plot_val_study(x=x) # plot + + +if __name__ == "__main__": + opt = parse_opt() + main(opt) diff --git a/weights/download_weights.sh b/weights/download_weights.sh deleted file mode 100755 index 6bb58023c9..0000000000 --- a/weights/download_weights.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Download latest models from https://github.com/ultralytics/yolov3/releases -# Usage: -# $ bash weights/download_weights.sh - -python - <