julien.blanchon commited on
Commit
c8c12e9
·
1 Parent(s): 7caa065
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .devcontainer/devcontainer.json +56 -0
  2. .dockerignore +13 -0
  3. .gitattributes +1 -0
  4. .github/ISSUE_TEMPLATE/bug_report.md +35 -0
  5. .github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. .github/pull_request_template.md +22 -0
  7. .github/workflows/docs.yml +66 -0
  8. .github/workflows/nightly.yml +29 -0
  9. .github/workflows/pre_merge.yml +32 -0
  10. .gitignore +173 -0
  11. .pre-commit-config.yaml +70 -0
  12. CHANGELOG.md +152 -0
  13. CITATION.cff +92 -0
  14. CODE_OF_CONDUCT.md +128 -0
  15. CONTRIBUTING.md +64 -0
  16. Dockerfile +38 -0
  17. LICENSE +202 -0
  18. MANIFEST.in +1 -0
  19. README copy.md +253 -0
  20. anomalib/__init__.py +17 -0
  21. anomalib/config/__init__.py +23 -0
  22. anomalib/config/config.py +170 -0
  23. anomalib/data/__init__.py +104 -0
  24. anomalib/data/btech.py +453 -0
  25. anomalib/data/folder.py +540 -0
  26. anomalib/data/inference.py +67 -0
  27. anomalib/data/mvtec.py +457 -0
  28. anomalib/data/utils/__init__.py +20 -0
  29. anomalib/data/utils/download.py +195 -0
  30. anomalib/data/utils/image.py +91 -0
  31. anomalib/data/utils/split.py +94 -0
  32. anomalib/deploy/__init__.py +20 -0
  33. anomalib/deploy/inferencers/__init__.py +21 -0
  34. anomalib/deploy/inferencers/base.py +204 -0
  35. anomalib/deploy/inferencers/openvino.py +149 -0
  36. anomalib/deploy/inferencers/torch.py +164 -0
  37. anomalib/deploy/optimize.py +89 -0
  38. anomalib/models/__init__.py +75 -0
  39. anomalib/models/cflow/README.md +49 -0
  40. anomalib/models/cflow/__init__.py +19 -0
  41. anomalib/models/cflow/anomaly_map.py +97 -0
  42. anomalib/models/cflow/config.yaml +101 -0
  43. anomalib/models/cflow/lightning_model.py +161 -0
  44. anomalib/models/cflow/torch_model.py +130 -0
  45. anomalib/models/cflow/utils.py +125 -0
  46. anomalib/models/components/__init__.py +32 -0
  47. anomalib/models/components/base/__init__.py +20 -0
  48. anomalib/models/components/base/anomaly_module.py +181 -0
  49. anomalib/models/components/base/dynamic_module.py +63 -0
  50. anomalib/models/components/dimensionality_reduction/__init__.py +20 -0
.devcontainer/devcontainer.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2
+ // https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/docker-existing-dockerfile
3
+ {
4
+ "name": "Anomalib Development Environment",
5
+ // Sets the run context to one level up instead of the .devcontainer folder.
6
+ "context": "..",
7
+ // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
8
+ "dockerFile": "../Dockerfile",
9
+ // Set *default* container specific settings.json values on container create.
10
+ "settings": {
11
+ "python.pythonPath": "/opt/conda/bin/python",
12
+ "python.formatting.provider": "black",
13
+ "editor.formatOnSave": true,
14
+ "python.formatting.blackArgs": [
15
+ "--line-length",
16
+ "120"
17
+ ],
18
+ "editor.codeActionsOnSave": {
19
+ "source.organizeImports": true
20
+ },
21
+ "python.linting.enabled": true,
22
+ "python.linting.flake8Enabled": true,
23
+ "python.linting.flake8Args": [
24
+ "--max-line-length=120"
25
+ ],
26
+ "python.testing.cwd": ".",
27
+ "python.linting.lintOnSave": true,
28
+ "python.testing.unittestEnabled": false,
29
+ "python.testing.nosetestsEnabled": false,
30
+ "python.testing.pytestEnabled": true,
31
+ "python.testing.pytestArgs": [
32
+ "tests"
33
+ ]
34
+ },
35
+ // Add the IDs of extensions you want installed when the container is created.
36
+ "extensions": [
37
+ "ms-python.python",
38
+ "njpwerner.autodocstring",
39
+ "streetsidesoftware.code-spell-checker",
40
+ "eamodio.gitlens",
41
+ "littlefoxteam.vscode-python-test-adapter"
42
+ ],
43
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
44
+ // "forwardPorts": [],
45
+ // Uncomment the next line to run commands after the container is created - for example installing curl.
46
+ // "postCreateCommand": "apt-get update && apt-get install -y curl",
47
+ // Uncomment when using a ptrace-based debugger like C++, Go, and Rust
48
+ "runArgs": [
49
+ "--gpus=all",
50
+ "--shm-size=20g"
51
+ ]
52
+ // Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
53
+ // "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
54
+ // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
55
+ // "remoteUser": "vscode"
56
+ }
.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.dot
2
+ .idea
3
+ .pytest_cache
4
+ .tox
5
+ .vscode
6
+ __pycache__
7
+ {toxworkdir}
8
+ datasets/
9
+ embeddings/
10
+ lightning_logs/
11
+ lightning_test/
12
+ papers/
13
+ results/
.gitattributes CHANGED
@@ -25,3 +25,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
  *.zstandard filter=lfs diff=lfs merge=lfs -text
27
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
  *.zstandard filter=lfs diff=lfs merge=lfs -text
27
  *tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
.github/ISSUE_TEMPLATE/bug_report.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ - A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ - A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ - If applicable, add screenshots to help explain your problem.
25
+
26
+ **Hardware and Software Configuration**
27
+ - OS: [Ubuntu, OD]
28
+ - NVIDIA Driver Version [470.57.02]
29
+ - CUDA Version [e.g. 11.4]
30
+ - CUDNN Version [e.g. v11.4.120]
31
+ - OpenVINO Version [Optional e.g. v2021.4.2]
32
+
33
+
34
+ **Additional context**
35
+ - Add any other context about the problem here.
.github/ISSUE_TEMPLATE/feature_request.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ - A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ - A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ - A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ - Add any other context or screenshots about the feature request here.
.github/pull_request_template.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Description
2
+
3
+ - Provide a summary of the modification as well as the issue that has been resolved. List any dependencies that this modification necessitates.
4
+
5
+ - Fixes # (issue)
6
+
7
+ ## Changes
8
+
9
+ - [ ] Bug fix (non-breaking change which fixes an issue)
10
+ - [ ] New feature (non-breaking change which adds functionality)
11
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12
+ - [ ] This change requires a documentation update
13
+
14
+ ## Checklist
15
+
16
+ - [ ] My code follows the [pre-commit style and check guidelines](https://openvinotoolkit.github.io/anomalib/guides/using_pre_commit.html#pre-commit-hooks) of this project.
17
+ - [ ] I have performed a self-review of my code
18
+ - [ ] I have commented my code, particularly in hard-to-understand areas
19
+ - [ ] I have made corresponding changes to the documentation
20
+ - [ ] My changes generate no new warnings
21
+ - [ ] I have added tests that prove my fix is effective or that my feature works
22
+ - [ ] New and existing tests pass locally with my changes
.github/workflows/docs.yml ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [development]
6
+ paths-ignore:
7
+ - ".github/**" # Ignore changes towards the .github directory
8
+ workflow_dispatch: # run on request (no need for PR)
9
+
10
+ jobs:
11
+ Build-and-Publish-Documentation:
12
+ runs-on: [docs]
13
+ steps:
14
+ - name: CHECKOUT REPOSITORY
15
+ uses: actions/checkout@v2
16
+ - name: Install requirements
17
+ run: |
18
+ pip install -r requirements/base.txt
19
+ pip install -r requirements/dev.txt
20
+ pip install -r requirements/openvino.txt
21
+ - name: Build and Commit Docs
22
+ run: |
23
+ pip install -r requirements/docs.txt
24
+ cd docs
25
+ make html
26
+ - name: Create gh-pages branch
27
+ run: |
28
+ echo ::set-output name=SOURCE_NAME::${GITHUB_REF#refs/*/}
29
+ echo ::set-output name=SOURCE_BRANCH::${GITHUB_REF#refs/heads/}
30
+ echo ::set-output name=SOURCE_TAG::${GITHUB_REF#refs/tags/}
31
+
32
+ existed_in_remote=$(git ls-remote --heads origin gh-pages)
33
+
34
+ if [[ -z ${existed_in_remote} ]]; then
35
+ echo "Creating gh-pages branch"
36
+ git config --local user.email "[email protected]"
37
+ git config --local user.name "GitHub Action"
38
+ git checkout --orphan gh-pages
39
+ git reset --hard
40
+ touch .nojekyll
41
+ git add .nojekyll
42
+ git commit -m "Initializing gh-pages branch"
43
+ git push origin gh-pages
44
+ git checkout ${{steps.branch_name.outputs.SOURCE_NAME}}
45
+ echo "Created gh-pages branch"
46
+ else
47
+ echo "Branch gh-pages already exists"
48
+ fi
49
+ - name: Commit docs to gh-pages branch
50
+ run: |
51
+ git fetch
52
+ git checkout gh-pages
53
+ mkdir -p /tmp/docs_build
54
+ cp -r docs/build/html/* /tmp/docs_build/
55
+ rm -rf ./*
56
+ cp -r /tmp/docs_build/* ./
57
+ rm -rf /tmp/docs_build
58
+ git config --local user.email "[email protected]"
59
+ git config --local user.name "GitHub Action"
60
+ git add .
61
+ git commit -m "Update documentation" -a || true
62
+ - name: Push changes
63
+ uses: ad-m/github-push-action@master
64
+ with:
65
+ github_token: ${{ secrets.GITHUB_TOKEN }}
66
+ branch: gh-pages
.github/workflows/nightly.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Nightly-regression Test
2
+
3
+ on:
4
+ workflow_dispatch: # run on request (no need for PR)
5
+ schedule:
6
+ - cron: "0 0 * * *"
7
+
8
+ jobs:
9
+ Tox:
10
+ runs-on: [self-hosted, linux, x64]
11
+ strategy:
12
+ max-parallel: 1
13
+ if: github.ref == 'refs/heads/development'
14
+ steps:
15
+ - name: Print GPU status
16
+ run: nvidia-smi
17
+ - name: CHECKOUT REPOSITORY
18
+ uses: actions/checkout@v2
19
+ - name: Install Tox
20
+ run: pip install tox
21
+ - name: Coverage
22
+ run: |
23
+ export ANOMALIB_DATASET_PATH=/media/data1/datasets/
24
+ tox -e nightly
25
+ - name: Upload coverage result
26
+ uses: actions/upload-artifact@v2
27
+ with:
28
+ name: coverage
29
+ path: .tox/coverage.xml
.github/workflows/pre_merge.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Pre-merge Checks
2
+
3
+ on:
4
+ push:
5
+ branches: [development, master]
6
+ pull_request:
7
+ workflow_dispatch: # run on request (no need for PR)
8
+
9
+ jobs:
10
+ Tox:
11
+ runs-on: [self-hosted, linux, x64]
12
+ strategy:
13
+ max-parallel: 1
14
+ steps:
15
+ - name: Print GPU status
16
+ run: nvidia-smi
17
+ - name: CHECKOUT REPOSITORY
18
+ uses: actions/checkout@v2
19
+ - name: Install Tox
20
+ run: pip install tox
21
+ - name: Code quality checks
22
+ run: tox -e black,isort,flake8,pylint,mypy,pydocstyle
23
+ - name: Coverage
24
+ run: |
25
+ export ANOMALIB_DATASET_PATH=/media/data1/datasets/
26
+ export CUDA_VISIBLE_DEVICES=3
27
+ tox -e pre_merge
28
+ - name: Upload coverage result
29
+ uses: actions/upload-artifact@v2
30
+ with:
31
+ name: coverage
32
+ path: .tox/coverage.xml
.gitignore ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project related
2
+ datasets
3
+ !anomalib/datasets
4
+ !tests/pre_merge/datasets
5
+ results
6
+ !anomalib/core/results
7
+
8
+ backup.py
9
+ train.ipynb
10
+
11
+ # VENV
12
+ .python-version
13
+ .anomalib
14
+ .toxbase
15
+
16
+ # IDE
17
+ .vscode
18
+ .idea
19
+
20
+ # Byte-compiled / optimized / DLL files
21
+ __pycache__/
22
+ *.py[cod]
23
+ *$py.class
24
+
25
+ # C extensions
26
+ *.so
27
+
28
+ # Distribution / packaging
29
+ .Python
30
+ build/
31
+ develop-eggs/
32
+ dist/
33
+ downloads/
34
+ eggs/
35
+ .eggs/
36
+ lib/
37
+ lib64/
38
+ parts/
39
+ sdist/
40
+ var/
41
+ wheels/
42
+ share/python-wheels/
43
+ *.egg-info/
44
+ .installed.cfg
45
+ *.egg
46
+ MANIFEST
47
+
48
+ # PyInstaller
49
+ # Usually these files are written by a python script from a template
50
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
51
+ *.manifest
52
+ *.spec
53
+
54
+ # Installer logs
55
+ pip-log.txt
56
+ pip-delete-this-directory.txt
57
+
58
+ # Unit test / coverage reports
59
+ htmlcov/
60
+ .tox/
61
+ .nox/
62
+ .coverage
63
+ .coverage.*
64
+ .cache
65
+ nosetests.xml
66
+ coverage.xml
67
+ *.cover
68
+ *.py,cover
69
+ .hypothesis/
70
+ .pytest_cache/
71
+ cover/
72
+
73
+ # Translations
74
+ *.mo
75
+ *.pot
76
+
77
+ # Django stuff:
78
+ *.log
79
+ local_settings.py
80
+ db.sqlite3
81
+ db.sqlite3-journal
82
+
83
+ # Flask stuff:
84
+ instance/
85
+ .webassets-cache
86
+
87
+ # Scrapy stuff:
88
+ .scrapy
89
+
90
+ # Sphinx documentation
91
+ docs/build/
92
+ docs/source/_build/
93
+
94
+ # PyBuilder
95
+ .pybuilder/
96
+ target/
97
+
98
+ # Jupyter Notebook
99
+ .ipynb_checkpoints
100
+
101
+ # IPython
102
+ profile_default/
103
+ ipython_config.py
104
+
105
+ # pyenv
106
+ # For a library or package, you might want to ignore these files since the code is
107
+ # intended to run in multiple environments; otherwise, check them in:
108
+ # .python-version
109
+
110
+ # pipenv
111
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
112
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
113
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
114
+ # install all needed dependencies.
115
+ #Pipfile.lock
116
+
117
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
118
+ __pypackages__/
119
+
120
+ # Celery stuff
121
+ celerybeat-schedule
122
+ celerybeat.pid
123
+
124
+ # SageMath parsed files
125
+ *.sage.py
126
+
127
+ # Environments
128
+ .env
129
+ .venv
130
+ env/
131
+ venv/
132
+ ENV/
133
+ env.bak/
134
+ venv.bak/
135
+
136
+ # Spyder project settings
137
+ .spyderproject
138
+ .spyproject
139
+
140
+ # Rope project settings
141
+ .ropeproject
142
+
143
+ # mkdocs documentation
144
+ /site
145
+
146
+ # mypy
147
+ .mypy_cache/
148
+ .dmypy.json
149
+ dmypy.json
150
+
151
+ # Pyre type checker
152
+ .pyre/
153
+
154
+ # pytype static type analyzer
155
+ .pytype/
156
+
157
+ # Cython debug symbols
158
+ cython_debug/
159
+
160
+
161
+ # Documentations
162
+ docs/source/generated
163
+ docs/source/api
164
+ docs/source/models
165
+ docs/build/
166
+ docs/source/_build/
167
+
168
+ # Misc
169
+ .DS_Store
170
+
171
+ # logs
172
+ wandb/
173
+ lightning_logs/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v3.4.0
4
+ hooks:
5
+ # list of supported hooks: https://pre-commit.com/hooks.html
6
+ - id: trailing-whitespace
7
+ - id: end-of-file-fixer
8
+ - id: check-yaml
9
+ - id: check-added-large-files
10
+ - id: debug-statements
11
+ - id: detect-private-key
12
+
13
+ # python code formatting
14
+ - repo: https://github.com/psf/black
15
+ rev: 22.3.0
16
+ hooks:
17
+ - id: black
18
+ args: [--line-length, "120"]
19
+
20
+ # python import sorting
21
+ - repo: https://github.com/PyCQA/isort
22
+ rev: 5.8.0
23
+ hooks:
24
+ - id: isort
25
+ args: ["--profile", "black"]
26
+
27
+ # yaml formatting
28
+ - repo: https://github.com/pre-commit/mirrors-prettier
29
+ rev: v2.3.0
30
+ hooks:
31
+ - id: prettier
32
+ types: [yaml]
33
+
34
+ # python code analysis
35
+ - repo: https://github.com/PyCQA/flake8
36
+ rev: 3.9.2
37
+ hooks:
38
+ - id: flake8
39
+ args: [--config=tox.ini]
40
+ exclude: "tests|anomalib/models/components/freia"
41
+
42
+ # python linting
43
+ - repo: local
44
+ hooks:
45
+ - id: pylint
46
+ name: pylint
47
+ entry: pylint --score=no --rcfile=tox.ini
48
+ language: system
49
+ types: [python]
50
+ exclude: "tests|docs|anomalib/models/components/freia"
51
+
52
+ # python static type checking
53
+ - repo: https://github.com/pre-commit/mirrors-mypy
54
+ rev: "v0.910"
55
+ hooks:
56
+ - id: mypy
57
+ args: [--config-file=tox.ini]
58
+ additional_dependencies: [types-PyYAML]
59
+ exclude: "tests|anomalib/models/components/freia"
60
+
61
+ - repo: https://github.com/PyCQA/pydocstyle
62
+ rev: 6.1.1
63
+ hooks:
64
+ - id: pydocstyle
65
+ name: pydocstyle
66
+ entry: pydocstyle
67
+ language: python
68
+ types: [python]
69
+ args: [--config=tox.ini]
70
+ exclude: "tests|docs|anomalib/models/components/freia"
CHANGELOG.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## v.0.3.0
4
+ ### What's Changed
5
+ * 🛠 ⚠️ Fix configs to properly use pytorch-lightning==1.6 with GPU by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/234
6
+ * 🛠 Fix `get_version` in `setup.py` to avoid hard-coding version. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/229
7
+ * 🐞 Fix image loggers by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/233
8
+ * Configurable metrics by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/230
9
+ * Make OpenVINO throughput optional in benchmarking by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/239
10
+ * 🔨 Minor fix: Ensure docs build runs only on isea-server by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/245
11
+ * 🏷 Rename `--model_config_path` to `config` by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/246
12
+ * Revert "🏷 Rename `--model_config_path` to `config`" by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/247
13
+ * ➕ Add `--model_config_path` deprecation warning to `inference.py` by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/248
14
+ * Add console logger by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/241
15
+ * Add segmentation mask to inference output by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/242
16
+ * 🛠 Fix broken mvtec link, and split url to fit to 120 by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/264
17
+ * 🛠 Fix mask filenames in folder dataset by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/249
18
+
19
+
20
+ **Full Changelog**: https://github.com/openvinotoolkit/anomalib/compare/v0.2.6...v0.3.0
21
+ ## v.0.2.6
22
+ ### What's Changed
23
+ * ✏️ Add `torchtext==0.9.1` to support Kaggle environments. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/165
24
+ * 🛠 Fix `KeyError:'label'` in classification folder dataset by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/175
25
+ * 📝 Added MVTec license to the repo by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/177
26
+ * load best model from checkpoint by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/195
27
+ * Replace `SaveToCSVCallback` with PL `CSVLogger` by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/198
28
+ * WIP Refactor test by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/197
29
+ * 🔧 Dockerfile enhancements by @LukasBommes in https://github.com/openvinotoolkit/anomalib/pull/172
30
+ * 🛠 Fix visualization issue for fully defected images by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/194
31
+ * ✨ Add hpo search using `wandb` by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/82
32
+ * Separate train and validation transformations by @alexriedel1 in https://github.com/openvinotoolkit/anomalib/pull/168
33
+ * 🛠 Fix docs workflow by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/200
34
+ * 🔄 CFlow: Switch soft permutation to false by default to speed up training. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/201
35
+ * Return only `image`, `path` and `label` for classification tasks in `Mvtec` and `Btech` datasets. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/196
36
+ * 🗑 Remove `freia` as dependency and include it in `anomalib/models/components` by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/174
37
+ * Visualizer show classification and segmentation by @alexriedel1 in https://github.com/openvinotoolkit/anomalib/pull/178
38
+ * ↗️ Bump up `pytorch-lightning` version to `1.6.0` or higher by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/193
39
+ * 🛠 Refactor DFKDE model by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/207
40
+ * 🛠 Minor fixes: Update callbacks to AnomalyModule by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/208
41
+ * 🛠 Minor update: Update pre-commit docs by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/206
42
+ * ✨ Directory streaming by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/210
43
+ * ✏️ Updated documentation for development on Docker by @LukasBommes in https://github.com/openvinotoolkit/anomalib/pull/217
44
+ * 🏷 Fix Mac M1 dependency conflicts by @dreaquil in https://github.com/openvinotoolkit/anomalib/pull/158
45
+ * 🐞 Set tiling off in pathcore to correctly reproduce the stats. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/222
46
+ * 🐞fix support for non-square images by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/204
47
+ * Allow specifying feature layer and pool factor in DFM by @nahuja-intel in https://github.com/openvinotoolkit/anomalib/pull/215
48
+ * 📝 Add GANomaly metrics to readme by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/224
49
+ * ↗️ Bump the version to 0.2.6 by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/223
50
+ * ���� 🛠 Fix inconsistent benchmarking throughput/time by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/221
51
+ * assign test split for folder dataset by @alexriedel1 in https://github.com/openvinotoolkit/anomalib/pull/220
52
+ * 🛠 Refactor model implementations by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/225
53
+
54
+ ## New Contributors
55
+ * @LukasBommes made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/172
56
+ * @dreaquil made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/158
57
+ * @nahuja-intel made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/215
58
+
59
+ **Full Changelog**: https://github.com/openvinotoolkit/anomalib/compare/v.0.2.5...v0.2.6
60
+ ## v.0.2.5
61
+ ### What's Changed
62
+ * Bugfix: fix random val/test split issue by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/48
63
+ * Fix Readmes by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/46
64
+ * Updated changelog by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/49
65
+ * add distinction between image and pixel threshold in postprocessor by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/50
66
+ * Fix docstrings by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/22
67
+ * Fix networkx requirement by @LeonidBeynenson in https://github.com/openvinotoolkit/anomalib/pull/52
68
+ * Add min-max normalization by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/53
69
+ * Change hardcoded dataset path to environ variable by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/51
70
+ * Added cflow algorithm by @blakshma in https://github.com/openvinotoolkit/anomalib/pull/47
71
+ * perform metric computation on cpu by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/64
72
+ * Fix Inferencer by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/60
73
+ * Updated readme for cflow and change default config to reflect results by @blakshma in https://github.com/openvinotoolkit/anomalib/pull/68
74
+ * Fixed issue with model loading by @blakshma in https://github.com/openvinotoolkit/anomalib/pull/69
75
+ * Docs/sa/fix readme by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/71
76
+ * Updated coreset subsampling method to improve accuracy by @blakshma in https://github.com/openvinotoolkit/anomalib/pull/73
77
+ * Revert "Updated coreset subsampling method to improve accuracy" by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/79
78
+ * Replace `SupportIndex` with `int` by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/76
79
+ * Added reference to official CFLOW repo by @blakshma in https://github.com/openvinotoolkit/anomalib/pull/81
80
+ * Fixed issue with k_greedy method by @blakshma in https://github.com/openvinotoolkit/anomalib/pull/80
81
+ * Fix Mix Data type issue on inferencer by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/77
82
+ * Create CODE_OF_CONDUCT.md by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/86
83
+ * ✨ Add GANomaly by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/70
84
+ * Reorder auc only when needed by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/91
85
+ * Bump up the pytorch lightning to master branch due to vulnurability issues by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/55
86
+ * 🚀 CI: Nightly Build by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/66
87
+ * Refactor by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/87
88
+ * Benchmarking Script by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/17
89
+ * 🐞 Fix tensor detach and gpu count issues in benchmarking script by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/100
90
+ * Return predicted masks in predict step by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/103
91
+ * Add Citation to the Readme by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/106
92
+ * Nightly build by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/104
93
+ * c_idx cast to LongTensor in random sparse projection by @alexriedel1 in https://github.com/openvinotoolkit/anomalib/pull/113
94
+ * Update Nightly by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/126
95
+ * Updated logos by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/131
96
+ * Add third-party-programs.txt file and update license by @LeonidBeynenson in https://github.com/openvinotoolkit/anomalib/pull/132
97
+ * 🔨 Increase inference + openvino support by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/122
98
+ * Fix/da/image size bug by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/135
99
+ * Fix/da/image size bug by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/140
100
+ * optimize compute_anomaly_score by using torch native funcrtions by @alexriedel1 in https://github.com/openvinotoolkit/anomalib/pull/141
101
+ * Fix IndexError in adaptive threshold computation by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/146
102
+ * Feature/data/btad by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/120
103
+ * Update for nncf_task by @AlexanderDokuchaev in https://github.com/openvinotoolkit/anomalib/pull/145
104
+ * fix non-adaptive thresholding bug by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/152
105
+ * Calculate feature map shape patchcore by @alexriedel1 in https://github.com/openvinotoolkit/anomalib/pull/148
106
+ * Add `transform_config` to the main `config.yaml` file. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/156
107
+ * Add Custom Dataset Training Support by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/154
108
+ * Added extension as an option when saving the result images. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/162
109
+ * Update `anomalib` version and requirements by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/163
110
+
111
+ ## New Contributors
112
+ * @LeonidBeynenson made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/52
113
+ * @blakshma made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/47
114
+ * @alexriedel1 made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/113
115
+ * @AlexanderDokuchaev made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/145
116
+
117
+ **Full Changelog**: https://github.com/openvinotoolkit/anomalib/compare/v.0.2.4...v.0.2.5
118
+ ## v.0.2.4
119
+ ### What's Changed
120
+ * Bump up the version to 0.2.4 by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/45
121
+ * fix heatmap color scheme by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/44
122
+
123
+ **Full Changelog**: https://github.com/openvinotoolkit/anomalib/compare/v.0.2.3...v.0.2.4
124
+
125
+ ## v.0.2.3
126
+ ### What's Changed
127
+ * Address docs build failing issue by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/39
128
+ * Fix docs pipeline 📄 by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/41
129
+ * Feature/dick/anomaly score normalization by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/35
130
+ * Shuffle train dataloader by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/42
131
+
132
+
133
+ **Full Changelog**: https://github.com/openvinotoolkit/anomalib/compare/v0.2.2...v.0.2.3
134
+
135
+ ## v0.2.0 Pre-release (2021-12-15)
136
+ ### What's Changed
137
+ * Address compatibility issues with OTE, that are caused by the legacy code. by @samet-akcay in [#24](https://github.com/openvinotoolkit/anomalib/pull/24)
138
+ * Initial docs string by @ashwinvaidya17 in [#9](https://github.com/openvinotoolkit/anomalib/pull/9)
139
+ * Load model did not work correctly as DFMModel did not inherit by @ashwinvaidya17 in [#5](https://github.com/openvinotoolkit/anomalib/pull/5)
140
+ * Refactor/samet/data by @samet-akcay in [#8](https://github.com/openvinotoolkit/anomalib/pull/8)
141
+ * Delete make.bat by @samet-akcay in [#11](https://github.com/openvinotoolkit/anomalib/pull/11)
142
+ * TorchMetrics by @djdameln in [#7](https://github.com/openvinotoolkit/anomalib/pull/7)
143
+ * ONNX node naming by @djdameln in [#13](https://github.com/openvinotoolkit/anomalib/pull/13)
144
+ * Add FPS counter to `TimerCallback` by @ashwinvaidya17 in [#12](https://github.com/openvinotoolkit/anomalib/pull/12)
145
+
146
+
147
+ ## Contributors
148
+ * @ashwinvaidya17
149
+ * @djdameln
150
+ * @samet-akcay
151
+
152
+ **Full Changelog**: https://github.com/openvinotoolkit/anomalib/commits/v0.2.0
CITATION.cff ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This CITATION.cff file was generated with cffinit.
2
+ # Visit https://bit.ly/cffinit to generate yours today!
3
+
4
+ cff-version: 1.2.0
5
+ title: "Anomalib: A Deep Learning Library for Anomaly Detection"
6
+ message: "If you use this library and love it, cite the software and the paper \U0001F917"
7
+ authors:
8
+ - given-names: Samet
9
+ family-names: Akcay
10
11
+ affiliation: Intel
12
+ - given-names: Dick
13
+ family-names: Ameln
14
15
+ affiliation: Intel
16
+ - given-names: Ashwin
17
+ family-names: Vaidya
18
19
+ affiliation: Intel
20
+ - given-names: Barath
21
+ family-names: Lakshmanan
22
23
+ affiliation: Intel
24
+ - given-names: Nilesh
25
+ family-names: Ahuja
26
27
+ affiliation: Intel
28
+ - given-names: Utku
29
+ family-names: Genc
30
31
+ affiliation: Intel
32
+ version: 0.2.6
33
+ doi: https://doi.org/10.48550/arXiv.2202.08341
34
+ date-released: 2022-02-18
35
+ references:
36
+ - type: article
37
+ authors:
38
+ - given-names: Samet
39
+ family-names: Akcay
40
41
+ affiliation: Intel
42
+ - given-names: Dick
43
+ family-names: Ameln
44
45
+ affiliation: Intel
46
+ - given-names: Ashwin
47
+ family-names: Vaidya
48
49
+ affiliation: Intel
50
+ - given-names: Barath
51
+ family-names: Lakshmanan
52
53
+ affiliation: Intel
54
+ - given-names: Nilesh
55
+ family-names: Ahuja
56
57
+ affiliation: Intel
58
+ - given-names: Utku
59
+ family-names: Genc
60
61
+ affiliation: Intel
62
+ title: "Anomalib: A Deep Learning Library for Anomaly Detection"
63
+ year: 2022
64
+ journal: ArXiv
65
+ doi: https://doi.org/10.48550/arXiv.2202.08341
66
+ url: https://arxiv.org/abs/2202.08341
67
+
68
+ abstract: >-
69
+ This paper introduces anomalib, a novel library for
70
+ unsupervised anomaly detection and localization.
71
+ With reproducibility and modularity in mind, this
72
+ open-source library provides algorithms from the
73
+ literature and a set of tools to design custom
74
+ anomaly detection algorithms via a plug-and-play
75
+ approach. Anomalib comprises state-of-the-art
76
+ anomaly detection algorithms that achieve top
77
+ performance on the benchmarks and that can be used
78
+ off-the-shelf. In addition, the library provides
79
+ components to design custom algorithms that could
80
+ be tailored towards specific needs. Additional
81
+ tools, including experiment trackers, visualizers,
82
+ and hyper-parameter optimizers, make it simple to
83
+ design and implement anomaly detection models. The
84
+ library also supports OpenVINO model optimization
85
+ and quantization for real-time deployment. Overall,
86
+ anomalib is an extensive library for the design,
87
+ implementation, and deployment of unsupervised
88
+ anomaly detection models from data to the edge.
89
+ keywords:
90
+ - Unsupervised Anomaly detection
91
+ - Unsupervised Anomaly localization
92
+ license: Apache-2.0
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [Intel Integrity Line](https://secure.ethicspoint.com/domain/media/en/gui/31244/index.html).
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
CONTRIBUTING.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to Anomalib
2
+ We welcome your input! 👐
3
+
4
+ We want to make it as simple and straightforward as possible to contribute to this project, whether it is a:
5
+
6
+ - Bug Report
7
+ - Discussion
8
+ - Feature Request
9
+ - Creating a Pull Request (PR)
10
+ - Becoming a maintainer
11
+
12
+
13
+ ## Bug Report
14
+ We use GitHub issues to track the bugs. Report a bug by using our Bug Report Template in [Issues](https://github.com/openvinotoolkit/anomalib/issues/new?assignees=&labels=&template=bug_report.md).
15
+
16
+
17
+ ## Discussion
18
+ We enabled [GitHub Discussions](https://github.com/openvinotoolkit/anomalib/discussions/) in anomalib to welcome the community to ask questions and/or propose ideas/solutions. This will not only provide a medium to the community to discuss about anomalib but also help us de-clutter [Issues](https://github.com/openvinotoolkit/anomalib/issues/new?assignees=&labels=&template=bug_report.md).
19
+
20
+
21
+ ## Feature Request
22
+ We utilize GitHub issues to track the feature requests as well. If you are certain regarding the feature you are interested and have a solid proposal, you could then create the feature request by using our [Feature Request Template](https://github.com/openvinotoolkit/anomalib/issues/new?assignees=&labels=&template=feature_request.md) in Issues. If it's still in an idea phase, you could then discuss that with the community in our [Discussion](https://github.com/openvinotoolkit/anomalib/discussions/categories/ideas).
23
+
24
+
25
+ ## Development & PRs
26
+ We actively welcome your pull requests:
27
+
28
+ 1. Fork the repo and create your branch from [`development`](https://github.com/openvinotoolkit/anomalib/tree/development).
29
+ 2. If you've added code that should be tested, add tests.
30
+ 3. If you've changed APIs, update the documentation.
31
+ 4. Ensure the test suite passes.
32
+ 5. Make sure your code lints.
33
+ 6. Make sure you own the code you're submitting or that you obtain it from a source with an appropriate license.
34
+ 7. Issue that pull request!
35
+
36
+ To setup the development environment, you will need to install development requirements.
37
+ ```
38
+ pip install -r requirements/dev.txt
39
+ ```
40
+
41
+ To enforce consistency within the repo, we use several formatters, linters, and style- and type checkers:
42
+
43
+ | Tool | Function | Documentation |
44
+ | ------ | -------------------------- | --------------------------------------- |
45
+ | Black | Code formatting | https://black.readthedocs.io/en/stable/ |
46
+ | isort | Organize import statements | https://pycqa.github.io/isort/ |
47
+ | Flake8 | Code style | https://flake8.pycqa.org/en/latest/ |
48
+ | Pylint | Linting | http://pylint.pycqa.org/en/latest/ |
49
+ | MyPy | Type checking | https://mypy.readthedocs.io/en/stable/ |
50
+
51
+ Instead of running each of these tools manually, we automatically run them before each commit and after each merge request. To achieve this we use pre-commit hooks and tox. Every developer is expected to use pre-commit hooks to make sure that their code remains free of typing and linting issues, and complies with the coding style requirements. When an MR is submitted, tox will be automatically invoked from the CI pipeline in Gitlab to check if the code quality is up to standard. Developers can also run tox locally before making an MR, though this is not strictly necessary since pre-commit hooks should be sufficient to prevent code quality issues. More detailed explanations of how to work with these tools is given in the respective guides:
52
+
53
+ - Pre-commit hooks: [Pre-commit hooks guide](https://openvinotoolkit.github.io/anomalib/guides/using_pre_commit.html#pre-commit-hooks)
54
+ - Tox: [Using Tox](https://openvinotoolkit.github.io/anomalib/guides/using_tox.html#using-tox)
55
+
56
+ In rare cases it might be desired to ignore certain errors or warnings for a particular part of your code. Flake8, Pylint and MyPy allow disabling specific errors for a line or block of code. The instructions for this can be found in the the documentations of each of these tools. Please make sure to only ignore errors/warnings when absolutely necessary, and always add a comment in your code stating why you chose to ignore it.
57
+
58
+
59
+ ## License
60
+ You accept that your contributions will be licensed under the [Apache-2.0 License](https://choosealicense.com/licenses/apache-2.0/) if you contribute to this repository. If this is a concern, please notify the maintainers.
61
+
62
+
63
+ ## References
64
+ This document was adapted from [here](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62).
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #########################################################
2
+ ## Python Environment with CUDA
3
+ #########################################################
4
+
5
+ FROM nvidia/cuda:11.4.0-devel-ubuntu20.04 AS python_base_cuda
6
+ LABEL MAINTAINER="Anomalib Development Team"
7
+
8
+ # Update system and install wget
9
+ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y wget ffmpeg libpython3.8 git sudo
10
+
11
+ # Install Conda
12
+ RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh --quiet && \
13
+ bash ~/miniconda.sh -b -p /opt/conda
14
+ ENV PATH "/opt/conda/bin:${PATH}"
15
+ RUN conda install python=3.8
16
+
17
+
18
+ #########################################################
19
+ ## Anomalib Development Env
20
+ #########################################################
21
+
22
+ FROM python_base_cuda as anomalib_development_env
23
+
24
+ # Install all anomalib requirements
25
+ COPY ./requirements/base.txt /tmp/anomalib/requirements/base.txt
26
+ RUN pip install -r /tmp/anomalib/requirements/base.txt
27
+
28
+ COPY ./requirements/openvino.txt /tmp/anomalib/requirements/openvino.txt
29
+ RUN pip install -r /tmp/anomalib/requirements/openvino.txt
30
+
31
+ # Install other requirements related to development
32
+ COPY ./requirements/dev.txt /tmp/anomalib/requirements/dev.txt
33
+ RUN pip install -r /tmp/anomalib/requirements/dev.txt
34
+
35
+ # Install anomalib
36
+ COPY . /anomalib
37
+ WORKDIR /anomalib
38
+ RUN pip install -e .
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright (C) 2020-2021 Intel Corporation
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
MANIFEST.in ADDED
@@ -0,0 +1 @@
 
 
1
+ recursive-include requirements *
README copy.md ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ <img src="docs/source/images/logos/anomalib-wide-blue.png" width="600px">
4
+
5
+ **A library for benchmarking, developing and deploying deep learning anomaly detection algorithms**
6
+ ___
7
+
8
+ [Key Features](#key-features) •
9
+ [Getting Started](#getting-started) •
10
+ [Docs](https://openvinotoolkit.github.io/anomalib) •
11
+ [License](https://github.com/openvinotoolkit/anomalib/blob/development/LICENSE)
12
+
13
+ [![python](https://img.shields.io/badge/python-3.7%2B-green)]()
14
+ [![pytorch](https://img.shields.io/badge/pytorch-1.8.1%2B-orange)]()
15
+ [![openvino](https://img.shields.io/badge/openvino-2021.4.2-purple)]()
16
+ [![black](https://img.shields.io/badge/code%20style-black-000000.svg)]()
17
+ [![Nightly-regression Test](https://github.com/openvinotoolkit/anomalib/actions/workflows/nightly.yml/badge.svg)](https://github.com/openvinotoolkit/anomalib/actions/workflows/nightly.yml)
18
+ [![Pre-merge Checks](https://github.com/openvinotoolkit/anomalib/actions/workflows/pre_merge.yml/badge.svg)](https://github.com/openvinotoolkit/anomalib/actions/workflows/pre_merge.yml)
19
+ [![Build Docs](https://github.com/openvinotoolkit/anomalib/actions/workflows/docs.yml/badge.svg)](https://github.com/openvinotoolkit/anomalib/actions/workflows/docs.yml)
20
+
21
+ </div>
22
+
23
+ ___
24
+
25
+ ## Introduction
26
+
27
+ Anomalib is a deep learning library that aims to collect state-of-the-art anomaly detection algorithms for benchmarking on both public and private datasets. Anomalib provides several ready-to-use implementations of anomaly detection algorithms described in the recent literature, as well as a set of tools that facilitate the development and implementation of custom models. The library has a strong focus on image-based anomaly detection, where the goal of the algorithm is to identify anomalous images, or anomalous pixel regions within images in a dataset. Anomalib is constantly updated with new algorithms and training/inference extensions, so keep checking!
28
+
29
+ ![Sample Image](./docs/source/images/readme.png)
30
+
31
+ **Key features:**
32
+
33
+ - The largest public collection of ready-to-use deep learning anomaly detection algorithms and benchmark datasets.
34
+ - [**PyTorch Lightning**](https://www.pytorchlightning.ai/) based model implementations to reduce boilerplate code and limit the implementation efforts to the bare essentials.
35
+ - All models can be exported to [**OpenVINO**](https://www.intel.com/content/www/us/en/developer/tools/openvino-toolkit/overview.html) Intermediate Representation (IR) for accelerated inference on intel hardware.
36
+ - A set of [inference tools](#inference) for quick and easy deployment of the standard or custom anomaly detection models.
37
+
38
+ ___
39
+
40
+ ## Getting Started
41
+
42
+ To get an overview of all the devices where `anomalib` as been tested thoroughly, look at the [Supported Hardware](https://openvinotoolkit.github.io/anomalib/#supported-hardware) section in the documentation.
43
+
44
+ ### PyPI Install
45
+
46
+ You can get started with `anomalib` by just using pip.
47
+
48
+ ```bash
49
+ pip install anomalib
50
+ ```
51
+
52
+ ### Local Install
53
+ It is highly recommended to use virtual environment when installing anomalib. For instance, with [anaconda](https://www.anaconda.com/products/individual), `anomalib` could be installed as,
54
+
55
+ ```bash
56
+ yes | conda create -n anomalib_env python=3.8
57
+ conda activate anomalib_env
58
+ git clone https://github.com/openvinotoolkit/anomalib.git
59
+ cd anomalib
60
+ pip install -e .
61
+ ```
62
+
63
+ ## Training
64
+
65
+ By default [`python tools/train.py`](https://gitlab-icv.inn.intel.com/algo_rnd_team/anomaly/-/blob/development/train.py)
66
+ runs [PADIM](https://arxiv.org/abs/2011.08785) model on `leather` category from the [MVTec AD](https://www.mvtec.com/company/research/datasets/mvtec-ad) [(CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/) dataset.
67
+
68
+ ```bash
69
+ python tools/train.py # Train PADIM on MVTec AD leather
70
+ ```
71
+
72
+ Training a model on a specific dataset and category requires further configuration. Each model has its own configuration
73
+ file, [`config.yaml`](https://gitlab-icv.inn.intel.com/algo_rnd_team/anomaly/-/blob/development/padim/anomalib/models/padim/config.yaml)
74
+ , which contains data, model and training configurable parameters. To train a specific model on a specific dataset and
75
+ category, the config file is to be provided:
76
+
77
+ ```bash
78
+ python tools/train.py --config <path/to/model/config.yaml>
79
+ ```
80
+
81
+ For example, to train [PADIM](anomalib/models/padim) you can use
82
+
83
+ ```bash
84
+ python tools/train.py --config anomalib/models/padim/config.yaml
85
+ ```
86
+
87
+ Note that `--model_config_path` will be deprecated in `v0.2.8` and removed
88
+ in `v0.2.9`.
89
+
90
+ Alternatively, a model name could also be provided as an argument, where the scripts automatically finds the corresponding config file.
91
+
92
+ ```bash
93
+ python tools/train.py --model padim
94
+ ```
95
+
96
+ where the currently available models are:
97
+
98
+ - [CFlow](anomalib/models/cflow)
99
+ - [PatchCore](anomalib/models/patchcore)
100
+ - [PADIM](anomalib/models/padim)
101
+ - [STFPM](anomalib/models/stfpm)
102
+ - [DFM](anomalib/models/dfm)
103
+ - [DFKDE](anomalib/models/dfkde)
104
+ - [GANomaly](anomalib/models/ganomaly)
105
+
106
+ ### Custom Dataset
107
+ It is also possible to train on a custom folder dataset. To do so, `data` section in `config.yaml` is to be modified as follows:
108
+ ```yaml
109
+ dataset:
110
+ name: <name-of-the-dataset>
111
+ format: folder
112
+ path: <path/to/folder/dataset>
113
+ normal: normal # name of the folder containing normal images.
114
+ abnormal: abnormal # name of the folder containing abnormal images.
115
+ task: segmentation # classification or segmentation
116
+ mask: <path/to/mask/annotations> #optional
117
+ extensions: null
118
+ split_ratio: 0.2 # ratio of the normal images that will be used to create a test split
119
+ seed: 0
120
+ image_size: 256
121
+ train_batch_size: 32
122
+ test_batch_size: 32
123
+ num_workers: 8
124
+ transform_config: null
125
+ create_validation_set: true
126
+ tiling:
127
+ apply: false
128
+ tile_size: null
129
+ stride: null
130
+ remove_border_count: 0
131
+ use_random_tiling: False
132
+ random_tile_count: 16
133
+ ```
134
+ ## Inference
135
+
136
+ Anomalib contains several tools that can be used to perform inference with a trained model. The script in [`tools/inference`](tools/inference.py) contains an example of how the inference tools can be used to generate a prediction for an input image.
137
+
138
+ If the specified weight path points to a PyTorch Lightning checkpoint file (`.ckpt`), inference will run in PyTorch. If the path points to an ONNX graph (`.onnx`) or OpenVINO IR (`.bin` or `.xml`), inference will run in OpenVINO.
139
+
140
+ The following command can be used to run inference from the command line:
141
+
142
+ ```bash
143
+ python tools/inference.py \
144
+ --config <path/to/model/config.yaml> \
145
+ --weight_path <path/to/weight/file> \
146
+ --image_path <path/to/image>
147
+ ```
148
+
149
+ As a quick example:
150
+
151
+ ```bash
152
+ python tools/inference.py \
153
+ --config anomalib/models/padim/config.yaml \
154
+ --weight_path results/padim/mvtec/bottle/weights/model.ckpt \
155
+ --image_path datasets/MVTec/bottle/test/broken_large/000.png
156
+ ```
157
+
158
+ If you want to run OpenVINO model, ensure that `openvino` `apply` is set to `True` in the respective model `config.yaml`.
159
+
160
+ ```yaml
161
+ optimization:
162
+ openvino:
163
+ apply: true
164
+ ```
165
+
166
+ Example OpenVINO Inference:
167
+
168
+ ```bash
169
+ python tools/inference.py \
170
+ --config \
171
+ anomalib/models/padim/config.yaml \
172
+ --weight_path \
173
+ results/padim/mvtec/bottle/compressed/compressed_model.xml \
174
+ --image_path \
175
+ datasets/MVTec/bottle/test/broken_large/000.png \
176
+ --meta_data \
177
+ results/padim/mvtec/bottle/compressed/meta_data.json
178
+ ```
179
+
180
+ > Ensure that you provide path to `meta_data.json` if you want the normalization to be applied correctly.
181
+
182
+ ___
183
+
184
+ ## Datasets
185
+ `anomalib` supports MVTec AD [(CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/) and BeanTech [(CC-BY-SA)](https://creativecommons.org/licenses/by-sa/4.0/legalcode) for benchmarking and `folder` for custom dataset training/inference.
186
+
187
+ ### [MVTec AD Dataset](https://www.mvtec.com/company/research/datasets/mvtec-ad)
188
+ MVTec AD dataset is one of the main benchmarks for anomaly detection, and is released under the
189
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License [(CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/).
190
+
191
+ ### Image-Level AUC
192
+
193
+ | Model | | Avg | Carpet | Grid | Leather | Tile | Wood | Bottle | Cable | Capsule | Hazelnut | Metal Nut | Pill | Screw | Toothbrush | Transistor | Zipper |
194
+ | ------------- | ------------------ | :-------: | :-------: | :-------: | :-----: | :-------: | :-------: | :-----: | :-------: | :-------: | :------: | :-------: | :-------: | :-------: | :--------: | :--------: | :-------: |
195
+ | **PatchCore** | **Wide ResNet-50** | **0.980** | 0.984 | 0.959 | 1.000 | **1.000** | 0.989 | 1.000 | **0.990** | **0.982** | 1.000 | 0.994 | 0.924 | 0.960 | 0.933 | **1.000** | 0.982 |
196
+ | PatchCore | ResNet-18 | 0.973 | 0.970 | 0.947 | 1.000 | 0.997 | 0.997 | 1.000 | 0.986 | 0.965 | 1.000 | 0.991 | 0.916 | **0.943** | 0.931 | 0.996 | 0.953 |
197
+ | CFlow | Wide ResNet-50 | 0.962 | 0.986 | 0.962 | **1.0** | 0.999 | **0.993** | **1.0** | 0.893 | 0.945 | **1.0** | **0.995** | 0.924 | 0.908 | 0.897 | 0.943 | **0.984** |
198
+ | PaDiM | Wide ResNet-50 | 0.950 | **0.995** | 0.942 | 1.0 | 0.974 | **0.993** | 0.999 | 0.878 | 0.927 | 0.964 | 0.989 | **0.939** | 0.845 | 0.942 | 0.976 | 0.882 |
199
+ | PaDiM | ResNet-18 | 0.891 | 0.945 | 0.857 | 0.982 | 0.950 | 0.976 | 0.994 | 0.844 | 0.901 | 0.750 | 0.961 | 0.863 | 0.759 | 0.889 | 0.920 | 0.780 |
200
+ | STFPM | Wide ResNet-50 | 0.876 | 0.957 | 0.977 | 0.981 | 0.976 | 0.939 | 0.987 | 0.878 | 0.732 | 0.995 | 0.973 | 0.652 | 0.825 | 0.5 | 0.875 | 0.899 |
201
+ | STFPM | ResNet-18 | 0.893 | 0.954 | **0.982** | 0.989 | 0.949 | 0.961 | 0.979 | 0.838 | 0.759 | 0.999 | 0.956 | 0.705 | 0.835 | **0.997** | 0.853 | 0.645 |
202
+ | DFM | Wide ResNet-50 | 0.891 | 0.978 | 0.540 | 0.979 | 0.977 | 0.974 | 0.990 | 0.891 | 0.931 | 0.947 | 0.839 | 0.809 | 0.700 | 0.911 | 0.915 | 0.981 |
203
+ | DFM | ResNet-18 | 0.894 | 0.864 | 0.558 | 0.945 | 0.984 | 0.946 | 0.994 | 0.913 | 0.871 | 0.979 | 0.941 | 0.838 | 0.761 | 0.95 | 0.911 | 0.949 |
204
+ | DFKDE | Wide ResNet-50 | 0.774 | 0.708 | 0.422 | 0.905 | 0.959 | 0.903 | 0.936 | 0.746 | 0.853 | 0.736 | 0.687 | 0.749 | 0.574 | 0.697 | 0.843 | 0.892 |
205
+ | DFKDE | ResNet-18 | 0.762 | 0.646 | 0.577 | 0.669 | 0.965 | 0.863 | 0.951 | 0.751 | 0.698 | 0.806 | 0.729 | 0.607 | 0.694 | 0.767 | 0.839 | 0.866 |
206
+ | GANomaly | | 0.421 | 0.203 | 0.404 | 0.413 | 0.408 | 0.744 | 0.251 | 0.457 | 0.682 | 0.537 | 0.270 | 0.472 | 0.231 | 0.372 | 0.440 | 0.434 |
207
+
208
+ ### Pixel-Level AUC
209
+
210
+ | Model | | Avg | Carpet | Grid | Leather | Tile | Wood | Bottle | Cable | Capsule | Hazelnut | Metal Nut | Pill | Screw | Toothbrush | Transistor | Zipper |
211
+ | ------------- | ------------------ | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :--------: | :--------: | :-------: |
212
+ | **PatchCore** | **Wide ResNet-50** | **0.980** | 0.988 | 0.968 | 0.991 | 0.961 | 0.934 | 0.984 | **0.988** | **0.988** | 0.987 | **0.989** | 0.980 | **0.989** | 0.988 | **0.981** | 0.983 |
213
+ | PatchCore | ResNet-18 | 0.976 | 0.986 | 0.955 | 0.990 | 0.943 | 0.933 | 0.981 | 0.984 | 0.986 | 0.986 | 0.986 | 0.974 | 0.991 | 0.988 | 0.974 | 0.983 |
214
+ | CFlow | Wide ResNet-50 | 0.971 | 0.986 | 0.968 | 0.993 | **0.968** | 0.924 | 0.981 | 0.955 | **0.988** | **0.990** | 0.982 | **0.983** | 0.979 | 0.985 | 0.897 | 0.980 |
215
+ | PaDiM | Wide ResNet-50 | 0.979 | **0.991** | 0.970 | 0.993 | 0.955 | **0.957** | **0.985** | 0.970 | **0.988** | 0.985 | 0.982 | 0.966 | 0.988 | **0.991** | 0.976 | **0.986** |
216
+ | PaDiM | ResNet-18 | 0.968 | 0.984 | 0.918 | **0.994** | 0.934 | 0.947 | 0.983 | 0.965 | 0.984 | 0.978 | 0.970 | 0.957 | 0.978 | 0.988 | 0.968 | 0.979 |
217
+ | STFPM | Wide ResNet-50 | 0.903 | 0.987 | **0.989** | 0.980 | 0.966 | 0.956 | 0.966 | 0.913 | 0.956 | 0.974 | 0.961 | 0.946 | 0.988 | 0.178 | 0.807 | 0.980 |
218
+ | STFPM | ResNet-18 | 0.951 | 0.986 | 0.988 | 0.991 | 0.946 | 0.949 | 0.971 | 0.898 | 0.962 | 0.981 | 0.942 | 0.878 | 0.983 | 0.983 | 0.838 | 0.972 |
219
+
220
+ ### Image F1 Score
221
+
222
+ | Model | | Avg | Carpet | Grid | Leather | Tile | Wood | Bottle | Cable | Capsule | Hazelnut | Metal Nut | Pill | Screw | Toothbrush | Transistor | Zipper |
223
+ | ------------- | ------------------ | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :-------: | :--------: | :--------: | :-------: |
224
+ | **PatchCore** | **Wide ResNet-50** | **0.976** | 0.971 | 0.974 | **1.000** | **1.000** | 0.967 | **1.000** | 0.968 | **0.982** | **1.000** | 0.984 | 0.940 | 0.943 | 0.938 | **1.000** | **0.979** |
225
+ | PatchCore | ResNet-18 | 0.970 | 0.949 | 0.946 | **1.000** | 0.98 | **0.992** | **1.000** | **0.978** | 0.969 | **1.000** | **0.989** | 0.940 | 0.932 | 0.935 | 0.974 | 0.967 |
226
+ | CFlow | Wide ResNet-50 | 0.944 | 0.972 | 0.932 | **1.0** | 0.988 | 0.967 | **1.0** | 0.832 | 0.939 | **1.0** | 0.979 | 0.924 | **0.971** | 0.870 | 0.818 | 0.967 |
227
+ | PaDiM | Wide ResNet-50 | 0.951 | **0.989** | 0.930 | **1.0** | 0.960 | 0.983 | 0.992 | 0.856 | **0.982** | 0.937 | 0.978 | **0.946** | 0.895 | 0.952 | 0.914 | 0.947 |
228
+ | PaDiM | ResNet-18 | 0.916 | 0.930 | 0.893 | 0.984 | 0.934 | 0.952 | 0.976 | 0.858 | 0.960 | 0.836 | 0.974 | 0.932 | 0.879 | 0.923 | 0.796 | 0.915 |
229
+ | STFPM | Wide ResNet-50 | 0.926 | 0.973 | 0.973 | 0.974 | 0.965 | 0.929 | 0.976 | 0.853 | 0.920 | 0.972 | 0.974 | 0.922 | 0.884 | 0.833 | 0.815 | 0.931 |
230
+ | STFPM | ResNet-18 | 0.932 | 0.961 | **0.982** | 0.989 | 0.930 | 0.951 | 0.984 | 0.819 | 0.918 | 0.993 | 0.973 | 0.918 | 0.887 | **0.984** | 0.790 | 0.908 |
231
+ | DFM | Wide ResNet-50 | 0.918 | 0.960 | 0.844 | 0.990 | 0.970 | 0.959 | 0.976 | 0.848 | 0.944 | 0.913 | 0.912 | 0.919 | 0.859 | 0.893 | 0.815 | 0.961 |
232
+ | DFM | ResNet-18 | 0.919 | 0.895 | 0.844 | 0.926 | 0.971 | 0.948 | 0.977 | 0.874 | 0.935 | 0.957 | 0.958 | 0.921 | 0.874 | 0.933 | 0.833 | 0.943 |
233
+ | DFKDE | Wide ResNet-50 | 0.875 | 0.907 | 0.844 | 0.905 | 0.945 | 0.914 | 0.946 | 0.790 | 0.914 | 0.817 | 0.894 | 0.922 | 0.855 | 0.845 | 0.722 | 0.910 |
234
+ | DFKDE | ResNet-18 | 0.872 | 0.864 | 0.844 | 0.854 | 0.960 | 0.898 | 0.942 | 0.793 | 0.908 | 0.827 | 0.894 | 0.916 | 0.859 | 0.853 | 0.756 | 0.916 |
235
+ | GANomaly | | 0.834 | 0.864 | 0.844 | 0.852 | 0.836 | 0.863 | 0.863 | 0.760 | 0.905 | 0.777 | 0.894 | 0.916 | 0.853 | 0.833 | 0.571 | 0.881 |
236
+
237
+ ## Reference
238
+ If you use this library and love it, use this to cite it 🤗
239
+ ```
240
+ @misc{anomalib,
241
+ title={Anomalib: A Deep Learning Library for Anomaly Detection},
242
+ author={Samet Akcay and
243
+ Dick Ameln and
244
+ Ashwin Vaidya and
245
+ Barath Lakshmanan and
246
+ Nilesh Ahuja and
247
+ Utku Genc},
248
+ year={2022},
249
+ eprint={2202.08341},
250
+ archivePrefix={arXiv},
251
+ primaryClass={cs.CV}
252
+ }
253
+ ```
anomalib/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Anomalib library for research and benchmarking."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ __version__ = "0.3.0"
anomalib/config/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utilities for parsing model configuration."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .config import (
18
+ get_configurable_parameters,
19
+ update_input_size_config,
20
+ update_nncf_config,
21
+ )
22
+
23
+ __all__ = ["get_configurable_parameters", "update_nncf_config", "update_input_size_config"]
anomalib/config/config.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Get configurable parameters."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ # TODO: This would require a new design.
18
+ # TODO: https://jira.devtools.intel.com/browse/IAAALD-149
19
+
20
+ from pathlib import Path
21
+ from typing import List, Optional, Union
22
+ from warnings import warn
23
+
24
+ from omegaconf import DictConfig, ListConfig, OmegaConf
25
+
26
+
27
+ def update_input_size_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]:
28
+ """Update config with image size as tuple, effective input size and tiling stride.
29
+
30
+ Convert integer image size parameters into tuples, calculate the effective input size based on image size
31
+ and crop size, and set tiling stride if undefined.
32
+
33
+ Args:
34
+ config (Union[DictConfig, ListConfig]): Configurable parameters object
35
+
36
+ Returns:
37
+ Union[DictConfig, ListConfig]: Configurable parameters with updated values
38
+ """
39
+ # handle image size
40
+ if isinstance(config.dataset.image_size, int):
41
+ config.dataset.image_size = (config.dataset.image_size,) * 2
42
+
43
+ config.model.input_size = config.dataset.image_size
44
+
45
+ if "tiling" in config.dataset.keys() and config.dataset.tiling.apply:
46
+ if isinstance(config.dataset.tiling.tile_size, int):
47
+ config.dataset.tiling.tile_size = (config.dataset.tiling.tile_size,) * 2
48
+ if config.dataset.tiling.stride is None:
49
+ config.dataset.tiling.stride = config.dataset.tiling.tile_size
50
+
51
+ return config
52
+
53
+
54
+ def update_nncf_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]:
55
+ """Set the NNCF input size based on the value of the crop_size parameter in the configurable parameters object.
56
+
57
+ Args:
58
+ config (Union[DictConfig, ListConfig]): Configurable parameters of the current run.
59
+
60
+ Returns:
61
+ Union[DictConfig, ListConfig]: Updated configurable parameters in DictConfig object.
62
+ """
63
+ crop_size = config.dataset.image_size
64
+ sample_size = (crop_size, crop_size) if isinstance(crop_size, int) else crop_size
65
+ if "optimization" in config.keys():
66
+ if "nncf" in config.optimization.keys():
67
+ config.optimization.nncf.input_info.sample_size = [1, 3, *sample_size]
68
+ if config.optimization.nncf.apply:
69
+ if "update_config" in config.optimization.nncf:
70
+ return OmegaConf.merge(config, config.optimization.nncf.update_config)
71
+ return config
72
+
73
+
74
+ def update_multi_gpu_training_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]:
75
+ """Updates the config to change learning rate based on number of gpus assigned.
76
+
77
+ Current behaviour is to ensure only ddp accelerator is used.
78
+
79
+ Args:
80
+ config (Union[DictConfig, ListConfig]): Configurable parameters for the current run
81
+
82
+ Raises:
83
+ ValueError: If unsupported accelerator is passed
84
+
85
+ Returns:
86
+ Union[DictConfig, ListConfig]: Updated config
87
+ """
88
+ # validate accelerator
89
+ if config.trainer.accelerator is not None:
90
+ if config.trainer.accelerator.lower() != "ddp":
91
+ if config.trainer.accelerator.lower() in ("dp", "ddp_spawn", "ddp2"):
92
+ warn(
93
+ f"Using accelerator {config.trainer.accelerator.lower()} is discouraged. "
94
+ f"Please use one of [null, ddp]. Setting accelerator to ddp"
95
+ )
96
+ config.trainer.accelerator = "ddp"
97
+ else:
98
+ raise ValueError(
99
+ f"Unsupported accelerator found: {config.trainer.accelerator}. Should be one of [null, ddp]"
100
+ )
101
+ # Increase learning rate
102
+ # since pytorch averages the gradient over devices, the idea is to
103
+ # increase the learning rate by the number of devices
104
+ if "lr" in config.model:
105
+ # Number of GPUs can either be passed as gpus: 2 or gpus: [0,1]
106
+ n_gpus: Union[int, List] = 1
107
+ if "trainer" in config and "gpus" in config.trainer:
108
+ n_gpus = config.trainer.gpus
109
+ lr_scaler = n_gpus if isinstance(n_gpus, int) else len(n_gpus)
110
+ config.model.lr = config.model.lr * lr_scaler
111
+ return config
112
+
113
+
114
+ def get_configurable_parameters(
115
+ model_name: Optional[str] = None,
116
+ config_path: Optional[Union[Path, str]] = None,
117
+ weight_file: Optional[str] = None,
118
+ config_filename: Optional[str] = "config",
119
+ config_file_extension: Optional[str] = "yaml",
120
+ ) -> Union[DictConfig, ListConfig]:
121
+ """Get configurable parameters.
122
+
123
+ Args:
124
+ model_name: Optional[str]: (Default value = None)
125
+ config_path: Optional[Union[Path, str]]: (Default value = None)
126
+ weight_file: Path to the weight file
127
+ config_filename: Optional[str]: (Default value = "config")
128
+ config_file_extension: Optional[str]: (Default value = "yaml")
129
+
130
+ Returns:
131
+ Union[DictConfig, ListConfig]: Configurable parameters in DictConfig object.
132
+ """
133
+ if model_name is None and config_path is None:
134
+ raise ValueError(
135
+ "Both model_name and model config path cannot be None! "
136
+ "Please provide a model name or path to a config file!"
137
+ )
138
+
139
+ if config_path is None:
140
+ config_path = Path(f"anomalib/models/{model_name}/{config_filename}.{config_file_extension}")
141
+
142
+ config = OmegaConf.load(config_path)
143
+
144
+ # Dataset Configs
145
+ if "format" not in config.dataset.keys():
146
+ config.dataset.format = "mvtec"
147
+
148
+ config = update_input_size_config(config)
149
+
150
+ # Project Configs
151
+ project_path = Path(config.project.path) / config.model.name / config.dataset.name
152
+ if config.dataset.format.lower() in ("btech", "mvtec"):
153
+ project_path = project_path / config.dataset.category
154
+
155
+ (project_path / "weights").mkdir(parents=True, exist_ok=True)
156
+ (project_path / "images").mkdir(parents=True, exist_ok=True)
157
+ config.project.path = str(project_path)
158
+ # loggers should write to results/model/dataset/category/ folder
159
+ config.trainer.default_root_dir = str(project_path)
160
+
161
+ if weight_file:
162
+ config.model.weight_file = weight_file
163
+
164
+ config = update_nncf_config(config)
165
+
166
+ # thresholding
167
+ if "pixel_default" not in config.model.threshold.keys():
168
+ config.model.threshold.pixel_default = config.model.threshold.image_default
169
+
170
+ return config
anomalib/data/__init__.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Anomalib Datasets."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from typing import Union
18
+
19
+ from omegaconf import DictConfig, ListConfig
20
+ from pytorch_lightning import LightningDataModule
21
+
22
+ from .btech import BTechDataModule
23
+ from .folder import FolderDataModule
24
+ from .inference import InferenceDataset
25
+ from .mvtec import MVTecDataModule
26
+
27
+
28
+ def get_datamodule(config: Union[DictConfig, ListConfig]) -> LightningDataModule:
29
+ """Get Anomaly Datamodule.
30
+
31
+ Args:
32
+ config (Union[DictConfig, ListConfig]): Configuration of the anomaly model.
33
+
34
+ Returns:
35
+ PyTorch Lightning DataModule
36
+ """
37
+ datamodule: LightningDataModule
38
+
39
+ if config.dataset.format.lower() == "mvtec":
40
+ datamodule = MVTecDataModule(
41
+ # TODO: Remove config values. IAAALD-211
42
+ root=config.dataset.path,
43
+ category=config.dataset.category,
44
+ image_size=(config.dataset.image_size[0], config.dataset.image_size[1]),
45
+ train_batch_size=config.dataset.train_batch_size,
46
+ test_batch_size=config.dataset.test_batch_size,
47
+ num_workers=config.dataset.num_workers,
48
+ seed=config.project.seed,
49
+ task=config.dataset.task,
50
+ transform_config_train=config.dataset.transform_config.train,
51
+ transform_config_val=config.dataset.transform_config.val,
52
+ create_validation_set=config.dataset.create_validation_set,
53
+ )
54
+ elif config.dataset.format.lower() == "btech":
55
+ datamodule = BTechDataModule(
56
+ # TODO: Remove config values. IAAALD-211
57
+ root=config.dataset.path,
58
+ category=config.dataset.category,
59
+ image_size=(config.dataset.image_size[0], config.dataset.image_size[1]),
60
+ train_batch_size=config.dataset.train_batch_size,
61
+ test_batch_size=config.dataset.test_batch_size,
62
+ num_workers=config.dataset.num_workers,
63
+ seed=config.project.seed,
64
+ task=config.dataset.task,
65
+ transform_config_train=config.dataset.transform_config.train,
66
+ transform_config_val=config.dataset.transform_config.val,
67
+ create_validation_set=config.dataset.create_validation_set,
68
+ )
69
+ elif config.dataset.format.lower() == "folder":
70
+ datamodule = FolderDataModule(
71
+ root=config.dataset.path,
72
+ normal_dir=config.dataset.normal_dir,
73
+ abnormal_dir=config.dataset.abnormal_dir,
74
+ task=config.dataset.task,
75
+ normal_test_dir=config.dataset.normal_test_dir,
76
+ mask_dir=config.dataset.mask,
77
+ extensions=config.dataset.extensions,
78
+ split_ratio=config.dataset.split_ratio,
79
+ seed=config.dataset.seed,
80
+ image_size=(config.dataset.image_size[0], config.dataset.image_size[1]),
81
+ train_batch_size=config.dataset.train_batch_size,
82
+ test_batch_size=config.dataset.test_batch_size,
83
+ num_workers=config.dataset.num_workers,
84
+ transform_config_train=config.dataset.transform_config.train,
85
+ transform_config_val=config.dataset.transform_config.val,
86
+ create_validation_set=config.dataset.create_validation_set,
87
+ )
88
+ else:
89
+ raise ValueError(
90
+ "Unknown dataset! \n"
91
+ "If you use a custom dataset make sure you initialize it in"
92
+ "`get_datamodule` in `anomalib.data.__init__.py"
93
+ )
94
+
95
+ return datamodule
96
+
97
+
98
+ __all__ = [
99
+ "get_datamodule",
100
+ "BTechDataModule",
101
+ "FolderDataModule",
102
+ "InferenceDataset",
103
+ "MVTecDataModule",
104
+ ]
anomalib/data/btech.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BTech Dataset.
2
+
3
+ This script contains PyTorch Lightning DataModule for the BTech dataset.
4
+
5
+ If the dataset is not on the file system, the script downloads and
6
+ extracts the dataset and create PyTorch data objects.
7
+ """
8
+
9
+ # Copyright (C) 2020 Intel Corporation
10
+ #
11
+ # Licensed under the Apache License, Version 2.0 (the "License");
12
+ # you may not use this file except in compliance with the License.
13
+ # You may obtain a copy of the License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing,
18
+ # software distributed under the License is distributed on an "AS IS" BASIS,
19
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ # See the License for the specific language governing permissions
21
+ # and limitations under the License.
22
+
23
+ import logging
24
+ import shutil
25
+ import zipfile
26
+ from pathlib import Path
27
+ from typing import Dict, Optional, Tuple, Union
28
+ from urllib.request import urlretrieve
29
+
30
+ import albumentations as A
31
+ import cv2
32
+ import numpy as np
33
+ import pandas as pd
34
+ from pandas.core.frame import DataFrame
35
+ from pytorch_lightning.core.datamodule import LightningDataModule
36
+ from pytorch_lightning.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS
37
+ from torch import Tensor
38
+ from torch.utils.data import DataLoader
39
+ from torch.utils.data.dataset import Dataset
40
+ from torchvision.datasets.folder import VisionDataset
41
+ from tqdm import tqdm
42
+
43
+ from anomalib.data.inference import InferenceDataset
44
+ from anomalib.data.utils import DownloadProgressBar, read_image
45
+ from anomalib.data.utils.split import (
46
+ create_validation_set_from_test_set,
47
+ split_normal_images_in_train_set,
48
+ )
49
+ from anomalib.pre_processing import PreProcessor
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ def make_btech_dataset(
55
+ path: Path,
56
+ split: Optional[str] = None,
57
+ split_ratio: float = 0.1,
58
+ seed: int = 0,
59
+ create_validation_set: bool = False,
60
+ ) -> DataFrame:
61
+ """Create BTech samples by parsing the BTech data file structure.
62
+
63
+ The files are expected to follow the structure:
64
+ path/to/dataset/split/category/image_filename.png
65
+ path/to/dataset/ground_truth/category/mask_filename.png
66
+
67
+ Args:
68
+ path (Path): Path to dataset
69
+ split (str, optional): Dataset split (ie., either train or test). Defaults to None.
70
+ split_ratio (float, optional): Ratio to split normal training images and add to the
71
+ test set in case test set doesn't contain any normal images.
72
+ Defaults to 0.1.
73
+ seed (int, optional): Random seed to ensure reproducibility when splitting. Defaults to 0.
74
+ create_validation_set (bool, optional): Boolean to create a validation set from the test set.
75
+ BTech dataset does not contain a validation set. Those wanting to create a validation set
76
+ could set this flag to ``True``.
77
+
78
+ Example:
79
+ The following example shows how to get training samples from BTech 01 category:
80
+
81
+ >>> root = Path('./BTech')
82
+ >>> category = '01'
83
+ >>> path = root / category
84
+ >>> path
85
+ PosixPath('BTech/01')
86
+
87
+ >>> samples = make_btech_dataset(path, split='train', split_ratio=0.1, seed=0)
88
+ >>> samples.head()
89
+ path split label image_path mask_path label_index
90
+ 0 BTech/01 train 01 BTech/01/train/ok/105.bmp BTech/01/ground_truth/ok/105.png 0
91
+ 1 BTech/01 train 01 BTech/01/train/ok/017.bmp BTech/01/ground_truth/ok/017.png 0
92
+ ...
93
+
94
+ Returns:
95
+ DataFrame: an output dataframe containing samples for the requested split (ie., train or test)
96
+ """
97
+ samples_list = [
98
+ (str(path),) + filename.parts[-3:] for filename in path.glob("**/*") if filename.suffix in (".bmp", ".png")
99
+ ]
100
+ if len(samples_list) == 0:
101
+ raise RuntimeError(f"Found 0 images in {path}")
102
+
103
+ samples = pd.DataFrame(samples_list, columns=["path", "split", "label", "image_path"])
104
+ samples = samples[samples.split != "ground_truth"]
105
+
106
+ # Create mask_path column
107
+ samples["mask_path"] = (
108
+ samples.path
109
+ + "/ground_truth/"
110
+ + samples.label
111
+ + "/"
112
+ + samples.image_path.str.rstrip("png").str.rstrip(".")
113
+ + ".png"
114
+ )
115
+
116
+ # Modify image_path column by converting to absolute path
117
+ samples["image_path"] = samples.path + "/" + samples.split + "/" + samples.label + "/" + samples.image_path
118
+
119
+ # Split the normal images in training set if test set doesn't
120
+ # contain any normal images. This is needed because AUC score
121
+ # cannot be computed based on 1-class
122
+ if sum((samples.split == "test") & (samples.label == "ok")) == 0:
123
+ samples = split_normal_images_in_train_set(samples, split_ratio, seed)
124
+
125
+ # Good images don't have mask
126
+ samples.loc[(samples.split == "test") & (samples.label == "ok"), "mask_path"] = ""
127
+
128
+ # Create label index for normal (0) and anomalous (1) images.
129
+ samples.loc[(samples.label == "ok"), "label_index"] = 0
130
+ samples.loc[(samples.label != "ok"), "label_index"] = 1
131
+ samples.label_index = samples.label_index.astype(int)
132
+
133
+ if create_validation_set:
134
+ samples = create_validation_set_from_test_set(samples, seed=seed)
135
+
136
+ # Get the data frame for the split.
137
+ if split is not None and split in ["train", "val", "test"]:
138
+ samples = samples[samples.split == split]
139
+ samples = samples.reset_index(drop=True)
140
+
141
+ return samples
142
+
143
+
144
+ class BTech(VisionDataset):
145
+ """BTech PyTorch Dataset."""
146
+
147
+ def __init__(
148
+ self,
149
+ root: Union[Path, str],
150
+ category: str,
151
+ pre_process: PreProcessor,
152
+ split: str,
153
+ task: str = "segmentation",
154
+ seed: int = 0,
155
+ create_validation_set: bool = False,
156
+ ) -> None:
157
+ """Btech Dataset class.
158
+
159
+ Args:
160
+ root: Path to the BTech dataset
161
+ category: Name of the BTech category.
162
+ pre_process: List of pre_processing object containing albumentation compose.
163
+ split: 'train', 'val' or 'test'
164
+ task: ``classification`` or ``segmentation``
165
+ seed: seed used for the random subset splitting
166
+ create_validation_set: Create a validation subset in addition to the train and test subsets
167
+
168
+ Examples:
169
+ >>> from anomalib.data.btech import BTech
170
+ >>> from anomalib.data.transforms import PreProcessor
171
+ >>> pre_process = PreProcessor(image_size=256)
172
+ >>> dataset = BTech(
173
+ ... root='./datasets/BTech',
174
+ ... category='leather',
175
+ ... pre_process=pre_process,
176
+ ... task="classification",
177
+ ... is_train=True,
178
+ ... )
179
+ >>> dataset[0].keys()
180
+ dict_keys(['image'])
181
+
182
+ >>> dataset.split = "test"
183
+ >>> dataset[0].keys()
184
+ dict_keys(['image', 'image_path', 'label'])
185
+
186
+ >>> dataset.task = "segmentation"
187
+ >>> dataset.split = "train"
188
+ >>> dataset[0].keys()
189
+ dict_keys(['image'])
190
+
191
+ >>> dataset.split = "test"
192
+ >>> dataset[0].keys()
193
+ dict_keys(['image_path', 'label', 'mask_path', 'image', 'mask'])
194
+
195
+ >>> dataset[0]["image"].shape, dataset[0]["mask"].shape
196
+ (torch.Size([3, 256, 256]), torch.Size([256, 256]))
197
+ """
198
+ super().__init__(root)
199
+ self.root = Path(root) if isinstance(root, str) else root
200
+ self.category: str = category
201
+ self.split = split
202
+ self.task = task
203
+
204
+ self.pre_process = pre_process
205
+
206
+ self.samples = make_btech_dataset(
207
+ path=self.root / category,
208
+ split=self.split,
209
+ seed=seed,
210
+ create_validation_set=create_validation_set,
211
+ )
212
+
213
+ def __len__(self) -> int:
214
+ """Get length of the dataset."""
215
+ return len(self.samples)
216
+
217
+ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]:
218
+ """Get dataset item for the index ``index``.
219
+
220
+ Args:
221
+ index (int): Index to get the item.
222
+
223
+ Returns:
224
+ Union[Dict[str, Tensor], Dict[str, Union[str, Tensor]]]: Dict of image tensor during training.
225
+ Otherwise, Dict containing image path, target path, image tensor, label and transformed bounding box.
226
+ """
227
+ item: Dict[str, Union[str, Tensor]] = {}
228
+
229
+ image_path = self.samples.image_path[index]
230
+ image = read_image(image_path)
231
+
232
+ pre_processed = self.pre_process(image=image)
233
+ item = {"image": pre_processed["image"]}
234
+
235
+ if self.split in ["val", "test"]:
236
+ label_index = self.samples.label_index[index]
237
+
238
+ item["image_path"] = image_path
239
+ item["label"] = label_index
240
+
241
+ if self.task == "segmentation":
242
+ mask_path = self.samples.mask_path[index]
243
+
244
+ # Only Anomalous (1) images has masks in BTech dataset.
245
+ # Therefore, create empty mask for Normal (0) images.
246
+ if label_index == 0:
247
+ mask = np.zeros(shape=image.shape[:2])
248
+ else:
249
+ mask = cv2.imread(mask_path, flags=0) / 255.0
250
+
251
+ pre_processed = self.pre_process(image=image, mask=mask)
252
+
253
+ item["mask_path"] = mask_path
254
+ item["image"] = pre_processed["image"]
255
+ item["mask"] = pre_processed["mask"]
256
+
257
+ return item
258
+
259
+
260
+ class BTechDataModule(LightningDataModule):
261
+ """BTechDataModule Lightning Data Module."""
262
+
263
+ def __init__(
264
+ self,
265
+ root: str,
266
+ category: str,
267
+ # TODO: Remove default values. IAAALD-211
268
+ image_size: Optional[Union[int, Tuple[int, int]]] = None,
269
+ train_batch_size: int = 32,
270
+ test_batch_size: int = 32,
271
+ num_workers: int = 8,
272
+ task: str = "segmentation",
273
+ transform_config_train: Optional[Union[str, A.Compose]] = None,
274
+ transform_config_val: Optional[Union[str, A.Compose]] = None,
275
+ seed: int = 0,
276
+ create_validation_set: bool = False,
277
+ ) -> None:
278
+ """Instantiate BTech Lightning Data Module.
279
+
280
+ Args:
281
+ root: Path to the BTech dataset
282
+ category: Name of the BTech category.
283
+ image_size: Variable to which image is resized.
284
+ train_batch_size: Training batch size.
285
+ test_batch_size: Testing batch size.
286
+ num_workers: Number of workers.
287
+ task: ``classification`` or ``segmentation``
288
+ transform_config_train: Config for pre-processing during training.
289
+ transform_config_val: Config for pre-processing during validation.
290
+ seed: seed used for the random subset splitting
291
+ create_validation_set: Create a validation subset in addition to the train and test subsets
292
+
293
+ Examples
294
+ >>> from anomalib.data import BTechDataModule
295
+ >>> datamodule = BTechDataModule(
296
+ ... root="./datasets/BTech",
297
+ ... category="leather",
298
+ ... image_size=256,
299
+ ... train_batch_size=32,
300
+ ... test_batch_size=32,
301
+ ... num_workers=8,
302
+ ... transform_config_train=None,
303
+ ... transform_config_val=None,
304
+ ... )
305
+ >>> datamodule.setup()
306
+
307
+ >>> i, data = next(enumerate(datamodule.train_dataloader()))
308
+ >>> data.keys()
309
+ dict_keys(['image'])
310
+ >>> data["image"].shape
311
+ torch.Size([32, 3, 256, 256])
312
+
313
+ >>> i, data = next(enumerate(datamodule.val_dataloader()))
314
+ >>> data.keys()
315
+ dict_keys(['image_path', 'label', 'mask_path', 'image', 'mask'])
316
+ >>> data["image"].shape, data["mask"].shape
317
+ (torch.Size([32, 3, 256, 256]), torch.Size([32, 256, 256]))
318
+ """
319
+ super().__init__()
320
+
321
+ self.root = root if isinstance(root, Path) else Path(root)
322
+ self.category = category
323
+ self.dataset_path = self.root / self.category
324
+ self.transform_config_train = transform_config_train
325
+ self.transform_config_val = transform_config_val
326
+ self.image_size = image_size
327
+
328
+ if self.transform_config_train is not None and self.transform_config_val is None:
329
+ self.transform_config_val = self.transform_config_train
330
+
331
+ self.pre_process_train = PreProcessor(config=self.transform_config_train, image_size=self.image_size)
332
+ self.pre_process_val = PreProcessor(config=self.transform_config_val, image_size=self.image_size)
333
+
334
+ self.train_batch_size = train_batch_size
335
+ self.test_batch_size = test_batch_size
336
+ self.num_workers = num_workers
337
+
338
+ self.create_validation_set = create_validation_set
339
+ self.task = task
340
+ self.seed = seed
341
+
342
+ self.train_data: Dataset
343
+ self.test_data: Dataset
344
+ if create_validation_set:
345
+ self.val_data: Dataset
346
+ self.inference_data: Dataset
347
+
348
+ def prepare_data(self) -> None:
349
+ """Download the dataset if not available."""
350
+ if (self.root / self.category).is_dir():
351
+ logger.info("Found the dataset.")
352
+ else:
353
+ zip_filename = self.root.parent / "btad.zip"
354
+
355
+ logger.info("Downloading the BTech dataset.")
356
+ with DownloadProgressBar(unit="B", unit_scale=True, miniters=1, desc="BTech") as progress_bar:
357
+ urlretrieve(
358
+ url="https://avires.dimi.uniud.it/papers/btad/btad.zip",
359
+ filename=zip_filename,
360
+ reporthook=progress_bar.update_to,
361
+ ) # nosec
362
+
363
+ logger.info("Extracting the dataset.")
364
+ with zipfile.ZipFile(zip_filename, "r") as zip_file:
365
+ zip_file.extractall(self.root.parent)
366
+
367
+ logger.info("Renaming the dataset directory")
368
+ shutil.move(src=str(self.root.parent / "BTech_Dataset_transformed"), dst=str(self.root))
369
+
370
+ # NOTE: Each BTech category has different image extension as follows
371
+ # | Category | Image | Mask |
372
+ # |----------|-------|------|
373
+ # | 01 | bmp | png |
374
+ # | 02 | png | png |
375
+ # | 03 | bmp | bmp |
376
+ # To avoid any conflict, the following script converts all the extensions to png.
377
+ # This solution works fine, but it's also possible to properly ready the bmp and
378
+ # png filenames from categories in `make_btech_dataset` function.
379
+ logger.info("Convert the bmp formats to png to have consistent image extensions")
380
+ for filename in tqdm(self.root.glob("**/*.bmp"), desc="Converting bmp to png"):
381
+ image = cv2.imread(str(filename))
382
+ cv2.imwrite(str(filename.with_suffix(".png")), image)
383
+ filename.unlink()
384
+
385
+ logger.info("Cleaning the tar file")
386
+ zip_filename.unlink()
387
+
388
+ def setup(self, stage: Optional[str] = None) -> None:
389
+ """Setup train, validation and test data.
390
+
391
+ BTech dataset uses BTech dataset structure, which is the reason for
392
+ using `anomalib.data.btech.BTech` class to get the dataset items.
393
+
394
+ Args:
395
+ stage: Optional[str]: Train/Val/Test stages. (Default value = None)
396
+
397
+ """
398
+ logger.info("Setting up train, validation, test and prediction datasets.")
399
+ if stage in (None, "fit"):
400
+ self.train_data = BTech(
401
+ root=self.root,
402
+ category=self.category,
403
+ pre_process=self.pre_process_train,
404
+ split="train",
405
+ task=self.task,
406
+ seed=self.seed,
407
+ create_validation_set=self.create_validation_set,
408
+ )
409
+
410
+ if self.create_validation_set:
411
+ self.val_data = BTech(
412
+ root=self.root,
413
+ category=self.category,
414
+ pre_process=self.pre_process_val,
415
+ split="val",
416
+ task=self.task,
417
+ seed=self.seed,
418
+ create_validation_set=self.create_validation_set,
419
+ )
420
+
421
+ self.test_data = BTech(
422
+ root=self.root,
423
+ category=self.category,
424
+ pre_process=self.pre_process_val,
425
+ split="test",
426
+ task=self.task,
427
+ seed=self.seed,
428
+ create_validation_set=self.create_validation_set,
429
+ )
430
+
431
+ if stage == "predict":
432
+ self.inference_data = InferenceDataset(
433
+ path=self.root, image_size=self.image_size, transform_config=self.transform_config_val
434
+ )
435
+
436
+ def train_dataloader(self) -> TRAIN_DATALOADERS:
437
+ """Get train dataloader."""
438
+ return DataLoader(self.train_data, shuffle=True, batch_size=self.train_batch_size, num_workers=self.num_workers)
439
+
440
+ def val_dataloader(self) -> EVAL_DATALOADERS:
441
+ """Get validation dataloader."""
442
+ dataset = self.val_data if self.create_validation_set else self.test_data
443
+ return DataLoader(dataset=dataset, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers)
444
+
445
+ def test_dataloader(self) -> EVAL_DATALOADERS:
446
+ """Get test dataloader."""
447
+ return DataLoader(self.test_data, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers)
448
+
449
+ def predict_dataloader(self) -> EVAL_DATALOADERS:
450
+ """Get predict dataloader."""
451
+ return DataLoader(
452
+ self.inference_data, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers
453
+ )
anomalib/data/folder.py ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Custom Folder Dataset.
2
+
3
+ This script creates a custom dataset from a folder.
4
+ """
5
+
6
+ # Copyright (C) 2020 Intel Corporation
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing,
15
+ # software distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions
18
+ # and limitations under the License.
19
+
20
+ import logging
21
+ import warnings
22
+ from pathlib import Path
23
+ from typing import Dict, Optional, Tuple, Union
24
+
25
+ import albumentations as A
26
+ import cv2
27
+ import numpy as np
28
+ from pandas.core.frame import DataFrame
29
+ from pytorch_lightning.core.datamodule import LightningDataModule
30
+ from pytorch_lightning.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS
31
+ from torch import Tensor
32
+ from torch.utils.data import DataLoader, Dataset
33
+ from torchvision.datasets.folder import IMG_EXTENSIONS
34
+
35
+ from anomalib.data.inference import InferenceDataset
36
+ from anomalib.data.utils import read_image
37
+ from anomalib.data.utils.split import (
38
+ create_validation_set_from_test_set,
39
+ split_normal_images_in_train_set,
40
+ )
41
+ from anomalib.pre_processing import PreProcessor
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ def _check_and_convert_path(path: Union[str, Path]) -> Path:
47
+ """Check an input path, and convert to Pathlib object.
48
+
49
+ Args:
50
+ path (Union[str, Path]): Input path.
51
+
52
+ Returns:
53
+ Path: Output path converted to pathlib object.
54
+ """
55
+ if not isinstance(path, Path):
56
+ path = Path(path)
57
+ return path
58
+
59
+
60
+ def _prepare_files_labels(
61
+ path: Union[str, Path], path_type: str, extensions: Optional[Tuple[str, ...]] = None
62
+ ) -> Tuple[list, list]:
63
+ """Return a list of filenames and list corresponding labels.
64
+
65
+ Args:
66
+ path (Union[str, Path]): Path to the directory containing images.
67
+ path_type (str): Type of images in the provided path ("normal", "abnormal", "normal_test")
68
+ extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the
69
+ directory.
70
+
71
+ Returns:
72
+ List, List: Filenames of the images provided in the paths, labels of the images provided in the paths
73
+ """
74
+ path = _check_and_convert_path(path)
75
+ if extensions is None:
76
+ extensions = IMG_EXTENSIONS
77
+
78
+ if isinstance(extensions, str):
79
+ extensions = (extensions,)
80
+
81
+ filenames = [f for f in path.glob(r"**/*") if f.suffix in extensions and not f.is_dir()]
82
+ if len(filenames) == 0:
83
+ raise RuntimeError(f"Found 0 {path_type} images in {path}")
84
+
85
+ labels = [path_type] * len(filenames)
86
+
87
+ return filenames, labels
88
+
89
+
90
+ def make_dataset(
91
+ normal_dir: Union[str, Path],
92
+ abnormal_dir: Union[str, Path],
93
+ normal_test_dir: Optional[Union[str, Path]] = None,
94
+ mask_dir: Optional[Union[str, Path]] = None,
95
+ split: Optional[str] = None,
96
+ split_ratio: float = 0.2,
97
+ seed: int = 0,
98
+ create_validation_set: bool = True,
99
+ extensions: Optional[Tuple[str, ...]] = None,
100
+ ):
101
+ """Make Folder Dataset.
102
+
103
+ Args:
104
+ normal_dir (Union[str, Path]): Path to the directory containing normal images.
105
+ abnormal_dir (Union[str, Path]): Path to the directory containing abnormal images.
106
+ normal_test_dir (Optional[Union[str, Path]], optional): Path to the directory containing
107
+ normal images for the test dataset. Normal test images will be a split of `normal_dir`
108
+ if `None`. Defaults to None.
109
+ mask_dir (Optional[Union[str, Path]], optional): Path to the directory containing
110
+ the mask annotations. Defaults to None.
111
+ split (Optional[str], optional): Dataset split (ie., either train or test). Defaults to None.
112
+ split_ratio (float, optional): Ratio to split normal training images and add to the
113
+ test set in case test set doesn't contain any normal images.
114
+ Defaults to 0.2.
115
+ seed (int, optional): Random seed to ensure reproducibility when splitting. Defaults to 0.
116
+ create_validation_set (bool, optional):Boolean to create a validation set from the test set.
117
+ Those wanting to create a validation set could set this flag to ``True``.
118
+ extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the
119
+ directory.
120
+
121
+ Returns:
122
+ DataFrame: an output dataframe containing samples for the requested split (ie., train or test)
123
+ """
124
+
125
+ filenames = []
126
+ labels = []
127
+ dirs = {"normal": normal_dir, "abnormal": abnormal_dir}
128
+
129
+ if normal_test_dir:
130
+ dirs = {**dirs, **{"normal_test": normal_test_dir}}
131
+
132
+ for dir_type, path in dirs.items():
133
+ filename, label = _prepare_files_labels(path, dir_type, extensions)
134
+ filenames += filename
135
+ labels += label
136
+
137
+ samples = DataFrame({"image_path": filenames, "label": labels})
138
+
139
+ # Create label index for normal (0) and abnormal (1) images.
140
+ samples.loc[(samples.label == "normal") | (samples.label == "normal_test"), "label_index"] = 0
141
+ samples.loc[(samples.label == "abnormal"), "label_index"] = 1
142
+ samples.label_index = samples.label_index.astype(int)
143
+
144
+ # If a path to mask is provided, add it to the sample dataframe.
145
+ if mask_dir is not None:
146
+ mask_dir = _check_and_convert_path(mask_dir)
147
+ samples["mask_path"] = ""
148
+ for index, row in samples.iterrows():
149
+ if row.label_index == 1:
150
+ samples.loc[index, "mask_path"] = str(mask_dir / row.image_path.name)
151
+
152
+ # Ensure the pathlib objects are converted to str.
153
+ # This is because torch dataloader doesn't like pathlib.
154
+ samples = samples.astype({"image_path": "str"})
155
+
156
+ # Create train/test split.
157
+ # By default, all the normal samples are assigned as train.
158
+ # and all the abnormal samples are test.
159
+ samples.loc[(samples.label == "normal"), "split"] = "train"
160
+ samples.loc[(samples.label == "abnormal") | (samples.label == "normal_test"), "split"] = "test"
161
+
162
+ if not normal_test_dir:
163
+ samples = split_normal_images_in_train_set(
164
+ samples=samples, split_ratio=split_ratio, seed=seed, normal_label="normal"
165
+ )
166
+
167
+ # If `create_validation_set` is set to True, the test set is split into half.
168
+ if create_validation_set:
169
+ samples = create_validation_set_from_test_set(samples, seed=seed, normal_label="normal")
170
+
171
+ # Get the data frame for the split.
172
+ if split is not None and split in ["train", "val", "test"]:
173
+ samples = samples[samples.split == split]
174
+ samples = samples.reset_index(drop=True)
175
+
176
+ return samples
177
+
178
+
179
+ class FolderDataset(Dataset):
180
+ """Folder Dataset."""
181
+
182
+ def __init__(
183
+ self,
184
+ normal_dir: Union[Path, str],
185
+ abnormal_dir: Union[Path, str],
186
+ split: str,
187
+ pre_process: PreProcessor,
188
+ normal_test_dir: Optional[Union[Path, str]] = None,
189
+ split_ratio: float = 0.2,
190
+ mask_dir: Optional[Union[Path, str]] = None,
191
+ extensions: Optional[Tuple[str, ...]] = None,
192
+ task: Optional[str] = None,
193
+ seed: int = 0,
194
+ create_validation_set: bool = False,
195
+ ) -> None:
196
+ """Create Folder Folder Dataset.
197
+
198
+ Args:
199
+ normal_dir (Union[str, Path]): Path to the directory containing normal images.
200
+ abnormal_dir (Union[str, Path]): Path to the directory containing abnormal images.
201
+ split (Optional[str], optional): Dataset split (ie., either train or test). Defaults to None.
202
+ pre_process (Optional[PreProcessor], optional): Image Pro-processor to apply transform.
203
+ Defaults to None.
204
+ normal_test_dir (Optional[Union[str, Path]], optional): Path to the directory containing
205
+ normal images for the test dataset. Defaults to None.
206
+ split_ratio (float, optional): Ratio to split normal training images and add to the
207
+ test set in case test set doesn't contain any normal images.
208
+ Defaults to 0.2.
209
+ mask_dir (Optional[Union[str, Path]], optional): Path to the directory containing
210
+ the mask annotations. Defaults to None.
211
+ extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the
212
+ directory.
213
+ task (Optional[str], optional): Task type. (classification or segmentation) Defaults to None.
214
+ seed (int, optional): Random seed to ensure reproducibility when splitting. Defaults to 0.
215
+ create_validation_set (bool, optional):Boolean to create a validation set from the test set.
216
+ Those wanting to create a validation set could set this flag to ``True``.
217
+
218
+ Raises:
219
+ ValueError: When task is set to classification and `mask_dir` is provided. When `mask_dir` is
220
+ provided, `task` should be set to `segmentation`.
221
+
222
+ """
223
+ self.split = split
224
+
225
+ if task == "segmentation" and mask_dir is None:
226
+ warnings.warn(
227
+ "Segmentation task is requested, but mask directory is not provided. "
228
+ "Classification is to be chosen if mask directory is not provided."
229
+ )
230
+ self.task = "classification"
231
+
232
+ if task == "classification" and mask_dir:
233
+ warnings.warn(
234
+ "Classification task is requested, but mask directory is provided. "
235
+ "Segmentation task is to be chosen if mask directory is provided."
236
+ )
237
+ self.task = "segmentation"
238
+
239
+ if task is None or mask_dir is None:
240
+ self.task = "classification"
241
+ else:
242
+ self.task = task
243
+
244
+ self.pre_process = pre_process
245
+ self.samples = make_dataset(
246
+ normal_dir=normal_dir,
247
+ abnormal_dir=abnormal_dir,
248
+ normal_test_dir=normal_test_dir,
249
+ mask_dir=mask_dir,
250
+ split=split,
251
+ split_ratio=split_ratio,
252
+ seed=seed,
253
+ create_validation_set=create_validation_set,
254
+ extensions=extensions,
255
+ )
256
+
257
+ def __len__(self) -> int:
258
+ """Get length of the dataset."""
259
+ return len(self.samples)
260
+
261
+ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]:
262
+ """Get dataset item for the index ``index``.
263
+
264
+ Args:
265
+ index (int): Index to get the item.
266
+
267
+ Returns:
268
+ Union[Dict[str, Tensor], Dict[str, Union[str, Tensor]]]: Dict of image tensor during training.
269
+ Otherwise, Dict containing image path, target path, image tensor, label and transformed bounding box.
270
+ """
271
+ item: Dict[str, Union[str, Tensor]] = {}
272
+
273
+ image_path = self.samples.image_path[index]
274
+ image = read_image(image_path)
275
+
276
+ pre_processed = self.pre_process(image=image)
277
+ item = {"image": pre_processed["image"]}
278
+
279
+ if self.split in ["val", "test"]:
280
+ label_index = self.samples.label_index[index]
281
+
282
+ item["image_path"] = image_path
283
+ item["label"] = label_index
284
+
285
+ if self.task == "segmentation":
286
+ mask_path = self.samples.mask_path[index]
287
+
288
+ # Only Anomalous (1) images has masks in MVTec AD dataset.
289
+ # Therefore, create empty mask for Normal (0) images.
290
+ if label_index == 0:
291
+ mask = np.zeros(shape=image.shape[:2])
292
+ else:
293
+ mask = cv2.imread(mask_path, flags=0) / 255.0
294
+
295
+ pre_processed = self.pre_process(image=image, mask=mask)
296
+
297
+ item["mask_path"] = mask_path
298
+ item["image"] = pre_processed["image"]
299
+ item["mask"] = pre_processed["mask"]
300
+
301
+ return item
302
+
303
+
304
+ class FolderDataModule(LightningDataModule):
305
+ """Folder Lightning Data Module."""
306
+
307
+ def __init__(
308
+ self,
309
+ root: Union[str, Path],
310
+ normal_dir: str = "normal",
311
+ abnormal_dir: str = "abnormal",
312
+ task: str = "classification",
313
+ normal_test_dir: Optional[Union[Path, str]] = None,
314
+ mask_dir: Optional[Union[Path, str]] = None,
315
+ extensions: Optional[Tuple[str, ...]] = None,
316
+ split_ratio: float = 0.2,
317
+ seed: int = 0,
318
+ image_size: Optional[Union[int, Tuple[int, int]]] = None,
319
+ train_batch_size: int = 32,
320
+ test_batch_size: int = 32,
321
+ num_workers: int = 8,
322
+ transform_config_train: Optional[Union[str, A.Compose]] = None,
323
+ transform_config_val: Optional[Union[str, A.Compose]] = None,
324
+ create_validation_set: bool = False,
325
+ ) -> None:
326
+ """Folder Dataset PL Datamodule.
327
+
328
+ Args:
329
+ root (Union[str, Path]): Path to the root folder containing normal and abnormal dirs.
330
+ normal_dir (str, optional): Name of the directory containing normal images.
331
+ Defaults to "normal".
332
+ abnormal_dir (str, optional): Name of the directory containing abnormal images.
333
+ Defaults to "abnormal".
334
+ task (str, optional): Task type. Could be either classification or segmentation.
335
+ Defaults to "classification".
336
+ normal_test_dir (Optional[Union[str, Path]], optional): Path to the directory containing
337
+ normal images for the test dataset. Defaults to None.
338
+ mask_dir (Optional[Union[str, Path]], optional): Path to the directory containing
339
+ the mask annotations. Defaults to None.
340
+ extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the
341
+ directory. Defaults to None.
342
+ split_ratio (float, optional): Ratio to split normal training images and add to the
343
+ test set in case test set doesn't contain any normal images.
344
+ Defaults to 0.2.
345
+ seed (int, optional): Random seed to ensure reproducibility when splitting. Defaults to 0.
346
+ image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image.
347
+ Defaults to None.
348
+ train_batch_size (int, optional): Training batch size. Defaults to 32.
349
+ test_batch_size (int, optional): Test batch size. Defaults to 32.
350
+ num_workers (int, optional): Number of workers. Defaults to 8.
351
+ transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing
352
+ during training.
353
+ Defaults to None.
354
+ transform_config_val (Optional[Union[str, A.Compose]], optional): Config for pre-processing
355
+ during validation.
356
+ Defaults to None.
357
+ create_validation_set (bool, optional):Boolean to create a validation set from the test set.
358
+ Those wanting to create a validation set could set this flag to ``True``.
359
+
360
+ Examples:
361
+ Assume that we use Folder Dataset for the MVTec/bottle/broken_large category. We would do:
362
+ >>> from anomalib.data import FolderDataModule
363
+ >>> datamodule = FolderDataModule(
364
+ ... root="./datasets/MVTec/bottle/test",
365
+ ... normal="good",
366
+ ... abnormal="broken_large",
367
+ ... image_size=256
368
+ ... )
369
+ >>> datamodule.setup()
370
+ >>> i, data = next(enumerate(datamodule.train_dataloader()))
371
+ >>> data["image"].shape
372
+ torch.Size([16, 3, 256, 256])
373
+
374
+ >>> i, test_data = next(enumerate(datamodule.test_dataloader()))
375
+ >>> test_data.keys()
376
+ dict_keys(['image'])
377
+
378
+ We could also create a Folder DataModule for datasets containing mask annotations.
379
+ The dataset expects that mask annotation filenames must be same as the original filename.
380
+ To this end, we modified mask filenames in MVTec AD bottle category.
381
+ Now we could try folder data module using the mvtec bottle broken large category
382
+ >>> datamodule = FolderDataModule(
383
+ ... root="./datasets/bottle/test",
384
+ ... normal="good",
385
+ ... abnormal="broken_large",
386
+ ... mask_dir="./datasets/bottle/ground_truth/broken_large",
387
+ ... image_size=256
388
+ ... )
389
+
390
+ >>> i , train_data = next(enumerate(datamodule.train_dataloader()))
391
+ >>> train_data.keys()
392
+ dict_keys(['image'])
393
+ >>> train_data["image"].shape
394
+ torch.Size([16, 3, 256, 256])
395
+
396
+ >>> i, test_data = next(enumerate(datamodule.test_dataloader()))
397
+ dict_keys(['image_path', 'label', 'mask_path', 'image', 'mask'])
398
+ >>> print(test_data["image"].shape, test_data["mask"].shape)
399
+ torch.Size([24, 3, 256, 256]) torch.Size([24, 256, 256])
400
+
401
+ By default, Folder Data Module does not create a validation set. If a validation set
402
+ is needed it could be set as follows:
403
+
404
+ >>> datamodule = FolderDataModule(
405
+ ... root="./datasets/bottle/test",
406
+ ... normal="good",
407
+ ... abnormal="broken_large",
408
+ ... mask_dir="./datasets/bottle/ground_truth/broken_large",
409
+ ... image_size=256,
410
+ ... create_validation_set=True,
411
+ ... )
412
+
413
+ >>> i, val_data = next(enumerate(datamodule.val_dataloader()))
414
+ >>> val_data.keys()
415
+ dict_keys(['image_path', 'label', 'mask_path', 'image', 'mask'])
416
+ >>> print(val_data["image"].shape, val_data["mask"].shape)
417
+ torch.Size([12, 3, 256, 256]) torch.Size([12, 256, 256])
418
+
419
+ >>> i, test_data = next(enumerate(datamodule.test_dataloader()))
420
+ >>> print(test_data["image"].shape, test_data["mask"].shape)
421
+ torch.Size([12, 3, 256, 256]) torch.Size([12, 256, 256])
422
+
423
+ """
424
+ super().__init__()
425
+
426
+ self.root = _check_and_convert_path(root)
427
+ self.normal_dir = self.root / normal_dir
428
+ self.abnormal_dir = self.root / abnormal_dir
429
+ self.normal_test = normal_test_dir
430
+ if normal_test_dir:
431
+ self.normal_test = self.root / normal_test_dir
432
+ self.mask_dir = mask_dir
433
+ self.extensions = extensions
434
+ self.split_ratio = split_ratio
435
+
436
+ if task == "classification" and mask_dir is not None:
437
+ raise ValueError(
438
+ "Classification type is set but mask_dir provided. "
439
+ "If mask_dir is provided task type must be segmentation. "
440
+ "Check your configuration."
441
+ )
442
+ self.task = task
443
+ self.transform_config_train = transform_config_train
444
+ self.transform_config_val = transform_config_val
445
+ self.image_size = image_size
446
+
447
+ if self.transform_config_train is not None and self.transform_config_val is None:
448
+ self.transform_config_val = self.transform_config_train
449
+
450
+ self.pre_process_train = PreProcessor(config=self.transform_config_train, image_size=self.image_size)
451
+ self.pre_process_val = PreProcessor(config=self.transform_config_val, image_size=self.image_size)
452
+
453
+ self.train_batch_size = train_batch_size
454
+ self.test_batch_size = test_batch_size
455
+ self.num_workers = num_workers
456
+
457
+ self.create_validation_set = create_validation_set
458
+ self.seed = seed
459
+
460
+ self.train_data: Dataset
461
+ self.test_data: Dataset
462
+ if create_validation_set:
463
+ self.val_data: Dataset
464
+ self.inference_data: Dataset
465
+
466
+ def setup(self, stage: Optional[str] = None) -> None:
467
+ """Setup train, validation and test data.
468
+
469
+ Args:
470
+ stage: Optional[str]: Train/Val/Test stages. (Default value = None)
471
+
472
+ """
473
+ logger.info("Setting up train, validation, test and prediction datasets.")
474
+ if stage in (None, "fit"):
475
+ self.train_data = FolderDataset(
476
+ normal_dir=self.normal_dir,
477
+ abnormal_dir=self.abnormal_dir,
478
+ normal_test_dir=self.normal_test,
479
+ split="train",
480
+ split_ratio=self.split_ratio,
481
+ mask_dir=self.mask_dir,
482
+ pre_process=self.pre_process_train,
483
+ extensions=self.extensions,
484
+ task=self.task,
485
+ seed=self.seed,
486
+ create_validation_set=self.create_validation_set,
487
+ )
488
+
489
+ if self.create_validation_set:
490
+ self.val_data = FolderDataset(
491
+ normal_dir=self.normal_dir,
492
+ abnormal_dir=self.abnormal_dir,
493
+ normal_test_dir=self.normal_test,
494
+ split="val",
495
+ split_ratio=self.split_ratio,
496
+ mask_dir=self.mask_dir,
497
+ pre_process=self.pre_process_val,
498
+ extensions=self.extensions,
499
+ task=self.task,
500
+ seed=self.seed,
501
+ create_validation_set=self.create_validation_set,
502
+ )
503
+
504
+ self.test_data = FolderDataset(
505
+ normal_dir=self.normal_dir,
506
+ abnormal_dir=self.abnormal_dir,
507
+ split="test",
508
+ normal_test_dir=self.normal_test,
509
+ split_ratio=self.split_ratio,
510
+ mask_dir=self.mask_dir,
511
+ pre_process=self.pre_process_val,
512
+ extensions=self.extensions,
513
+ task=self.task,
514
+ seed=self.seed,
515
+ create_validation_set=self.create_validation_set,
516
+ )
517
+
518
+ if stage == "predict":
519
+ self.inference_data = InferenceDataset(
520
+ path=self.root, image_size=self.image_size, transform_config=self.transform_config_val
521
+ )
522
+
523
+ def train_dataloader(self) -> TRAIN_DATALOADERS:
524
+ """Get train dataloader."""
525
+ return DataLoader(self.train_data, shuffle=True, batch_size=self.train_batch_size, num_workers=self.num_workers)
526
+
527
+ def val_dataloader(self) -> EVAL_DATALOADERS:
528
+ """Get validation dataloader."""
529
+ dataset = self.val_data if self.create_validation_set else self.test_data
530
+ return DataLoader(dataset=dataset, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers)
531
+
532
+ def test_dataloader(self) -> EVAL_DATALOADERS:
533
+ """Get test dataloader."""
534
+ return DataLoader(self.test_data, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers)
535
+
536
+ def predict_dataloader(self) -> EVAL_DATALOADERS:
537
+ """Get predict dataloader."""
538
+ return DataLoader(
539
+ self.inference_data, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers
540
+ )
anomalib/data/inference.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference Dataset."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from pathlib import Path
18
+ from typing import Any, Optional, Tuple, Union
19
+
20
+ import albumentations as A
21
+ from torch.utils.data.dataset import Dataset
22
+
23
+ from anomalib.data.utils import get_image_filenames, read_image
24
+ from anomalib.pre_processing import PreProcessor
25
+
26
+
27
+ class InferenceDataset(Dataset):
28
+ """Inference Dataset to perform prediction."""
29
+
30
+ def __init__(
31
+ self,
32
+ path: Union[str, Path],
33
+ pre_process: Optional[PreProcessor] = None,
34
+ image_size: Optional[Union[int, Tuple[int, int]]] = None,
35
+ transform_config: Optional[Union[str, A.Compose]] = None,
36
+ ) -> None:
37
+ """Inference Dataset to perform prediction.
38
+
39
+ Args:
40
+ path (Union[str, Path]): Path to an image or image-folder.
41
+ pre_process (Optional[PreProcessor], optional): Pre-Processing transforms to
42
+ pre-process the input dataset. Defaults to None.
43
+ image_size (Optional[Union[int, Tuple[int, int]]], optional): Target image size
44
+ to resize the original image. Defaults to None.
45
+ transform_config (Optional[Union[str, A.Compose]], optional): Configuration file
46
+ parse the albumentation transforms. Defaults to None.
47
+ """
48
+ super().__init__()
49
+
50
+ self.image_filenames = get_image_filenames(path)
51
+
52
+ if pre_process is None:
53
+ self.pre_process = PreProcessor(transform_config, image_size)
54
+ else:
55
+ self.pre_process = pre_process
56
+
57
+ def __len__(self) -> int:
58
+ """Get the number of images in the given path."""
59
+ return len(self.image_filenames)
60
+
61
+ def __getitem__(self, index: int) -> Any:
62
+ """Get the image based on the `index`."""
63
+ image_filename = self.image_filenames[index]
64
+ image = read_image(path=image_filename)
65
+ pre_processed = self.pre_process(image=image)
66
+
67
+ return pre_processed
anomalib/data/mvtec.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MVTec AD Dataset (CC BY-NC-SA 4.0).
2
+
3
+ Description:
4
+ This script contains PyTorch Dataset, Dataloader and PyTorch
5
+ Lightning DataModule for the MVTec AD dataset.
6
+
7
+ If the dataset is not on the file system, the script downloads and
8
+ extracts the dataset and create PyTorch data objects.
9
+
10
+ License:
11
+ MVTec AD dataset is released under the Creative Commons
12
+ Attribution-NonCommercial-ShareAlike 4.0 International License
13
+ (CC BY-NC-SA 4.0)(https://creativecommons.org/licenses/by-nc-sa/4.0/).
14
+
15
+ Reference:
16
+ - Paul Bergmann, Kilian Batzner, Michael Fauser, David Sattlegger, Carsten Steger:
17
+ The MVTec Anomaly Detection Dataset: A Comprehensive Real-World Dataset for
18
+ Unsupervised Anomaly Detection; in: International Journal of Computer Vision
19
+ 129(4):1038-1059, 2021, DOI: 10.1007/s11263-020-01400-4.
20
+
21
+ - Paul Bergmann, Michael Fauser, David Sattlegger, Carsten Steger: MVTec AD —
22
+ A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection;
23
+ in: IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR),
24
+ 9584-9592, 2019, DOI: 10.1109/CVPR.2019.00982.
25
+ """
26
+
27
+ # Copyright (C) 2020 Intel Corporation
28
+ #
29
+ # Licensed under the Apache License, Version 2.0 (the "License");
30
+ # you may not use this file except in compliance with the License.
31
+ # You may obtain a copy of the License at
32
+ #
33
+ # http://www.apache.org/licenses/LICENSE-2.0
34
+ #
35
+ # Unless required by applicable law or agreed to in writing,
36
+ # software distributed under the License is distributed on an "AS IS" BASIS,
37
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38
+ # See the License for the specific language governing permissions
39
+ # and limitations under the License.
40
+
41
+ import logging
42
+ import tarfile
43
+ from pathlib import Path
44
+ from typing import Dict, Optional, Tuple, Union
45
+ from urllib.request import urlretrieve
46
+
47
+ import albumentations as A
48
+ import cv2
49
+ import numpy as np
50
+ import pandas as pd
51
+ from pandas.core.frame import DataFrame
52
+ from pytorch_lightning.core.datamodule import LightningDataModule
53
+ from pytorch_lightning.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS
54
+ from torch import Tensor
55
+ from torch.utils.data import DataLoader
56
+ from torch.utils.data.dataset import Dataset
57
+ from torchvision.datasets.folder import VisionDataset
58
+
59
+ from anomalib.data.inference import InferenceDataset
60
+ from anomalib.data.utils import DownloadProgressBar, read_image
61
+ from anomalib.data.utils.split import (
62
+ create_validation_set_from_test_set,
63
+ split_normal_images_in_train_set,
64
+ )
65
+ from anomalib.pre_processing import PreProcessor
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+
70
+ def make_mvtec_dataset(
71
+ path: Path,
72
+ split: Optional[str] = None,
73
+ split_ratio: float = 0.1,
74
+ seed: int = 0,
75
+ create_validation_set: bool = False,
76
+ ) -> DataFrame:
77
+ """Create MVTec AD samples by parsing the MVTec AD data file structure.
78
+
79
+ The files are expected to follow the structure:
80
+ path/to/dataset/split/category/image_filename.png
81
+ path/to/dataset/ground_truth/category/mask_filename.png
82
+
83
+ This function creates a dataframe to store the parsed information based on the following format:
84
+ |---|---------------|-------|---------|---------------|---------------------------------------|-------------|
85
+ | | path | split | label | image_path | mask_path | label_index |
86
+ |---|---------------|-------|---------|---------------|---------------------------------------|-------------|
87
+ | 0 | datasets/name | test | defect | filename.png | ground_truth/defect/filename_mask.png | 1 |
88
+ |---|---------------|-------|---------|---------------|---------------------------------------|-------------|
89
+
90
+ Args:
91
+ path (Path): Path to dataset
92
+ split (str, optional): Dataset split (ie., either train or test). Defaults to None.
93
+ split_ratio (float, optional): Ratio to split normal training images and add to the
94
+ test set in case test set doesn't contain any normal images.
95
+ Defaults to 0.1.
96
+ seed (int, optional): Random seed to ensure reproducibility when splitting. Defaults to 0.
97
+ create_validation_set (bool, optional): Boolean to create a validation set from the test set.
98
+ MVTec AD dataset does not contain a validation set. Those wanting to create a validation set
99
+ could set this flag to ``True``.
100
+
101
+ Example:
102
+ The following example shows how to get training samples from MVTec AD bottle category:
103
+
104
+ >>> root = Path('./MVTec')
105
+ >>> category = 'bottle'
106
+ >>> path = root / category
107
+ >>> path
108
+ PosixPath('MVTec/bottle')
109
+
110
+ >>> samples = make_mvtec_dataset(path, split='train', split_ratio=0.1, seed=0)
111
+ >>> samples.head()
112
+ path split label image_path mask_path label_index
113
+ 0 MVTec/bottle train good MVTec/bottle/train/good/105.png MVTec/bottle/ground_truth/good/105_mask.png 0
114
+ 1 MVTec/bottle train good MVTec/bottle/train/good/017.png MVTec/bottle/ground_truth/good/017_mask.png 0
115
+ 2 MVTec/bottle train good MVTec/bottle/train/good/137.png MVTec/bottle/ground_truth/good/137_mask.png 0
116
+ 3 MVTec/bottle train good MVTec/bottle/train/good/152.png MVTec/bottle/ground_truth/good/152_mask.png 0
117
+ 4 MVTec/bottle train good MVTec/bottle/train/good/109.png MVTec/bottle/ground_truth/good/109_mask.png 0
118
+
119
+ Returns:
120
+ DataFrame: an output dataframe containing samples for the requested split (ie., train or test)
121
+ """
122
+ samples_list = [(str(path),) + filename.parts[-3:] for filename in path.glob("**/*.png")]
123
+ if len(samples_list) == 0:
124
+ raise RuntimeError(f"Found 0 images in {path}")
125
+
126
+ samples = pd.DataFrame(samples_list, columns=["path", "split", "label", "image_path"])
127
+ samples = samples[samples.split != "ground_truth"]
128
+
129
+ # Create mask_path column
130
+ samples["mask_path"] = (
131
+ samples.path
132
+ + "/ground_truth/"
133
+ + samples.label
134
+ + "/"
135
+ + samples.image_path.str.rstrip("png").str.rstrip(".")
136
+ + "_mask.png"
137
+ )
138
+
139
+ # Modify image_path column by converting to absolute path
140
+ samples["image_path"] = samples.path + "/" + samples.split + "/" + samples.label + "/" + samples.image_path
141
+
142
+ # Split the normal images in training set if test set doesn't
143
+ # contain any normal images. This is needed because AUC score
144
+ # cannot be computed based on 1-class
145
+ if sum((samples.split == "test") & (samples.label == "good")) == 0:
146
+ samples = split_normal_images_in_train_set(samples, split_ratio, seed)
147
+
148
+ # Good images don't have mask
149
+ samples.loc[(samples.split == "test") & (samples.label == "good"), "mask_path"] = ""
150
+
151
+ # Create label index for normal (0) and anomalous (1) images.
152
+ samples.loc[(samples.label == "good"), "label_index"] = 0
153
+ samples.loc[(samples.label != "good"), "label_index"] = 1
154
+ samples.label_index = samples.label_index.astype(int)
155
+
156
+ if create_validation_set:
157
+ samples = create_validation_set_from_test_set(samples, seed=seed)
158
+
159
+ # Get the data frame for the split.
160
+ if split is not None and split in ["train", "val", "test"]:
161
+ samples = samples[samples.split == split]
162
+ samples = samples.reset_index(drop=True)
163
+
164
+ return samples
165
+
166
+
167
+ class MVTec(VisionDataset):
168
+ """MVTec AD PyTorch Dataset."""
169
+
170
+ def __init__(
171
+ self,
172
+ root: Union[Path, str],
173
+ category: str,
174
+ pre_process: PreProcessor,
175
+ split: str,
176
+ task: str = "segmentation",
177
+ seed: int = 0,
178
+ create_validation_set: bool = False,
179
+ ) -> None:
180
+ """Mvtec AD Dataset class.
181
+
182
+ Args:
183
+ root: Path to the MVTec AD dataset
184
+ category: Name of the MVTec AD category.
185
+ pre_process: List of pre_processing object containing albumentation compose.
186
+ split: 'train', 'val' or 'test'
187
+ task: ``classification`` or ``segmentation``
188
+ seed: seed used for the random subset splitting
189
+ create_validation_set: Create a validation subset in addition to the train and test subsets
190
+
191
+ Examples:
192
+ >>> from anomalib.data.mvtec import MVTec
193
+ >>> from anomalib.data.transforms import PreProcessor
194
+ >>> pre_process = PreProcessor(image_size=256)
195
+ >>> dataset = MVTec(
196
+ ... root='./datasets/MVTec',
197
+ ... category='leather',
198
+ ... pre_process=pre_process,
199
+ ... task="classification",
200
+ ... is_train=True,
201
+ ... )
202
+ >>> dataset[0].keys()
203
+ dict_keys(['image'])
204
+
205
+ >>> dataset.split = "test"
206
+ >>> dataset[0].keys()
207
+ dict_keys(['image', 'image_path', 'label'])
208
+
209
+ >>> dataset.task = "segmentation"
210
+ >>> dataset.split = "train"
211
+ >>> dataset[0].keys()
212
+ dict_keys(['image'])
213
+
214
+ >>> dataset.split = "test"
215
+ >>> dataset[0].keys()
216
+ dict_keys(['image_path', 'label', 'mask_path', 'image', 'mask'])
217
+
218
+ >>> dataset[0]["image"].shape, dataset[0]["mask"].shape
219
+ (torch.Size([3, 256, 256]), torch.Size([256, 256]))
220
+ """
221
+ super().__init__(root)
222
+ self.root = Path(root) if isinstance(root, str) else root
223
+ self.category: str = category
224
+ self.split = split
225
+ self.task = task
226
+
227
+ self.pre_process = pre_process
228
+
229
+ self.samples = make_mvtec_dataset(
230
+ path=self.root / category,
231
+ split=self.split,
232
+ seed=seed,
233
+ create_validation_set=create_validation_set,
234
+ )
235
+
236
+ def __len__(self) -> int:
237
+ """Get length of the dataset."""
238
+ return len(self.samples)
239
+
240
+ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]:
241
+ """Get dataset item for the index ``index``.
242
+
243
+ Args:
244
+ index (int): Index to get the item.
245
+
246
+ Returns:
247
+ Union[Dict[str, Tensor], Dict[str, Union[str, Tensor]]]: Dict of image tensor during training.
248
+ Otherwise, Dict containing image path, target path, image tensor, label and transformed bounding box.
249
+ """
250
+ item: Dict[str, Union[str, Tensor]] = {}
251
+
252
+ image_path = self.samples.image_path[index]
253
+ image = read_image(image_path)
254
+
255
+ pre_processed = self.pre_process(image=image)
256
+ item = {"image": pre_processed["image"]}
257
+
258
+ if self.split in ["val", "test"]:
259
+ label_index = self.samples.label_index[index]
260
+
261
+ item["image_path"] = image_path
262
+ item["label"] = label_index
263
+
264
+ if self.task == "segmentation":
265
+ mask_path = self.samples.mask_path[index]
266
+
267
+ # Only Anomalous (1) images has masks in MVTec AD dataset.
268
+ # Therefore, create empty mask for Normal (0) images.
269
+ if label_index == 0:
270
+ mask = np.zeros(shape=image.shape[:2])
271
+ else:
272
+ mask = cv2.imread(mask_path, flags=0) / 255.0
273
+
274
+ pre_processed = self.pre_process(image=image, mask=mask)
275
+
276
+ item["mask_path"] = mask_path
277
+ item["image"] = pre_processed["image"]
278
+ item["mask"] = pre_processed["mask"]
279
+
280
+ return item
281
+
282
+
283
+ class MVTecDataModule(LightningDataModule):
284
+ """MVTec AD Lightning Data Module."""
285
+
286
+ def __init__(
287
+ self,
288
+ root: str,
289
+ category: str,
290
+ # TODO: Remove default values. IAAALD-211
291
+ image_size: Optional[Union[int, Tuple[int, int]]] = None,
292
+ train_batch_size: int = 32,
293
+ test_batch_size: int = 32,
294
+ num_workers: int = 8,
295
+ task: str = "segmentation",
296
+ transform_config_train: Optional[Union[str, A.Compose]] = None,
297
+ transform_config_val: Optional[Union[str, A.Compose]] = None,
298
+ seed: int = 0,
299
+ create_validation_set: bool = False,
300
+ ) -> None:
301
+ """Mvtec AD Lightning Data Module.
302
+
303
+ Args:
304
+ root: Path to the MVTec AD dataset
305
+ category: Name of the MVTec AD category.
306
+ image_size: Variable to which image is resized.
307
+ train_batch_size: Training batch size.
308
+ test_batch_size: Testing batch size.
309
+ num_workers: Number of workers.
310
+ task: ``classification`` or ``segmentation``
311
+ transform_config_train: Config for pre-processing during training.
312
+ transform_config_val: Config for pre-processing during validation.
313
+ seed: seed used for the random subset splitting
314
+ create_validation_set: Create a validation subset in addition to the train and test subsets
315
+
316
+ Examples
317
+ >>> from anomalib.data import MVTecDataModule
318
+ >>> datamodule = MVTecDataModule(
319
+ ... root="./datasets/MVTec",
320
+ ... category="leather",
321
+ ... image_size=256,
322
+ ... train_batch_size=32,
323
+ ... test_batch_size=32,
324
+ ... num_workers=8,
325
+ ... transform_config_train=None,
326
+ ... transform_config_val=None,
327
+ ... )
328
+ >>> datamodule.setup()
329
+
330
+ >>> i, data = next(enumerate(datamodule.train_dataloader()))
331
+ >>> data.keys()
332
+ dict_keys(['image'])
333
+ >>> data["image"].shape
334
+ torch.Size([32, 3, 256, 256])
335
+
336
+ >>> i, data = next(enumerate(datamodule.val_dataloader()))
337
+ >>> data.keys()
338
+ dict_keys(['image_path', 'label', 'mask_path', 'image', 'mask'])
339
+ >>> data["image"].shape, data["mask"].shape
340
+ (torch.Size([32, 3, 256, 256]), torch.Size([32, 256, 256]))
341
+ """
342
+ super().__init__()
343
+
344
+ self.root = root if isinstance(root, Path) else Path(root)
345
+ self.category = category
346
+ self.dataset_path = self.root / self.category
347
+ self.transform_config_train = transform_config_train
348
+ self.transform_config_val = transform_config_val
349
+ self.image_size = image_size
350
+
351
+ if self.transform_config_train is not None and self.transform_config_val is None:
352
+ self.transform_config_val = self.transform_config_train
353
+
354
+ self.pre_process_train = PreProcessor(config=self.transform_config_train, image_size=self.image_size)
355
+ self.pre_process_val = PreProcessor(config=self.transform_config_val, image_size=self.image_size)
356
+
357
+ self.train_batch_size = train_batch_size
358
+ self.test_batch_size = test_batch_size
359
+ self.num_workers = num_workers
360
+
361
+ self.create_validation_set = create_validation_set
362
+ self.task = task
363
+ self.seed = seed
364
+
365
+ self.train_data: Dataset
366
+ self.test_data: Dataset
367
+ if create_validation_set:
368
+ self.val_data: Dataset
369
+ self.inference_data: Dataset
370
+
371
+ def prepare_data(self) -> None:
372
+ """Download the dataset if not available."""
373
+ if (self.root / self.category).is_dir():
374
+ logger.info("Found the dataset.")
375
+ else:
376
+ self.root.mkdir(parents=True, exist_ok=True)
377
+
378
+ logger.info("Downloading the Mvtec AD dataset.")
379
+ url = "https://www.mydrive.ch/shares/38536/3830184030e49fe74747669442f0f282/download/420938113-1629952094"
380
+ dataset_name = "mvtec_anomaly_detection.tar.xz"
381
+ with DownloadProgressBar(unit="B", unit_scale=True, miniters=1, desc="MVTec AD") as progress_bar:
382
+ urlretrieve(
383
+ url=f"{url}/{dataset_name}",
384
+ filename=self.root / dataset_name,
385
+ reporthook=progress_bar.update_to,
386
+ )
387
+
388
+ logger.info("Extracting the dataset.")
389
+ with tarfile.open(self.root / dataset_name) as tar_file:
390
+ tar_file.extractall(self.root)
391
+
392
+ logger.info("Cleaning the tar file")
393
+ (self.root / dataset_name).unlink()
394
+
395
+ def setup(self, stage: Optional[str] = None) -> None:
396
+ """Setup train, validation and test data.
397
+
398
+ Args:
399
+ stage: Optional[str]: Train/Val/Test stages. (Default value = None)
400
+
401
+ """
402
+ logger.info("Setting up train, validation, test and prediction datasets.")
403
+ if stage in (None, "fit"):
404
+ self.train_data = MVTec(
405
+ root=self.root,
406
+ category=self.category,
407
+ pre_process=self.pre_process_train,
408
+ split="train",
409
+ task=self.task,
410
+ seed=self.seed,
411
+ create_validation_set=self.create_validation_set,
412
+ )
413
+
414
+ if self.create_validation_set:
415
+ self.val_data = MVTec(
416
+ root=self.root,
417
+ category=self.category,
418
+ pre_process=self.pre_process_val,
419
+ split="val",
420
+ task=self.task,
421
+ seed=self.seed,
422
+ create_validation_set=self.create_validation_set,
423
+ )
424
+
425
+ self.test_data = MVTec(
426
+ root=self.root,
427
+ category=self.category,
428
+ pre_process=self.pre_process_val,
429
+ split="test",
430
+ task=self.task,
431
+ seed=self.seed,
432
+ create_validation_set=self.create_validation_set,
433
+ )
434
+
435
+ if stage == "predict":
436
+ self.inference_data = InferenceDataset(
437
+ path=self.root, image_size=self.image_size, transform_config=self.transform_config_val
438
+ )
439
+
440
+ def train_dataloader(self) -> TRAIN_DATALOADERS:
441
+ """Get train dataloader."""
442
+ return DataLoader(self.train_data, shuffle=True, batch_size=self.train_batch_size, num_workers=self.num_workers)
443
+
444
+ def val_dataloader(self) -> EVAL_DATALOADERS:
445
+ """Get validation dataloader."""
446
+ dataset = self.val_data if self.create_validation_set else self.test_data
447
+ return DataLoader(dataset=dataset, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers)
448
+
449
+ def test_dataloader(self) -> EVAL_DATALOADERS:
450
+ """Get test dataloader."""
451
+ return DataLoader(self.test_data, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers)
452
+
453
+ def predict_dataloader(self) -> EVAL_DATALOADERS:
454
+ """Get predict dataloader."""
455
+ return DataLoader(
456
+ self.inference_data, shuffle=False, batch_size=self.test_batch_size, num_workers=self.num_workers
457
+ )
anomalib/data/utils/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper utilities for data."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .download import DownloadProgressBar
18
+ from .image import get_image_filenames, read_image
19
+
20
+ __all__ = ["get_image_filenames", "read_image", "DownloadProgressBar"]
anomalib/data/utils/download.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper to show progress bars with `urlretrieve`.
2
+
3
+ Based on https://stackoverflow.com/a/53877507
4
+ """
5
+
6
+ # Copyright (C) 2020 Intel Corporation
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing,
15
+ # software distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions
18
+ # and limitations under the License.
19
+
20
+ import io
21
+ from typing import Dict, Iterable, Optional, Union
22
+
23
+ from tqdm import tqdm
24
+
25
+
26
+ class DownloadProgressBar(tqdm):
27
+ """Create progress bar for urlretrieve. Subclasses `tqdm`.
28
+
29
+ For information about the parameters in constructor, refer to `tqdm`'s documentation.
30
+
31
+ Args:
32
+ iterable (Optional[Iterable]): Iterable to decorate with a progressbar.
33
+ Leave blank to manually manage the updates.
34
+ desc (Optional[str]): Prefix for the progressbar.
35
+ total (Optional[Union[int, float]]): The number of expected iterations. If unspecified,
36
+ len(iterable) is used if possible. If float("inf") or as a last
37
+ resort, only basic progress statistics are displayed
38
+ (no ETA, no progressbar).
39
+ If `gui` is True and this parameter needs subsequent updating,
40
+ specify an initial arbitrary large positive number,
41
+ e.g. 9e9.
42
+ leave (Optional[bool]): upon termination of iteration. If `None`, will leave only if `position` is `0`.
43
+ file (Optional[Union[io.TextIOWrapper, io.StringIO]]): Specifies where to output the progress messages
44
+ (default: sys.stderr). Uses `file.write(str)` and
45
+ `file.flush()` methods. For encoding, see
46
+ `write_bytes`.
47
+ ncols (Optional[int]): The width of the entire output message. If specified,
48
+ dynamically resizes the progressbar to stay within this bound.
49
+ If unspecified, attempts to use environment width. The
50
+ fallback is a meter width of 10 and no limit for the counter and
51
+ statistics. If 0, will not print any meter (only stats).
52
+ mininterval (Optional[float]): Minimum progress display update interval [default: 0.1] seconds.
53
+ maxinterval (Optional[float]): Maximum progress display update interval [default: 10] seconds.
54
+ Automatically adjusts `miniters` to correspond to `mininterval`
55
+ after long display update lag. Only works if `dynamic_miniters`
56
+ or monitor thread is enabled.
57
+ miniters (Optional[Union[int, float]]): Minimum progress display update interval, in iterations.
58
+ If 0 and `dynamic_miniters`, will automatically adjust to equal
59
+ `mininterval` (more CPU efficient, good for tight loops).
60
+ If > 0, will skip display of specified number of iterations.
61
+ Tweak this and `mininterval` to get very efficient loops.
62
+ If your progress is erratic with both fast and slow iterations
63
+ (network, skipping items, etc) you should set miniters=1.
64
+ use_ascii (Optional[Union[bool, str]]): If unspecified or False, use unicode (smooth blocks) to fill
65
+ the meter. The fallback is to use ASCII characters " 123456789#".
66
+ disable (Optional[bool]): Whether to disable the entire progressbar wrapper
67
+ [default: False]. If set to None, disable on non-TTY.
68
+ unit (Optional[str]): String that will be used to define the unit of each iteration
69
+ [default: it].
70
+ unit_scale (Union[bool, int, float]): If 1 or True, the number of iterations will be reduced/scaled
71
+ automatically and a metric prefix following the
72
+ International System of Units standard will be added
73
+ (kilo, mega, etc.) [default: False]. If any other non-zero
74
+ number, will scale `total` and `n`.
75
+ dynamic_ncols (Optional[bool]): If set, constantly alters `ncols` and `nrows` to the
76
+ environment (allowing for window resizes) [default: False].
77
+ smoothing (Optional[float]): Exponential moving average smoothing factor for speed estimates
78
+ (ignored in GUI mode). Ranges from 0 (average speed) to 1
79
+ (current/instantaneous speed) [default: 0.3].
80
+ bar_format (Optional[str]): Specify a custom bar string formatting. May impact performance.
81
+ [default: '{l_bar}{bar}{r_bar}'], where
82
+ l_bar='{desc}: {percentage:3.0f}%|' and
83
+ r_bar='| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, '
84
+ '{rate_fmt}{postfix}]'
85
+ Possible vars: l_bar, bar, r_bar, n, n_fmt, total, total_fmt,
86
+ percentage, elapsed, elapsed_s, ncols, nrows, desc, unit,
87
+ rate, rate_fmt, rate_noinv, rate_noinv_fmt,
88
+ rate_inv, rate_inv_fmt, postfix, unit_divisor,
89
+ remaining, remaining_s, eta.
90
+ Note that a trailing ": " is automatically removed after {desc}
91
+ if the latter is empty.
92
+ initial (Optional[Union[int, float]]): The initial counter value. Useful when restarting a progress
93
+ bar [default: 0]. If using float, consider specifying `{n:.3f}`
94
+ or similar in `bar_format`, or specifying `unit_scale`.
95
+ position (Optional[int]): Specify the line offset to print this bar (starting from 0)
96
+ Automatic if unspecified.
97
+ Useful to manage multiple bars at once (eg, from threads).
98
+ postfix (Optional[Dict]): Specify additional stats to display at the end of the bar.
99
+ Calls `set_postfix(**postfix)` if possible (dict).
100
+ unit_divisor (Optional[float]): [default: 1000], ignored unless `unit_scale` is True.
101
+ write_bytes (Optional[bool]): If (default: None) and `file` is unspecified,
102
+ bytes will be written in Python 2. If `True` will also write
103
+ bytes. In all other cases will default to unicode.
104
+ lock_args (Optional[tuple]): Passed to `refresh` for intermediate output
105
+ (initialisation, iterating, and updating).
106
+ nrows (Optional[int]): The screen height. If specified, hides nested bars
107
+ outside this bound. If unspecified, attempts to use environment height.
108
+ The fallback is 20.
109
+ colour (Optional[str]): Bar colour (e.g. 'green', '#00ff00').
110
+ delay (Optional[float]): Don't display until [default: 0] seconds have elapsed.
111
+ gui (Optional[bool]): WARNING: internal parameter - do not use.
112
+ Use tqdm.gui.tqdm(...) instead. If set, will attempt to use
113
+ matplotlib animations for a graphical output [default: False].
114
+
115
+
116
+ Example:
117
+ >>> with DownloadProgressBar(unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1]) as p_bar:
118
+ >>> urllib.request.urlretrieve(url, filename=output_path, reporthook=p_bar.update_to)
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ iterable: Optional[Iterable] = None,
124
+ desc: Optional[str] = None,
125
+ total: Optional[Union[int, float]] = None,
126
+ leave: Optional[bool] = True,
127
+ file: Optional[Union[io.TextIOWrapper, io.StringIO]] = None,
128
+ ncols: Optional[int] = None,
129
+ mininterval: Optional[float] = 0.1,
130
+ maxinterval: Optional[float] = 10.0,
131
+ miniters: Optional[Union[int, float]] = None,
132
+ use_ascii: Optional[Union[bool, str]] = None,
133
+ disable: Optional[bool] = False,
134
+ unit: Optional[str] = "it",
135
+ unit_scale: Optional[Union[bool, int, float]] = False,
136
+ dynamic_ncols: Optional[bool] = False,
137
+ smoothing: Optional[float] = 0.3,
138
+ bar_format: Optional[str] = None,
139
+ initial: Optional[Union[int, float]] = 0,
140
+ position: Optional[int] = None,
141
+ postfix: Optional[Dict] = None,
142
+ unit_divisor: Optional[float] = 1000,
143
+ write_bytes: Optional[bool] = None,
144
+ lock_args: Optional[tuple] = None,
145
+ nrows: Optional[int] = None,
146
+ colour: Optional[str] = None,
147
+ delay: Optional[float] = 0,
148
+ gui: Optional[bool] = False,
149
+ **kwargs
150
+ ):
151
+ super().__init__(
152
+ iterable=iterable,
153
+ desc=desc,
154
+ total=total,
155
+ leave=leave,
156
+ file=file,
157
+ ncols=ncols,
158
+ mininterval=mininterval,
159
+ maxinterval=maxinterval,
160
+ miniters=miniters,
161
+ ascii=use_ascii,
162
+ disable=disable,
163
+ unit=unit,
164
+ unit_scale=unit_scale,
165
+ dynamic_ncols=dynamic_ncols,
166
+ smoothing=smoothing,
167
+ bar_format=bar_format,
168
+ initial=initial,
169
+ position=position,
170
+ postfix=postfix,
171
+ unit_divisor=unit_divisor,
172
+ write_bytes=write_bytes,
173
+ lock_args=lock_args,
174
+ nrows=nrows,
175
+ colour=colour,
176
+ delay=delay,
177
+ gui=gui,
178
+ **kwargs
179
+ )
180
+ self.total: Optional[Union[int, float]]
181
+
182
+ def update_to(self, chunk_number: int = 1, max_chunk_size: int = 1, total_size=None):
183
+ """Progress bar hook for tqdm.
184
+
185
+ The implementor does not have to bother about passing parameters to this as it gets them from urlretrieve.
186
+ However the context needs a few parameters. Refer to the example.
187
+
188
+ Args:
189
+ chunk_number (int, optional): The current chunk being processed. Defaults to 1.
190
+ max_chunk_size (int, optional): Maximum size of each chunk. Defaults to 1.
191
+ total_size ([type], optional): Total download size. Defaults to None.
192
+ """
193
+ if total_size is not None:
194
+ self.total = total_size
195
+ self.update(chunk_number * max_chunk_size - self.n)
anomalib/data/utils/image.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image Utils."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ import math
18
+ from pathlib import Path
19
+ from typing import List, Union
20
+
21
+ import cv2
22
+ import numpy as np
23
+ import torch.nn.functional as F
24
+ from torch import Tensor
25
+ from torchvision.datasets.folder import IMG_EXTENSIONS
26
+
27
+
28
+ def get_image_filenames(path: Union[str, Path]) -> List[str]:
29
+ """Get image filenames.
30
+
31
+ Args:
32
+ path (Union[str, Path]): Path to image or image-folder.
33
+
34
+ Returns:
35
+ List[str]: List of image filenames
36
+
37
+ """
38
+ image_filenames: List[str]
39
+
40
+ if isinstance(path, str):
41
+ path = Path(path)
42
+
43
+ if path.is_file() and path.suffix in IMG_EXTENSIONS:
44
+ image_filenames = [str(path)]
45
+
46
+ if path.is_dir():
47
+ image_filenames = [str(p) for p in path.glob("**/*") if p.suffix in IMG_EXTENSIONS]
48
+
49
+ if len(image_filenames) == 0:
50
+ raise ValueError(f"Found 0 images in {path}")
51
+
52
+ return image_filenames
53
+
54
+
55
+ def read_image(path: Union[str, Path]) -> np.ndarray:
56
+ """Read image from disk in RGB format.
57
+
58
+ Args:
59
+ path (str, Path): path to the image file
60
+
61
+ Example:
62
+ >>> image = read_image("test_image.jpg")
63
+
64
+ Returns:
65
+ image as numpy array
66
+ """
67
+ path = path if isinstance(path, str) else str(path)
68
+ image = cv2.imread(path)
69
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
70
+
71
+ return image
72
+
73
+
74
+ def pad_nextpow2(batch: Tensor) -> Tensor:
75
+ """Compute required padding from input size and return padded images.
76
+
77
+ Finds the largest dimension and computes a square image of dimensions that are of the power of 2.
78
+ In case the image dimension is odd, it returns the image with an extra padding on one side.
79
+
80
+ Args:
81
+ batch (Tensor): Input images
82
+
83
+ Returns:
84
+ batch: Padded batch
85
+ """
86
+ # find the largest dimension
87
+ l_dim = 2 ** math.ceil(math.log(max(*batch.shape[-2:]), 2))
88
+ padding_w = [math.ceil((l_dim - batch.shape[-2]) / 2), math.floor((l_dim - batch.shape[-2]) / 2)]
89
+ padding_h = [math.ceil((l_dim - batch.shape[-1]) / 2), math.floor((l_dim - batch.shape[-1]) / 2)]
90
+ padded_batch = F.pad(batch, pad=[*padding_h, *padding_w])
91
+ return padded_batch
anomalib/data/utils/split.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataset Split Utils.
2
+
3
+ This module contains function in regards to splitting normal images in training set,
4
+ and creating validation sets from test sets.
5
+
6
+ These function are useful
7
+ - when the test set does not contain any normal images.
8
+ - when the dataset doesn't have a validation set.
9
+ """
10
+
11
+ # Copyright (C) 2020 Intel Corporation
12
+ #
13
+ # Licensed under the Apache License, Version 2.0 (the "License");
14
+ # you may not use this file except in compliance with the License.
15
+ # You may obtain a copy of the License at
16
+ #
17
+ # http://www.apache.org/licenses/LICENSE-2.0
18
+ #
19
+ # Unless required by applicable law or agreed to in writing,
20
+ # software distributed under the License is distributed on an "AS IS" BASIS,
21
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ # See the License for the specific language governing permissions
23
+ # and limitations under the License.
24
+
25
+ import random
26
+
27
+ from pandas.core.frame import DataFrame
28
+
29
+
30
+ def split_normal_images_in_train_set(
31
+ samples: DataFrame, split_ratio: float = 0.1, seed: int = 0, normal_label: str = "good"
32
+ ) -> DataFrame:
33
+ """Split normal images in train set.
34
+
35
+ This function splits the normal images in training set and assigns the
36
+ values to the test set. This is particularly useful especially when the
37
+ test set does not contain any normal images.
38
+
39
+ This is important because when the test set doesn't have any normal images,
40
+ AUC computation fails due to having single class.
41
+
42
+ Args:
43
+ samples (DataFrame): Dataframe containing dataset info such as filenames, splits etc.
44
+ split_ratio (float, optional): Train-Test normal image split ratio. Defaults to 0.1.
45
+ seed (int, optional): Random seed to ensure reproducibility. Defaults to 0.
46
+ normal_label (str): Name of the normal label. For MVTec AD, for instance, this is normal_label.
47
+
48
+ Returns:
49
+ DataFrame: Output dataframe where the part of the training set is assigned to test set.
50
+ """
51
+
52
+ if seed > 0:
53
+ random.seed(seed)
54
+
55
+ normal_train_image_indices = samples.index[(samples.split == "train") & (samples.label == normal_label)].to_list()
56
+ num_normal_train_images = len(normal_train_image_indices)
57
+ num_normal_valid_images = int(num_normal_train_images * split_ratio)
58
+
59
+ indices_to_split_from_train_set = random.sample(population=normal_train_image_indices, k=num_normal_valid_images)
60
+ samples.loc[indices_to_split_from_train_set, "split"] = "test"
61
+
62
+ return samples
63
+
64
+
65
+ def create_validation_set_from_test_set(samples: DataFrame, seed: int = 0, normal_label: str = "good") -> DataFrame:
66
+ """Craete Validation Set from Test Set.
67
+
68
+ This function creates a validation set from test set by splitting both
69
+ normal and abnormal samples to two.
70
+
71
+ Args:
72
+ samples (DataFrame): Dataframe containing dataset info such as filenames, splits etc.
73
+ seed (int, optional): Random seed to ensure reproducibility. Defaults to 0.
74
+ normal_label (str): Name of the normal label. For MVTec AD, for instance, this is normal_label.
75
+ """
76
+
77
+ if seed > 0:
78
+ random.seed(seed)
79
+
80
+ # Split normal images.
81
+ normal_test_image_indices = samples.index[(samples.split == "test") & (samples.label == normal_label)].to_list()
82
+ num_normal_valid_images = len(normal_test_image_indices) // 2
83
+
84
+ indices_to_sample = random.sample(population=normal_test_image_indices, k=num_normal_valid_images)
85
+ samples.loc[indices_to_sample, "split"] = "val"
86
+
87
+ # Split abnormal images.
88
+ abnormal_test_image_indices = samples.index[(samples.split == "test") & (samples.label != normal_label)].to_list()
89
+ num_abnormal_valid_images = len(abnormal_test_image_indices) // 2
90
+
91
+ indices_to_sample = random.sample(population=abnormal_test_image_indices, k=num_abnormal_valid_images)
92
+ samples.loc[indices_to_sample, "split"] = "val"
93
+
94
+ return samples
anomalib/deploy/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for Inference and model deployment."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .inferencers import OpenVINOInferencer, TorchInferencer
18
+ from .optimize import export_convert, get_model_metadata
19
+
20
+ __all__ = ["OpenVINOInferencer", "TorchInferencer", "export_convert", "get_model_metadata"]
anomalib/deploy/inferencers/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inferencers for Torch and OpenVINO."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .base import Inferencer
18
+ from .openvino import OpenVINOInferencer
19
+ from .torch import TorchInferencer
20
+
21
+ __all__ = ["Inferencer", "TorchInferencer", "OpenVINOInferencer"]
anomalib/deploy/inferencers/base.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base Inferencer for Torch and OpenVINO."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from abc import ABC, abstractmethod
18
+ from pathlib import Path
19
+ from typing import Dict, Optional, Tuple, Union, cast
20
+
21
+ import cv2
22
+ import numpy as np
23
+ from omegaconf import DictConfig, OmegaConf
24
+ from skimage.morphology import dilation
25
+ from skimage.segmentation import find_boundaries
26
+ from torch import Tensor
27
+
28
+ from anomalib.data.utils import read_image
29
+ from anomalib.post_processing import compute_mask, superimpose_anomaly_map
30
+ from anomalib.post_processing.normalization.cdf import normalize as normalize_cdf
31
+ from anomalib.post_processing.normalization.cdf import standardize
32
+ from anomalib.post_processing.normalization.min_max import (
33
+ normalize as normalize_min_max,
34
+ )
35
+
36
+
37
+ class Inferencer(ABC):
38
+ """Abstract class for the inference.
39
+
40
+ This is used by both Torch and OpenVINO inference.
41
+ """
42
+
43
+ @abstractmethod
44
+ def load_model(self, path: Union[str, Path]):
45
+ """Load Model."""
46
+ raise NotImplementedError
47
+
48
+ @abstractmethod
49
+ def pre_process(self, image: np.ndarray) -> Union[np.ndarray, Tensor]:
50
+ """Pre-process."""
51
+ raise NotImplementedError
52
+
53
+ @abstractmethod
54
+ def forward(self, image: Union[np.ndarray, Tensor]) -> Union[np.ndarray, Tensor]:
55
+ """Forward-Pass input to model."""
56
+ raise NotImplementedError
57
+
58
+ @abstractmethod
59
+ def post_process(
60
+ self, predictions: Union[np.ndarray, Tensor], meta_data: Optional[Dict]
61
+ ) -> Tuple[np.ndarray, float]:
62
+ """Post-Process."""
63
+ raise NotImplementedError
64
+
65
+ def predict(
66
+ self,
67
+ image: Union[str, np.ndarray, Path],
68
+ superimpose: bool = True,
69
+ meta_data: Optional[dict] = None,
70
+ overlay_mask: bool = False,
71
+ ) -> Tuple[np.ndarray, float]:
72
+ """Perform a prediction for a given input image.
73
+
74
+ The main workflow is (i) pre-processing, (ii) forward-pass, (iii) post-process.
75
+
76
+ Args:
77
+ image (Union[str, np.ndarray]): Input image whose output is to be predicted.
78
+ It could be either a path to image or numpy array itself.
79
+
80
+ superimpose (bool): If this is set to True, output predictions
81
+ will be superimposed onto the original image. If false, `predict`
82
+ method will return the raw heatmap.
83
+
84
+ overlay_mask (bool): If this is set to True, output segmentation mask on top of image.
85
+
86
+ Returns:
87
+ np.ndarray: Output predictions to be visualized.
88
+ """
89
+ if meta_data is None:
90
+ if hasattr(self, "meta_data"):
91
+ meta_data = getattr(self, "meta_data")
92
+ else:
93
+ meta_data = {}
94
+ if isinstance(image, (str, Path)):
95
+ image_arr: np.ndarray = read_image(image)
96
+ else: # image is already a numpy array. Kept for mypy compatibility.
97
+ image_arr = image
98
+ meta_data["image_shape"] = image_arr.shape[:2]
99
+
100
+ processed_image = self.pre_process(image_arr)
101
+ predictions = self.forward(processed_image)
102
+ anomaly_map, pred_scores = self.post_process(predictions, meta_data=meta_data)
103
+
104
+ # Overlay segmentation mask using raw predictions
105
+ if overlay_mask and meta_data is not None:
106
+ image_arr = self._superimpose_segmentation_mask(meta_data, anomaly_map, image_arr)
107
+
108
+ if superimpose is True:
109
+ anomaly_map = superimpose_anomaly_map(anomaly_map, image_arr)
110
+
111
+ return anomaly_map, pred_scores
112
+
113
+ def _superimpose_segmentation_mask(self, meta_data: dict, anomaly_map: np.ndarray, image: np.ndarray):
114
+ """Superimpose segmentation mask on top of image.
115
+
116
+ Args:
117
+ meta_data (dict): Metadata of the image which contains the image size.
118
+ anomaly_map (np.ndarray): Anomaly map which is used to extract segmentation mask.
119
+ image (np.ndarray): Image on which segmentation mask is to be superimposed.
120
+
121
+ Returns:
122
+ np.ndarray: Image with segmentation mask superimposed.
123
+ """
124
+ pred_mask = compute_mask(anomaly_map, 0.5) # assumes predictions are normalized.
125
+ image_height = meta_data["image_shape"][0]
126
+ image_width = meta_data["image_shape"][1]
127
+ pred_mask = cv2.resize(pred_mask, (image_width, image_height))
128
+ boundaries = find_boundaries(pred_mask)
129
+ outlines = dilation(boundaries, np.ones((7, 7)))
130
+ image[outlines] = [255, 0, 0]
131
+ return image
132
+
133
+ def __call__(self, image: np.ndarray) -> Tuple[np.ndarray, float]:
134
+ """Call predict on the Image.
135
+
136
+ Args:
137
+ image (np.ndarray): Input Image
138
+
139
+ Returns:
140
+ np.ndarray: Output predictions to be visualized
141
+ """
142
+ return self.predict(image)
143
+
144
+ def _normalize(
145
+ self,
146
+ anomaly_maps: Union[Tensor, np.ndarray],
147
+ pred_scores: Union[Tensor, np.float32],
148
+ meta_data: Union[Dict, DictConfig],
149
+ ) -> Tuple[Union[np.ndarray, Tensor], float]:
150
+ """Applies normalization and resizes the image.
151
+
152
+ Args:
153
+ anomaly_maps (Union[Tensor, np.ndarray]): Predicted raw anomaly map.
154
+ pred_scores (Union[Tensor, np.float32]): Predicted anomaly score
155
+ meta_data (Dict): Meta data. Post-processing step sometimes requires
156
+ additional meta data such as image shape. This variable comprises such info.
157
+
158
+ Returns:
159
+ Tuple[Union[np.ndarray, Tensor], float]: Post processed predictions that are ready to be visualized and
160
+ predicted scores.
161
+
162
+
163
+ """
164
+
165
+ # min max normalization
166
+ if "min" in meta_data and "max" in meta_data:
167
+ anomaly_maps = normalize_min_max(
168
+ anomaly_maps, meta_data["pixel_threshold"], meta_data["min"], meta_data["max"]
169
+ )
170
+ pred_scores = normalize_min_max(
171
+ pred_scores, meta_data["image_threshold"], meta_data["min"], meta_data["max"]
172
+ )
173
+
174
+ # standardize pixel scores
175
+ if "pixel_mean" in meta_data.keys() and "pixel_std" in meta_data.keys():
176
+ anomaly_maps = standardize(
177
+ anomaly_maps, meta_data["pixel_mean"], meta_data["pixel_std"], center_at=meta_data["image_mean"]
178
+ )
179
+ anomaly_maps = normalize_cdf(anomaly_maps, meta_data["pixel_threshold"])
180
+
181
+ # standardize image scores
182
+ if "image_mean" in meta_data.keys() and "image_std" in meta_data.keys():
183
+ pred_scores = standardize(pred_scores, meta_data["image_mean"], meta_data["image_std"])
184
+ pred_scores = normalize_cdf(pred_scores, meta_data["image_threshold"])
185
+
186
+ return anomaly_maps, float(pred_scores)
187
+
188
+ def _load_meta_data(
189
+ self, path: Optional[Union[str, Path]] = None
190
+ ) -> Union[DictConfig, Dict[str, Union[float, np.ndarray, Tensor]]]:
191
+ """Loads the meta data from the given path.
192
+
193
+ Args:
194
+ path (Optional[Union[str, Path]], optional): Path to JSON file containing the metadata.
195
+ If no path is provided, it returns an empty dict. Defaults to None.
196
+
197
+ Returns:
198
+ Union[DictConfig, Dict]: Dictionary containing the metadata.
199
+ """
200
+ meta_data: Union[DictConfig, Dict[str, Union[float, np.ndarray, Tensor]]] = {}
201
+ if path is not None:
202
+ config = OmegaConf.load(path)
203
+ meta_data = cast(DictConfig, config)
204
+ return meta_data
anomalib/deploy/inferencers/openvino.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This module contains inference-related abstract class and its Torch and OpenVINO implementations."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from importlib.util import find_spec
18
+ from pathlib import Path
19
+ from typing import Dict, Optional, Tuple, Union
20
+
21
+ import cv2
22
+ import numpy as np
23
+ from omegaconf import DictConfig, ListConfig
24
+
25
+ from anomalib.pre_processing import PreProcessor
26
+
27
+ from .base import Inferencer
28
+
29
+ if find_spec("openvino") is not None:
30
+ from openvino.inference_engine import ( # type: ignore # pylint: disable=no-name-in-module
31
+ IECore,
32
+ )
33
+
34
+
35
+ class OpenVINOInferencer(Inferencer):
36
+ """OpenVINO implementation for the inference.
37
+
38
+ Args:
39
+ config (DictConfig): Configurable parameters that are used
40
+ during the training stage.
41
+ path (Union[str, Path]): Path to the openvino onnx, xml or bin file.
42
+ meta_data_path (Union[str, Path], optional): Path to metadata file. Defaults to None.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ config: Union[DictConfig, ListConfig],
48
+ path: Union[str, Path, Tuple[bytes, bytes]],
49
+ meta_data_path: Union[str, Path] = None,
50
+ ):
51
+ self.config = config
52
+ self.input_blob, self.output_blob, self.network = self.load_model(path)
53
+ self.meta_data = super()._load_meta_data(meta_data_path)
54
+
55
+ def load_model(self, path: Union[str, Path, Tuple[bytes, bytes]]):
56
+ """Load the OpenVINO model.
57
+
58
+ Args:
59
+ path (Union[str, Path, Tuple[bytes, bytes]]): Path to the onnx or xml and bin files
60
+ or tuple of .xml and .bin data as bytes.
61
+
62
+ Returns:
63
+ [Tuple[str, str, ExecutableNetwork]]: Input and Output blob names
64
+ together with the Executable network.
65
+ """
66
+ ie_core = IECore()
67
+ # If tuple of bytes is passed
68
+
69
+ if isinstance(path, tuple):
70
+ network = ie_core.read_network(model=path[0], weights=path[1], init_from_buffer=True)
71
+ else:
72
+ path = path if isinstance(path, Path) else Path(path)
73
+ if path.suffix in (".bin", ".xml"):
74
+ if path.suffix == ".bin":
75
+ bin_path, xml_path = path, path.with_suffix(".xml")
76
+ elif path.suffix == ".xml":
77
+ xml_path, bin_path = path, path.with_suffix(".bin")
78
+ network = ie_core.read_network(xml_path, bin_path)
79
+ elif path.suffix == ".onnx":
80
+ network = ie_core.read_network(path)
81
+ else:
82
+ raise ValueError(f"Path must be .onnx, .bin or .xml file. Got {path.suffix}")
83
+
84
+ input_blob = next(iter(network.input_info))
85
+ output_blob = next(iter(network.outputs))
86
+ executable_network = ie_core.load_network(network=network, device_name="CPU")
87
+
88
+ return input_blob, output_blob, executable_network
89
+
90
+ def pre_process(self, image: np.ndarray) -> np.ndarray:
91
+ """Pre process the input image by applying transformations.
92
+
93
+ Args:
94
+ image (np.ndarray): Input image.
95
+
96
+ Returns:
97
+ np.ndarray: pre-processed image.
98
+ """
99
+ config = self.config.transform if "transform" in self.config.keys() else None
100
+ image_size = tuple(self.config.dataset.image_size)
101
+ pre_processor = PreProcessor(config, image_size)
102
+ processed_image = pre_processor(image=image)["image"]
103
+
104
+ if len(processed_image.shape) == 3:
105
+ processed_image = np.expand_dims(processed_image, axis=0)
106
+
107
+ if processed_image.shape[-1] == 3:
108
+ processed_image = processed_image.transpose(0, 3, 1, 2)
109
+
110
+ return processed_image
111
+
112
+ def forward(self, image: np.ndarray) -> np.ndarray:
113
+ """Forward-Pass input tensor to the model.
114
+
115
+ Args:
116
+ image (np.ndarray): Input tensor.
117
+
118
+ Returns:
119
+ np.ndarray: Output predictions.
120
+ """
121
+ return self.network.infer(inputs={self.input_blob: image})
122
+
123
+ def post_process(
124
+ self, predictions: np.ndarray, meta_data: Optional[Union[Dict, DictConfig]] = None
125
+ ) -> Tuple[np.ndarray, float]:
126
+ """Post process the output predictions.
127
+
128
+ Args:
129
+ predictions (np.ndarray): Raw output predicted by the model.
130
+ meta_data (Dict, optional): Meta data. Post-processing step sometimes requires
131
+ additional meta data such as image shape. This variable comprises such info.
132
+ Defaults to None.
133
+
134
+ Returns:
135
+ np.ndarray: Post processed predictions that are ready to be visualized.
136
+ """
137
+ if meta_data is None:
138
+ meta_data = self.meta_data
139
+
140
+ predictions = predictions[self.output_blob]
141
+ anomaly_map = predictions.squeeze()
142
+ pred_score = anomaly_map.reshape(-1).max()
143
+
144
+ anomaly_map, pred_score = self._normalize(anomaly_map, pred_score, meta_data)
145
+
146
+ if "image_shape" in meta_data and anomaly_map.shape != meta_data["image_shape"]:
147
+ anomaly_map = cv2.resize(anomaly_map, meta_data["image_shape"])
148
+
149
+ return anomaly_map, float(pred_score)
anomalib/deploy/inferencers/torch.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This module contains Torch inference implementations."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from pathlib import Path
18
+ from typing import Dict, Optional, Tuple, Union
19
+
20
+ import cv2
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import DictConfig, ListConfig
24
+ from torch import Tensor
25
+
26
+ from anomalib.deploy.optimize import get_model_metadata
27
+ from anomalib.models import get_model
28
+ from anomalib.models.components import AnomalyModule
29
+ from anomalib.pre_processing import PreProcessor
30
+
31
+ from .base import Inferencer
32
+
33
+
34
+ class TorchInferencer(Inferencer):
35
+ """PyTorch implementation for the inference.
36
+
37
+ Args:
38
+ config (DictConfig): Configurable parameters that are used
39
+ during the training stage.
40
+ model_source (Union[str, Path, AnomalyModule]): Path to the model ckpt file or the Anomaly model.
41
+ meta_data_path (Union[str, Path], optional): Path to metadata file. If none, it tries to load the params
42
+ from the model state_dict. Defaults to None.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ config: Union[DictConfig, ListConfig],
48
+ model_source: Union[str, Path, AnomalyModule],
49
+ meta_data_path: Union[str, Path] = None,
50
+ ):
51
+ self.config = config
52
+ if isinstance(model_source, AnomalyModule):
53
+ self.model = model_source
54
+ else:
55
+ self.model = self.load_model(model_source)
56
+
57
+ self.meta_data = self._load_meta_data(meta_data_path)
58
+
59
+ def _load_meta_data(self, path: Optional[Union[str, Path]] = None) -> Union[Dict, DictConfig]:
60
+ """Load metadata from file or from model state dict.
61
+
62
+ Args:
63
+ path (Optional[Union[str, Path]], optional): Path to metadata file. If none, it tries to load the params
64
+ from the model state_dict. Defaults to None.
65
+
66
+ Returns:
67
+ Dict: Dictionary containing the meta_data.
68
+ """
69
+ meta_data: Union[DictConfig, Dict[str, Union[float, Tensor, np.ndarray]]]
70
+ if path is None:
71
+ meta_data = get_model_metadata(self.model)
72
+ else:
73
+ meta_data = super()._load_meta_data(path)
74
+ return meta_data
75
+
76
+ def load_model(self, path: Union[str, Path]) -> AnomalyModule:
77
+ """Load the PyTorch model.
78
+
79
+ Args:
80
+ path (Union[str, Path]): Path to model ckpt file.
81
+
82
+ Returns:
83
+ (AnomalyModule): PyTorch Lightning model.
84
+ """
85
+ model = get_model(self.config)
86
+ model.load_state_dict(torch.load(path)["state_dict"])
87
+ model.eval()
88
+ return model
89
+
90
+ def pre_process(self, image: np.ndarray) -> Tensor:
91
+ """Pre process the input image by applying transformations.
92
+
93
+ Args:
94
+ image (np.ndarray): Input image
95
+
96
+ Returns:
97
+ Tensor: pre-processed image.
98
+ """
99
+ config = self.config.transform if "transform" in self.config.keys() else None
100
+ image_size = tuple(self.config.dataset.image_size)
101
+ pre_processor = PreProcessor(config, image_size)
102
+ processed_image = pre_processor(image=image)["image"]
103
+
104
+ if len(processed_image) == 3:
105
+ processed_image = processed_image.unsqueeze(0)
106
+
107
+ return processed_image
108
+
109
+ def forward(self, image: Tensor) -> Tensor:
110
+ """Forward-Pass input tensor to the model.
111
+
112
+ Args:
113
+ image (Tensor): Input tensor.
114
+
115
+ Returns:
116
+ Tensor: Output predictions.
117
+ """
118
+ return self.model(image)
119
+
120
+ def post_process(
121
+ self, predictions: Tensor, meta_data: Optional[Union[Dict, DictConfig]] = None
122
+ ) -> Tuple[np.ndarray, float]:
123
+ """Post process the output predictions.
124
+
125
+ Args:
126
+ predictions (Tensor): Raw output predicted by the model.
127
+ meta_data (Dict, optional): Meta data. Post-processing step sometimes requires
128
+ additional meta data such as image shape. This variable comprises such info.
129
+ Defaults to None.
130
+
131
+ Returns:
132
+ np.ndarray: Post processed predictions that are ready to be visualized.
133
+ """
134
+ if meta_data is None:
135
+ meta_data = self.meta_data
136
+
137
+ if isinstance(predictions, Tensor):
138
+ anomaly_map = predictions
139
+ pred_score = anomaly_map.reshape(-1).max()
140
+ else:
141
+ # NOTE: Patchcore `forward`` returns heatmap and score.
142
+ # We need to add the following check to ensure the variables
143
+ # are properly assigned. Without this check, the code
144
+ # throws an error regarding type mismatch torch vs np.
145
+ if isinstance(predictions[1], (Tensor)):
146
+ anomaly_map, pred_score = predictions
147
+ pred_score = pred_score.detach()
148
+ else:
149
+ anomaly_map, pred_score = predictions
150
+ pred_score = pred_score.detach().numpy()
151
+
152
+ anomaly_map = anomaly_map.squeeze()
153
+
154
+ anomaly_map, pred_score = self._normalize(anomaly_map, pred_score, meta_data)
155
+
156
+ if isinstance(anomaly_map, Tensor):
157
+ anomaly_map = anomaly_map.detach().cpu().numpy()
158
+
159
+ if "image_shape" in meta_data and anomaly_map.shape != meta_data["image_shape"]:
160
+ image_height = meta_data["image_shape"][0]
161
+ image_width = meta_data["image_shape"][1]
162
+ anomaly_map = cv2.resize(anomaly_map, (image_width, image_height))
163
+
164
+ return anomaly_map, float(pred_score)
anomalib/deploy/optimize.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utilities for optimization and OpenVINO conversion."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+
18
+ import json
19
+ import os
20
+ from pathlib import Path
21
+ from typing import Dict, List, Tuple, Union
22
+
23
+ import numpy as np
24
+ import torch
25
+ from torch import Tensor
26
+
27
+ from anomalib.models.components import AnomalyModule
28
+
29
+
30
+ def get_model_metadata(model: AnomalyModule) -> Dict[str, Tensor]:
31
+ """Get meta data related to normalization from model.
32
+
33
+ Args:
34
+ model (AnomalyModule): Anomaly model which contains metadata related to normalization.
35
+
36
+ Returns:
37
+ Dict[str, Tensor]: metadata
38
+ """
39
+ meta_data = {}
40
+ cached_meta_data = {
41
+ "image_threshold": model.image_threshold.cpu().value,
42
+ "pixel_threshold": model.pixel_threshold.cpu().value,
43
+ "pixel_mean": model.training_distribution.pixel_mean.cpu(),
44
+ "image_mean": model.training_distribution.image_mean.cpu(),
45
+ "pixel_std": model.training_distribution.pixel_std.cpu(),
46
+ "image_std": model.training_distribution.image_std.cpu(),
47
+ "min": model.min_max.min.cpu(),
48
+ "max": model.min_max.max.cpu(),
49
+ }
50
+ # Remove undefined values by copying in a new dict
51
+ for key, val in cached_meta_data.items():
52
+ if not np.isinf(val).all():
53
+ meta_data[key] = val
54
+ del cached_meta_data
55
+ return meta_data
56
+
57
+
58
+ def export_convert(
59
+ model: AnomalyModule,
60
+ input_size: Union[List[int], Tuple[int, int]],
61
+ onnx_path: Union[str, Path],
62
+ export_path: Union[str, Path],
63
+ ):
64
+ """Export the model to onnx format and convert to OpenVINO IR.
65
+
66
+ Args:
67
+ model (AnomalyModule): Model to convert.
68
+ input_size (Union[List[int], Tuple[int, int]]): Image size used as the input for onnx converter.
69
+ onnx_path (Union[str, Path]): Path to output onnx model.
70
+ export_path (Union[str, Path]): Path to exported OpenVINO IR.
71
+ """
72
+ height, width = input_size
73
+ torch.onnx.export(
74
+ model.model,
75
+ torch.zeros((1, 3, height, width)).to(model.device),
76
+ onnx_path,
77
+ opset_version=11,
78
+ input_names=["input"],
79
+ output_names=["output"],
80
+ )
81
+ optimize_command = "mo --input_model " + str(onnx_path) + " --output_dir " + str(export_path)
82
+ os.system(optimize_command)
83
+ with open(Path(export_path) / "meta_data.json", "w", encoding="utf-8") as metadata_file:
84
+ meta_data = get_model_metadata(model)
85
+ # Convert metadata from torch
86
+ for key, value in meta_data.items():
87
+ if isinstance(value, Tensor):
88
+ meta_data[key] = value.numpy().tolist()
89
+ json.dump(meta_data, metadata_file, ensure_ascii=False, indent=4)
anomalib/models/__init__.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load Anomaly Model."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ import os
18
+ from importlib import import_module
19
+ from typing import List, Union
20
+
21
+ from omegaconf import DictConfig, ListConfig
22
+ from torch import load
23
+
24
+ from anomalib.models.components import AnomalyModule
25
+
26
+ # TODO(AlexanderDokuchaev): Workaround of wrapping by NNCF.
27
+ # Can't not wrap `spatial_softmax2d` if use import_module.
28
+ from anomalib.models.padim.lightning_model import PadimLightning # noqa: F401
29
+
30
+
31
+ def get_model(config: Union[DictConfig, ListConfig]) -> AnomalyModule:
32
+ """Load model from the configuration file.
33
+
34
+ Works only when the convention for model naming is followed.
35
+
36
+ The convention for writing model classes is
37
+ `anomalib.models.<model_name>.model.<Model_name>Lightning`
38
+ `anomalib.models.stfpm.model.StfpmLightning`
39
+
40
+ and for OpenVINO
41
+ `anomalib.models.<model-name>.model.<Model_name>OpenVINO`
42
+ `anomalib.models.stfpm.model.StfpmOpenVINO`
43
+
44
+ Args:
45
+ config (Union[DictConfig, ListConfig]): Config.yaml loaded using OmegaConf
46
+
47
+ Raises:
48
+ ValueError: If unsupported model is passed
49
+
50
+ Returns:
51
+ AnomalyModule: Anomaly Model
52
+ """
53
+ openvino_model_list: List[str] = ["stfpm"]
54
+ torch_model_list: List[str] = ["padim", "stfpm", "dfkde", "dfm", "patchcore", "cflow", "ganomaly"]
55
+ model: AnomalyModule
56
+
57
+ if "openvino" in config.keys() and config.openvino:
58
+ if config.model.name in openvino_model_list:
59
+ module = import_module(f"anomalib.models.{config.model.name}.model")
60
+ model = getattr(module, f"{config.model.name.capitalize()}OpenVINO")
61
+ else:
62
+ raise ValueError(f"Unknown model {config.model.name} for OpenVINO model!")
63
+ else:
64
+ if config.model.name in torch_model_list:
65
+ module = import_module(f"anomalib.models.{config.model.name}")
66
+ model = getattr(module, f"{config.model.name.capitalize()}Lightning")
67
+ else:
68
+ raise ValueError(f"Unknown model {config.model.name}!")
69
+
70
+ model = model(config)
71
+
72
+ if "init_weights" in config.keys() and config.init_weights:
73
+ model.load_state_dict(load(os.path.join(config.project.path, config.init_weights))["state_dict"], strict=False)
74
+
75
+ return model
anomalib/models/cflow/README.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Real-Time Unsupervised Anomaly Detection via Conditional Normalizing Flows
2
+
3
+ This is the implementation of the [CFLOW-AD](https://arxiv.org/pdf/2107.12571v1.pdf) paper. This code is modified form of the [official repository](https://github.com/gudovskiy/cflow-ad).
4
+
5
+ Model Type: Segmentation
6
+
7
+ ## Description
8
+
9
+ CFLOW model is based on a conditional normalizing flow framework adopted for anomaly detection with localization. It consists of a discriminatively pretrained encoder followed by a multi-scale generative decoders. The encoder extracts features with multi-scale pyramid pooling to capture both global and local semantic information with the growing from top to bottom receptive fields. Pooled features are processed by a set of decoders to explicitly estimate likelihood of the encoded features. The estimated multi-scale likelyhoods are upsampled to input size and added up to produce the anomaly map.
10
+
11
+ ## Architecture
12
+
13
+ ![CFlow Architecture](../../../docs/source/images/cflow/architecture.jpg "CFlow Architecture")
14
+
15
+ ## Usage
16
+
17
+ `python tools/train.py --model cflow`
18
+
19
+ ## Benchmark
20
+
21
+ All results gathered with seed `42`.
22
+
23
+ ## [MVTec AD Dataset](https://www.mvtec.com/company/research/datasets/mvtec-ad)
24
+
25
+ ### Image-Level AUC
26
+
27
+ | | Avg | Carpet | Grid | Leather | Tile | Wood | Bottle | Cable | Capsule | Hazelnut | Metal Nut | Pill | Screw | Toothbrush | Transistor | Zipper |
28
+ | -------------- | :---: | :----: | :---: | :-----: | :---: | :---: | :----: | :---: | :-----: | :------: | :-------: | :---: | :---: | :--------: | :--------: | :----: |
29
+ | Wide ResNet-50 | 0.962 | 0.986 | 0.962 | 1.0 | 0.999 | 0.993 | 1.0 | 0.893 | 0.945 | 1.0 | 0.995 | 0.924 | 0.908 | 0.897 | 0.943 | 0.984 |
30
+
31
+ ### Pixel-Level AUC
32
+
33
+ | | Avg | Carpet | Grid | Leather | Tile | Wood | Bottle | Cable | Capsule | Hazelnut | Metal Nut | Pill | Screw | Toothbrush | Transistor | Zipper |
34
+ | -------------- | :---: | :----: | :---: | :-----: | :---: | :---: | :----: | :---: | :-----: | :------: | :-------: | :---: | :---: | :--------: | :--------: | :----: |
35
+ | Wide ResNet-50 | 0.971 | 0.986 | 0.968 | 0.993 | 0.968 | 0.924 | 0.981 | 0.955 | 0.988 | 0.990 | 0.982 | 0.983 | 0.979 | 0.985 | 0.897 | 0.980 |
36
+
37
+ ### Image F1 Score
38
+
39
+ | | Avg | Carpet | Grid | Leather | Tile | Wood | Bottle | Cable | Capsule | Hazelnut | Metal Nut | Pill | Screw | Toothbrush | Transistor | Zipper |
40
+ | -------------- | :---: | :----: | :---: | :-----: | :---: | :---: | :----: | :---: | :-----: | :------: | :-------: | :---: | :---: | :--------: | :--------: | :----: |
41
+ | Wide ResNet-50 | 0.944 | 0.972 | 0.932 | 1.000 | 0.988 | 0.967 | 1.000 | 0.832 | 0.939 | 1.000 | 0.979 | 0.924 | 0.971 | 0.870 | 0.818 | 0.967 |
42
+
43
+ ### Sample Results
44
+
45
+ ![Sample Result 1](../../../docs/source/images/cflow/results/0.png "Sample Result 1")
46
+
47
+ ![Sample Result 2](../../../docs/source/images/cflow/results/1.png "Sample Result 2")
48
+
49
+ ![Sample Result 3](../../../docs/source/images/cflow/results/2.png "Sample Result 3")
anomalib/models/cflow/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Real-Time Unsupervised Anomaly Detection via Conditional Normalizing Flows."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .lightning_model import CflowLightning
18
+
19
+ __all__ = ["CflowLightning"]
anomalib/models/cflow/anomaly_map.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Anomaly Map Generator for CFlow model implementation."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from typing import List, Tuple, Union, cast
18
+
19
+ import torch
20
+ import torch.nn.functional as F
21
+ from omegaconf import ListConfig
22
+ from torch import Tensor
23
+
24
+
25
+ class AnomalyMapGenerator:
26
+ """Generate Anomaly Heatmap."""
27
+
28
+ def __init__(
29
+ self,
30
+ image_size: Union[ListConfig, Tuple],
31
+ pool_layers: List[str],
32
+ ):
33
+ self.distance = torch.nn.PairwiseDistance(p=2, keepdim=True)
34
+ self.image_size = image_size if isinstance(image_size, tuple) else tuple(image_size)
35
+ self.pool_layers: List[str] = pool_layers
36
+
37
+ def compute_anomaly_map(
38
+ self, distribution: Union[List[Tensor], List[List]], height: List[int], width: List[int]
39
+ ) -> Tensor:
40
+ """Compute the layer map based on likelihood estimation.
41
+
42
+ Args:
43
+ distribution: Probability distribution for each decoder block
44
+ height: blocks height
45
+ width: blocks width
46
+
47
+ Returns:
48
+ Final Anomaly Map
49
+
50
+ """
51
+
52
+ test_map: List[Tensor] = []
53
+ for layer_idx in range(len(self.pool_layers)):
54
+ test_norm = torch.tensor(distribution[layer_idx], dtype=torch.double) # pylint: disable=not-callable
55
+ test_norm -= torch.max(test_norm) # normalize likelihoods to (-Inf:0] by subtracting a constant
56
+ test_prob = torch.exp(test_norm) # convert to probs in range [0:1]
57
+ test_mask = test_prob.reshape(-1, height[layer_idx], width[layer_idx])
58
+ # upsample
59
+ test_map.append(
60
+ F.interpolate(
61
+ test_mask.unsqueeze(1), size=self.image_size, mode="bilinear", align_corners=True
62
+ ).squeeze()
63
+ )
64
+ # score aggregation
65
+ score_map = torch.zeros_like(test_map[0])
66
+ for layer_idx in range(len(self.pool_layers)):
67
+ score_map += test_map[layer_idx]
68
+ score_mask = score_map
69
+ # invert probs to anomaly scores
70
+ anomaly_map = score_mask.max() - score_mask
71
+
72
+ return anomaly_map
73
+
74
+ def __call__(self, **kwargs: Union[List[Tensor], List[int], List[List]]) -> Tensor:
75
+ """Returns anomaly_map.
76
+
77
+ Expects `distribution`, `height` and 'width' keywords to be passed explicitly
78
+
79
+ Example
80
+ >>> anomaly_map_generator = AnomalyMapGenerator(image_size=tuple(hparams.model.input_size),
81
+ >>> pool_layers=pool_layers)
82
+ >>> output = self.anomaly_map_generator(distribution=dist, height=height, width=width)
83
+
84
+ Raises:
85
+ ValueError: `distribution`, `height` and 'width' keys are not found
86
+
87
+ Returns:
88
+ torch.Tensor: anomaly map
89
+ """
90
+ if not ("distribution" in kwargs and "height" in kwargs and "width" in kwargs):
91
+ raise KeyError(f"Expected keys `distribution`, `height` and `width`. Found {kwargs.keys()}")
92
+
93
+ # placate mypy
94
+ distribution: List[Tensor] = cast(List[Tensor], kwargs["distribution"])
95
+ height: List[int] = cast(List[int], kwargs["height"])
96
+ width: List[int] = cast(List[int], kwargs["width"])
97
+ return self.compute_anomaly_map(distribution, height, width)
anomalib/models/cflow/config.yaml ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dataset:
2
+ name: mvtec #options: [mvtec, btech, folder]
3
+ format: mvtec
4
+ path: ./datasets/MVTec
5
+ category: bottle
6
+ task: segmentation
7
+ image_size: 256
8
+ train_batch_size: 16
9
+ test_batch_size: 16
10
+ inference_batch_size: 16
11
+ fiber_batch_size: 64
12
+ num_workers: 8
13
+ transform_config:
14
+ train: null
15
+ val: null
16
+ create_validation_set: false
17
+
18
+ model:
19
+ name: cflow
20
+ backbone: wide_resnet50_2
21
+ layers:
22
+ - layer2
23
+ - layer3
24
+ - layer4
25
+ decoder: freia-cflow
26
+ condition_vector: 128
27
+ coupling_blocks: 8
28
+ clamp_alpha: 1.9
29
+ soft_permutation: false
30
+ lr: 0.0001
31
+ early_stopping:
32
+ patience: 2
33
+ metric: pixel_AUROC
34
+ mode: max
35
+ normalization_method: min_max # options: [null, min_max, cdf]
36
+ threshold:
37
+ image_default: 0
38
+ pixel_default: 0
39
+ adaptive: true
40
+
41
+ metrics:
42
+ image:
43
+ - F1Score
44
+ - AUROC
45
+ pixel:
46
+ - F1Score
47
+ - AUROC
48
+
49
+ project:
50
+ seed: 0
51
+ path: ./results
52
+ log_images_to: [local]
53
+ logger: false # options: [tensorboard, wandb, csv] or combinations.
54
+
55
+ # PL Trainer Args. Don't add extra parameter here.
56
+ trainer:
57
+ accelerator: auto # <"cpu", "gpu", "tpu", "ipu", "hpu", "auto">
58
+ accumulate_grad_batches: 1
59
+ amp_backend: native
60
+ auto_lr_find: false
61
+ auto_scale_batch_size: false
62
+ auto_select_gpus: false
63
+ benchmark: false
64
+ check_val_every_n_epoch: 1
65
+ default_root_dir: null
66
+ detect_anomaly: false
67
+ deterministic: false
68
+ enable_checkpointing: true
69
+ enable_model_summary: true
70
+ enable_progress_bar: true
71
+ fast_dev_run: false
72
+ gpus: null # Set automatically
73
+ gradient_clip_val: 0
74
+ ipus: null
75
+ limit_predict_batches: 1.0
76
+ limit_test_batches: 1.0
77
+ limit_train_batches: 1.0
78
+ limit_val_batches: 1.0
79
+ log_every_n_steps: 50
80
+ log_gpu_memory: null
81
+ max_epochs: 50
82
+ max_steps: -1
83
+ max_time: null
84
+ min_epochs: null
85
+ min_steps: null
86
+ move_metrics_to_cpu: false
87
+ multiple_trainloader_mode: max_size_cycle
88
+ num_nodes: 1
89
+ num_processes: 1
90
+ num_sanity_val_steps: 0
91
+ overfit_batches: 0.0
92
+ plugins: null
93
+ precision: 32
94
+ profiler: null
95
+ reload_dataloaders_every_n_epochs: 0
96
+ replace_sampler_ddp: true
97
+ strategy: null
98
+ sync_batchnorm: false
99
+ tpu_cores: null
100
+ track_grad_norm: -1
101
+ val_check_interval: 1.0
anomalib/models/cflow/lightning_model.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CFLOW: Real-Time Unsupervised Anomaly Detection via Conditional Normalizing Flows.
2
+
3
+ https://arxiv.org/pdf/2107.12571v1.pdf
4
+ """
5
+
6
+ # Copyright (C) 2020 Intel Corporation
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing,
15
+ # software distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions
18
+ # and limitations under the License.
19
+
20
+ import logging
21
+
22
+ import einops
23
+ import torch
24
+ import torch.nn.functional as F
25
+ from pytorch_lightning.callbacks import EarlyStopping
26
+ from torch import optim
27
+
28
+ from anomalib.models.cflow.torch_model import CflowModel
29
+ from anomalib.models.cflow.utils import get_logp, positional_encoding_2d
30
+ from anomalib.models.components import AnomalyModule
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ __all__ = ["CflowLightning"]
35
+
36
+
37
+ class CflowLightning(AnomalyModule):
38
+ """PL Lightning Module for the CFLOW algorithm."""
39
+
40
+ def __init__(self, hparams):
41
+ super().__init__(hparams)
42
+ logger.info("Initializing Cflow Lightning model.")
43
+
44
+ self.model: CflowModel = CflowModel(hparams)
45
+ self.loss_val = 0
46
+ self.automatic_optimization = False
47
+
48
+ def configure_callbacks(self):
49
+ """Configure model-specific callbacks."""
50
+ early_stopping = EarlyStopping(
51
+ monitor=self.hparams.model.early_stopping.metric,
52
+ patience=self.hparams.model.early_stopping.patience,
53
+ mode=self.hparams.model.early_stopping.mode,
54
+ )
55
+ return [early_stopping]
56
+
57
+ def configure_optimizers(self) -> torch.optim.Optimizer:
58
+ """Configures optimizers for each decoder.
59
+
60
+ Returns:
61
+ Optimizer: Adam optimizer for each decoder
62
+ """
63
+ decoders_parameters = []
64
+ for decoder_idx in range(len(self.model.pool_layers)):
65
+ decoders_parameters.extend(list(self.model.decoders[decoder_idx].parameters()))
66
+
67
+ optimizer = optim.Adam(
68
+ params=decoders_parameters,
69
+ lr=self.hparams.model.lr,
70
+ )
71
+ return optimizer
72
+
73
+ def training_step(self, batch, _): # pylint: disable=arguments-differ
74
+ """Training Step of CFLOW.
75
+
76
+ For each batch, decoder layers are trained with a dynamic fiber batch size.
77
+ Training step is performed manually as multiple training steps are involved
78
+ per batch of input images
79
+
80
+ Args:
81
+ batch: Input batch
82
+ _: Index of the batch.
83
+
84
+ Returns:
85
+ Loss value for the batch
86
+
87
+ """
88
+ opt = self.optimizers()
89
+ self.model.encoder.eval()
90
+
91
+ images = batch["image"]
92
+ activation = self.model.encoder(images)
93
+ avg_loss = torch.zeros([1], dtype=torch.float64).to(images.device)
94
+
95
+ height = []
96
+ width = []
97
+ for layer_idx, layer in enumerate(self.model.pool_layers):
98
+ encoder_activations = activation[layer].detach() # BxCxHxW
99
+
100
+ batch_size, dim_feature_vector, im_height, im_width = encoder_activations.size()
101
+ image_size = im_height * im_width
102
+ embedding_length = batch_size * image_size # number of rows in the conditional vector
103
+
104
+ height.append(im_height)
105
+ width.append(im_width)
106
+ # repeats positional encoding for the entire batch 1 C H W to B C H W
107
+ pos_encoding = einops.repeat(
108
+ positional_encoding_2d(self.model.condition_vector, im_height, im_width).unsqueeze(0),
109
+ "b c h w-> (tile b) c h w",
110
+ tile=batch_size,
111
+ ).to(images.device)
112
+ c_r = einops.rearrange(pos_encoding, "b c h w -> (b h w) c") # BHWxP
113
+ e_r = einops.rearrange(encoder_activations, "b c h w -> (b h w) c") # BHWxC
114
+ perm = torch.randperm(embedding_length) # BHW
115
+ decoder = self.model.decoders[layer_idx].to(images.device)
116
+
117
+ fiber_batches = embedding_length // self.model.fiber_batch_size # number of fiber batches
118
+ assert fiber_batches > 0, "Make sure we have enough fibers, otherwise decrease N or batch-size!"
119
+
120
+ for batch_num in range(fiber_batches): # per-fiber processing
121
+ opt.zero_grad()
122
+ if batch_num < (fiber_batches - 1):
123
+ idx = torch.arange(
124
+ batch_num * self.model.fiber_batch_size, (batch_num + 1) * self.model.fiber_batch_size
125
+ )
126
+ else: # When non-full batch is encountered batch_num * N will go out of bounds
127
+ idx = torch.arange(batch_num * self.model.fiber_batch_size, embedding_length)
128
+ # get random vectors
129
+ c_p = c_r[perm[idx]] # NxP
130
+ e_p = e_r[perm[idx]] # NxC
131
+ # decoder returns the transformed variable z and the log Jacobian determinant
132
+ p_u, log_jac_det = decoder(e_p, [c_p])
133
+ #
134
+ decoder_log_prob = get_logp(dim_feature_vector, p_u, log_jac_det)
135
+ log_prob = decoder_log_prob / dim_feature_vector # likelihood per dim
136
+ loss = -F.logsigmoid(log_prob)
137
+ self.manual_backward(loss.mean())
138
+ opt.step()
139
+ avg_loss += loss.sum()
140
+
141
+ return {"loss": avg_loss}
142
+
143
+ def validation_step(self, batch, _): # pylint: disable=arguments-differ
144
+ """Validation Step of CFLOW.
145
+
146
+ Similar to the training step, encoder features
147
+ are extracted from the CNN for each batch, and anomaly
148
+ map is computed.
149
+
150
+ Args:
151
+ batch: Input batch
152
+ _: Index of the batch.
153
+
154
+ Returns:
155
+ Dictionary containing images, anomaly maps, true labels and masks.
156
+ These are required in `validation_epoch_end` for feature concatenation.
157
+
158
+ """
159
+ batch["anomaly_maps"] = self.model(batch["image"])
160
+
161
+ return batch
anomalib/models/cflow/torch_model.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PyTorch model for CFlow model implementation."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from typing import List, Union
18
+
19
+ import einops
20
+ import torch
21
+ import torchvision
22
+ from omegaconf import DictConfig, ListConfig
23
+ from torch import nn
24
+
25
+ from anomalib.models.cflow.anomaly_map import AnomalyMapGenerator
26
+ from anomalib.models.cflow.utils import cflow_head, get_logp, positional_encoding_2d
27
+ from anomalib.models.components import FeatureExtractor
28
+
29
+
30
+ class CflowModel(nn.Module):
31
+ """CFLOW: Conditional Normalizing Flows."""
32
+
33
+ def __init__(self, hparams: Union[DictConfig, ListConfig]):
34
+ super().__init__()
35
+
36
+ self.backbone = getattr(torchvision.models, hparams.model.backbone)
37
+ self.fiber_batch_size = hparams.dataset.fiber_batch_size
38
+ self.condition_vector: int = hparams.model.condition_vector
39
+ self.dec_arch = hparams.model.decoder
40
+ self.pool_layers = hparams.model.layers
41
+
42
+ self.encoder = FeatureExtractor(backbone=self.backbone(pretrained=True), layers=self.pool_layers)
43
+ self.pool_dims = self.encoder.out_dims
44
+ self.decoders = nn.ModuleList(
45
+ [
46
+ cflow_head(
47
+ condition_vector=self.condition_vector,
48
+ coupling_blocks=hparams.model.coupling_blocks,
49
+ clamp_alpha=hparams.model.clamp_alpha,
50
+ n_features=pool_dim,
51
+ permute_soft=hparams.model.soft_permutation,
52
+ )
53
+ for pool_dim in self.pool_dims
54
+ ]
55
+ )
56
+
57
+ # encoder model is fixed
58
+ for parameters in self.encoder.parameters():
59
+ parameters.requires_grad = False
60
+
61
+ self.anomaly_map_generator = AnomalyMapGenerator(
62
+ image_size=tuple(hparams.model.input_size), pool_layers=self.pool_layers
63
+ )
64
+
65
+ def forward(self, images):
66
+ """Forward-pass images into the network to extract encoder features and compute probability.
67
+
68
+ Args:
69
+ images: Batch of images.
70
+
71
+ Returns:
72
+ Predicted anomaly maps.
73
+
74
+ """
75
+
76
+ self.encoder.eval()
77
+ self.decoders.eval()
78
+ with torch.no_grad():
79
+ activation = self.encoder(images)
80
+
81
+ distribution = [torch.Tensor(0).to(images.device) for _ in self.pool_layers]
82
+
83
+ height: List[int] = []
84
+ width: List[int] = []
85
+ for layer_idx, layer in enumerate(self.pool_layers):
86
+ encoder_activations = activation[layer] # BxCxHxW
87
+
88
+ batch_size, dim_feature_vector, im_height, im_width = encoder_activations.size()
89
+ image_size = im_height * im_width
90
+ embedding_length = batch_size * image_size # number of rows in the conditional vector
91
+
92
+ height.append(im_height)
93
+ width.append(im_width)
94
+ # repeats positional encoding for the entire batch 1 C H W to B C H W
95
+ pos_encoding = einops.repeat(
96
+ positional_encoding_2d(self.condition_vector, im_height, im_width).unsqueeze(0),
97
+ "b c h w-> (tile b) c h w",
98
+ tile=batch_size,
99
+ ).to(images.device)
100
+ c_r = einops.rearrange(pos_encoding, "b c h w -> (b h w) c") # BHWxP
101
+ e_r = einops.rearrange(encoder_activations, "b c h w -> (b h w) c") # BHWxC
102
+ decoder = self.decoders[layer_idx].to(images.device)
103
+
104
+ # Sometimes during validation, the last batch E / N is not a whole number. Hence we need to add 1.
105
+ # It is assumed that during training that E / N is a whole number as no errors were discovered during
106
+ # testing. In case it is observed in the future, we can use only this line and ensure that FIB is at
107
+ # least 1 or set `drop_last` in the dataloader to drop the last non-full batch.
108
+ fiber_batches = embedding_length // self.fiber_batch_size + int(
109
+ embedding_length % self.fiber_batch_size > 0
110
+ )
111
+
112
+ for batch_num in range(fiber_batches): # per-fiber processing
113
+ if batch_num < (fiber_batches - 1):
114
+ idx = torch.arange(batch_num * self.fiber_batch_size, (batch_num + 1) * self.fiber_batch_size)
115
+ else: # When non-full batch is encountered batch_num+1 * N will go out of bounds
116
+ idx = torch.arange(batch_num * self.fiber_batch_size, embedding_length)
117
+ c_p = c_r[idx] # NxP
118
+ e_p = e_r[idx] # NxC
119
+ # decoder returns the transformed variable z and the log Jacobian determinant
120
+ with torch.no_grad():
121
+ p_u, log_jac_det = decoder(e_p, [c_p])
122
+ #
123
+ decoder_log_prob = get_logp(dim_feature_vector, p_u, log_jac_det)
124
+ log_prob = decoder_log_prob / dim_feature_vector # likelihood per dim
125
+ distribution[layer_idx] = torch.cat((distribution[layer_idx], log_prob))
126
+
127
+ output = self.anomaly_map_generator(distribution=distribution, height=height, width=width)
128
+ self.decoders.train()
129
+
130
+ return output.to(images.device)
anomalib/models/cflow/utils.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for CFlow implementation."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ import logging
18
+ import math
19
+
20
+ import numpy as np
21
+ import torch
22
+ from torch import nn
23
+
24
+ from anomalib.models.components.freia.framework import SequenceINN
25
+ from anomalib.models.components.freia.modules import AllInOneBlock
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def get_logp(dim_feature_vector: int, p_u: torch.Tensor, logdet_j: torch.Tensor) -> torch.Tensor:
31
+ """Returns the log likelihood estimation.
32
+
33
+ Args:
34
+ dim_feature_vector (int): Dimensions of the condition vector
35
+ p_u (torch.Tensor): Random variable u
36
+ logdet_j (torch.Tensor): log of determinant of jacobian returned from the invertable decoder
37
+
38
+ Returns:
39
+ torch.Tensor: Log probability
40
+ """
41
+ ln_sqrt_2pi = -np.log(np.sqrt(2 * np.pi)) # ln(sqrt(2*pi))
42
+ logp = dim_feature_vector * ln_sqrt_2pi - 0.5 * torch.sum(p_u**2, 1) + logdet_j
43
+ return logp
44
+
45
+
46
+ def positional_encoding_2d(condition_vector: int, height: int, width: int) -> torch.Tensor:
47
+ """Creates embedding to store relative position of the feature vector using sine and cosine functions.
48
+
49
+ Args:
50
+ condition_vector (int): Length of the condition vector
51
+ height (int): H of the positions
52
+ width (int): W of the positions
53
+
54
+ Raises:
55
+ ValueError: Cannot generate encoding with conditional vector length not as multiple of 4
56
+
57
+ Returns:
58
+ torch.Tensor: condition_vector x HEIGHT x WIDTH position matrix
59
+ """
60
+ if condition_vector % 4 != 0:
61
+ raise ValueError(f"Cannot use sin/cos positional encoding with odd dimension (got dim={condition_vector})")
62
+ pos_encoding = torch.zeros(condition_vector, height, width)
63
+ # Each dimension use half of condition_vector
64
+ condition_vector = condition_vector // 2
65
+ div_term = torch.exp(torch.arange(0.0, condition_vector, 2) * -(math.log(1e4) / condition_vector))
66
+ pos_w = torch.arange(0.0, width).unsqueeze(1)
67
+ pos_h = torch.arange(0.0, height).unsqueeze(1)
68
+ pos_encoding[0:condition_vector:2, :, :] = (
69
+ torch.sin(pos_w * div_term).transpose(0, 1).unsqueeze(1).repeat(1, height, 1)
70
+ )
71
+ pos_encoding[1:condition_vector:2, :, :] = (
72
+ torch.cos(pos_w * div_term).transpose(0, 1).unsqueeze(1).repeat(1, height, 1)
73
+ )
74
+ pos_encoding[condition_vector::2, :, :] = (
75
+ torch.sin(pos_h * div_term).transpose(0, 1).unsqueeze(2).repeat(1, 1, width)
76
+ )
77
+ pos_encoding[condition_vector + 1 :: 2, :, :] = (
78
+ torch.cos(pos_h * div_term).transpose(0, 1).unsqueeze(2).repeat(1, 1, width)
79
+ )
80
+ return pos_encoding
81
+
82
+
83
+ def subnet_fc(dims_in: int, dims_out: int):
84
+ """Subnetwork which predicts the affine coefficients.
85
+
86
+ Args:
87
+ dims_in (int): input dimensions
88
+ dims_out (int): output dimensions
89
+
90
+ Returns:
91
+ nn.Sequential: Feed-forward subnetwork
92
+ """
93
+ return nn.Sequential(nn.Linear(dims_in, 2 * dims_in), nn.ReLU(), nn.Linear(2 * dims_in, dims_out))
94
+
95
+
96
+ def cflow_head(
97
+ condition_vector: int, coupling_blocks: int, clamp_alpha: float, n_features: int, permute_soft: bool = False
98
+ ) -> SequenceINN:
99
+ """Create invertible decoder network.
100
+
101
+ Args:
102
+ condition_vector (int): length of the condition vector
103
+ coupling_blocks (int): number of coupling blocks to build the decoder
104
+ clamp_alpha (float): clamping value to avoid exploding values
105
+ n_features (int): number of decoder features
106
+ permute_soft (bool): Whether to sample the permutation matrix :math:`R` from :math:`SO(N)`,
107
+ or to use hard permutations instead. Note, ``permute_soft=True`` is very slow
108
+ when working with >512 dimensions.
109
+
110
+ Returns:
111
+ SequenceINN: decoder network block
112
+ """
113
+ coder = SequenceINN(n_features)
114
+ logger.info("CNF coder: %d", n_features)
115
+ for _ in range(coupling_blocks):
116
+ coder.append(
117
+ AllInOneBlock,
118
+ cond=0,
119
+ cond_shape=(condition_vector,),
120
+ subnet_constructor=subnet_fc,
121
+ affine_clamping=clamp_alpha,
122
+ global_affine_type="SOFTPLUS",
123
+ permute_soft=permute_soft,
124
+ )
125
+ return coder
anomalib/models/components/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Components used within the models."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .base import AnomalyModule, DynamicBufferModule
18
+ from .dimensionality_reduction import PCA, SparseRandomProjection
19
+ from .feature_extractors import FeatureExtractor
20
+ from .sampling import KCenterGreedy
21
+ from .stats import GaussianKDE, MultiVariateGaussian
22
+
23
+ __all__ = [
24
+ "AnomalyModule",
25
+ "DynamicBufferModule",
26
+ "PCA",
27
+ "SparseRandomProjection",
28
+ "FeatureExtractor",
29
+ "KCenterGreedy",
30
+ "GaussianKDE",
31
+ "MultiVariateGaussian",
32
+ ]
anomalib/models/components/base/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base classes for all anomaly components."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .anomaly_module import AnomalyModule
18
+ from .dynamic_module import DynamicBufferModule
19
+
20
+ __all__ = ["AnomalyModule", "DynamicBufferModule"]
anomalib/models/components/base/anomaly_module.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base Anomaly Module for Training Task."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from abc import ABC
18
+ from typing import Any, List, Optional, Union
19
+
20
+ import pytorch_lightning as pl
21
+ from omegaconf import DictConfig, ListConfig
22
+ from pytorch_lightning.callbacks.base import Callback
23
+ from torch import Tensor, nn
24
+
25
+ from anomalib.utils.metrics import (
26
+ AdaptiveThreshold,
27
+ AnomalyScoreDistribution,
28
+ MinMax,
29
+ get_metrics,
30
+ )
31
+
32
+
33
+ class AnomalyModule(pl.LightningModule, ABC):
34
+ """AnomalyModule to train, validate, predict and test images.
35
+
36
+ Acts as a base class for all the Anomaly Modules in the library.
37
+
38
+ Args:
39
+ params (Union[DictConfig, ListConfig]): Configuration
40
+ """
41
+
42
+ def __init__(self, params: Union[DictConfig, ListConfig]):
43
+
44
+ super().__init__()
45
+ # Force the type for hparams so that it works with OmegaConfig style of accessing
46
+ self.hparams: Union[DictConfig, ListConfig] # type: ignore
47
+ self.save_hyperparameters(params)
48
+ self.loss: Tensor
49
+ self.callbacks: List[Callback]
50
+
51
+ self.image_threshold = AdaptiveThreshold(self.hparams.model.threshold.image_default).cpu()
52
+ self.pixel_threshold = AdaptiveThreshold(self.hparams.model.threshold.pixel_default).cpu()
53
+
54
+ self.training_distribution = AnomalyScoreDistribution().cpu()
55
+ self.min_max = MinMax().cpu()
56
+
57
+ self.model: nn.Module
58
+
59
+ # metrics
60
+ self.image_metrics, self.pixel_metrics = get_metrics(self.hparams)
61
+ self.image_metrics.set_threshold(self.hparams.model.threshold.image_default)
62
+ self.pixel_metrics.set_threshold(self.hparams.model.threshold.pixel_default)
63
+
64
+ def forward(self, batch): # pylint: disable=arguments-differ
65
+ """Forward-pass input tensor to the module.
66
+
67
+ Args:
68
+ batch (Tensor): Input Tensor
69
+
70
+ Returns:
71
+ Tensor: Output tensor from the model.
72
+ """
73
+ return self.model(batch)
74
+
75
+ def validation_step(self, batch, batch_idx) -> dict: # type: ignore # pylint: disable=arguments-differ
76
+ """To be implemented in the subclasses."""
77
+ raise NotImplementedError
78
+
79
+ def predict_step(self, batch: Any, batch_idx: int, _dataloader_idx: Optional[int] = None) -> Any:
80
+ """Step function called during :meth:`~pytorch_lightning.trainer.trainer.Trainer.predict`.
81
+
82
+ By default, it calls :meth:`~pytorch_lightning.core.lightning.LightningModule.forward`.
83
+ Override to add any processing logic.
84
+
85
+ Args:
86
+ batch (Tensor): Current batch
87
+ batch_idx (int): Index of current batch
88
+ _dataloader_idx (int): Index of the current dataloader
89
+
90
+ Return:
91
+ Predicted output
92
+ """
93
+ outputs = self.validation_step(batch, batch_idx)
94
+ self._post_process(outputs)
95
+ outputs["pred_labels"] = outputs["pred_scores"] >= self.image_threshold.value
96
+ if "anomaly_maps" in outputs.keys():
97
+ outputs["pred_masks"] = outputs["anomaly_maps"] >= self.pixel_threshold.value
98
+ return outputs
99
+
100
+ def test_step(self, batch, _): # pylint: disable=arguments-differ
101
+ """Calls validation_step for anomaly map/score calculation.
102
+
103
+ Args:
104
+ batch (Tensor): Input batch
105
+ _: Index of the batch.
106
+
107
+ Returns:
108
+ Dictionary containing images, features, true labels and masks.
109
+ These are required in `validation_epoch_end` for feature concatenation.
110
+ """
111
+ return self.validation_step(batch, _)
112
+
113
+ def validation_step_end(self, val_step_outputs): # pylint: disable=arguments-differ
114
+ """Called at the end of each validation step."""
115
+ self._outputs_to_cpu(val_step_outputs)
116
+ self._post_process(val_step_outputs)
117
+ return val_step_outputs
118
+
119
+ def test_step_end(self, test_step_outputs): # pylint: disable=arguments-differ
120
+ """Called at the end of each test step."""
121
+ self._outputs_to_cpu(test_step_outputs)
122
+ self._post_process(test_step_outputs)
123
+ return test_step_outputs
124
+
125
+ def validation_epoch_end(self, outputs):
126
+ """Compute threshold and performance metrics.
127
+
128
+ Args:
129
+ outputs: Batch of outputs from the validation step
130
+ """
131
+ if self.hparams.model.threshold.adaptive:
132
+ self._compute_adaptive_threshold(outputs)
133
+ self._collect_outputs(self.image_metrics, self.pixel_metrics, outputs)
134
+ self._log_metrics()
135
+
136
+ def test_epoch_end(self, outputs):
137
+ """Compute and save anomaly scores of the test set.
138
+
139
+ Args:
140
+ outputs: Batch of outputs from the validation step
141
+ """
142
+ self._collect_outputs(self.image_metrics, self.pixel_metrics, outputs)
143
+ self._log_metrics()
144
+
145
+ def _compute_adaptive_threshold(self, outputs):
146
+ self._collect_outputs(self.image_threshold, self.pixel_threshold, outputs)
147
+ self.image_threshold.compute()
148
+ if "mask" in outputs[0].keys() and "anomaly_maps" in outputs[0].keys():
149
+ self.pixel_threshold.compute()
150
+ else:
151
+ self.pixel_threshold.value = self.image_threshold.value
152
+
153
+ self.image_metrics.set_threshold(self.image_threshold.value.item())
154
+ self.pixel_metrics.set_threshold(self.pixel_threshold.value.item())
155
+
156
+ def _collect_outputs(self, image_metric, pixel_metric, outputs):
157
+ for output in outputs:
158
+ image_metric.cpu()
159
+ image_metric.update(output["pred_scores"], output["label"].int())
160
+ if "mask" in output.keys() and "anomaly_maps" in output.keys():
161
+ pixel_metric.cpu()
162
+ pixel_metric.update(output["anomaly_maps"].flatten(), output["mask"].flatten().int())
163
+
164
+ def _post_process(self, outputs):
165
+ """Compute labels based on model predictions."""
166
+ if "pred_scores" not in outputs and "anomaly_maps" in outputs:
167
+ outputs["pred_scores"] = (
168
+ outputs["anomaly_maps"].reshape(outputs["anomaly_maps"].shape[0], -1).max(dim=1).values
169
+ )
170
+
171
+ def _outputs_to_cpu(self, output):
172
+ # for output in outputs:
173
+ for key, value in output.items():
174
+ if isinstance(value, Tensor):
175
+ output[key] = value.cpu()
176
+
177
+ def _log_metrics(self):
178
+ """Log computed performance metrics."""
179
+ self.log_dict(self.image_metrics)
180
+ if self.pixel_metrics.update_called:
181
+ self.log_dict(self.pixel_metrics)
anomalib/models/components/base/dynamic_module.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dynamic Buffer Module."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from abc import ABC
18
+
19
+ from torch import Tensor, nn
20
+
21
+
22
+ class DynamicBufferModule(ABC, nn.Module):
23
+ """Torch module that allows loading variables from the state dict even in the case of shape mismatch."""
24
+
25
+ def get_tensor_attribute(self, attribute_name: str) -> Tensor:
26
+ """Get attribute of the tensor given the name.
27
+
28
+ Args:
29
+ attribute_name (str): Name of the tensor
30
+
31
+ Raises:
32
+ ValueError: `attribute_name` is not a torch Tensor
33
+
34
+ Returns:
35
+ Tensor: Tensor attribute
36
+ """
37
+ attribute = self.__getattr__(attribute_name)
38
+ if isinstance(attribute, Tensor):
39
+ return attribute
40
+
41
+ raise ValueError(f"Attribute with name '{attribute_name}' is not a torch Tensor")
42
+
43
+ def _load_from_state_dict(self, state_dict: dict, prefix: str, *args):
44
+ """Resizes the local buffers to match those stored in the state dict.
45
+
46
+ Overrides method from parent class.
47
+
48
+ Args:
49
+ state_dict (dict): State dictionary containing weights
50
+ prefix (str): Prefix of the weight file.
51
+ *args:
52
+ """
53
+ persistent_buffers = {k: v for k, v in self._buffers.items() if k not in self._non_persistent_buffers_set}
54
+ local_buffers = {k: v for k, v in persistent_buffers.items() if v is not None}
55
+
56
+ for param in local_buffers.keys():
57
+ for key in state_dict.keys():
58
+ if key.startswith(prefix) and key[len(prefix) :].split(".")[0] == param:
59
+ if not local_buffers[param].shape == state_dict[key].shape:
60
+ attribute = self.get_tensor_attribute(param)
61
+ attribute.resize_(state_dict[key].shape)
62
+
63
+ super()._load_from_state_dict(state_dict, prefix, *args)
anomalib/models/components/dimensionality_reduction/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algorithms for decomposition and dimensionality reduction."""
2
+
3
+ # Copyright (C) 2020 Intel Corporation
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions
15
+ # and limitations under the License.
16
+
17
+ from .pca import PCA
18
+ from .random_projection import SparseRandomProjection
19
+
20
+ __all__ = ["PCA", "SparseRandomProjection"]