Spaces:
Build error
Build error
julien.blanchon
commited on
Commit
·
c8c12e9
1
Parent(s):
7caa065
add app
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .devcontainer/devcontainer.json +56 -0
- .dockerignore +13 -0
- .gitattributes +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +35 -0
- .github/ISSUE_TEMPLATE/feature_request.md +20 -0
- .github/pull_request_template.md +22 -0
- .github/workflows/docs.yml +66 -0
- .github/workflows/nightly.yml +29 -0
- .github/workflows/pre_merge.yml +32 -0
- .gitignore +173 -0
- .pre-commit-config.yaml +70 -0
- CHANGELOG.md +152 -0
- CITATION.cff +92 -0
- CODE_OF_CONDUCT.md +128 -0
- CONTRIBUTING.md +64 -0
- Dockerfile +38 -0
- LICENSE +202 -0
- MANIFEST.in +1 -0
- README copy.md +253 -0
- anomalib/__init__.py +17 -0
- anomalib/config/__init__.py +23 -0
- anomalib/config/config.py +170 -0
- anomalib/data/__init__.py +104 -0
- anomalib/data/btech.py +453 -0
- anomalib/data/folder.py +540 -0
- anomalib/data/inference.py +67 -0
- anomalib/data/mvtec.py +457 -0
- anomalib/data/utils/__init__.py +20 -0
- anomalib/data/utils/download.py +195 -0
- anomalib/data/utils/image.py +91 -0
- anomalib/data/utils/split.py +94 -0
- anomalib/deploy/__init__.py +20 -0
- anomalib/deploy/inferencers/__init__.py +21 -0
- anomalib/deploy/inferencers/base.py +204 -0
- anomalib/deploy/inferencers/openvino.py +149 -0
- anomalib/deploy/inferencers/torch.py +164 -0
- anomalib/deploy/optimize.py +89 -0
- anomalib/models/__init__.py +75 -0
- anomalib/models/cflow/README.md +49 -0
- anomalib/models/cflow/__init__.py +19 -0
- anomalib/models/cflow/anomaly_map.py +97 -0
- anomalib/models/cflow/config.yaml +101 -0
- anomalib/models/cflow/lightning_model.py +161 -0
- anomalib/models/cflow/torch_model.py +130 -0
- anomalib/models/cflow/utils.py +125 -0
- anomalib/models/components/__init__.py +32 -0
- anomalib/models/components/base/__init__.py +20 -0
- anomalib/models/components/base/anomaly_module.py +181 -0
- anomalib/models/components/base/dynamic_module.py +63 -0
- 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 |
+
email: [email protected]
|
11 |
+
affiliation: Intel
|
12 |
+
- given-names: Dick
|
13 |
+
family-names: Ameln
|
14 |
+
email: [email protected]
|
15 |
+
affiliation: Intel
|
16 |
+
- given-names: Ashwin
|
17 |
+
family-names: Vaidya
|
18 |
+
email: [email protected]
|
19 |
+
affiliation: Intel
|
20 |
+
- given-names: Barath
|
21 |
+
family-names: Lakshmanan
|
22 |
+
email: [email protected]
|
23 |
+
affiliation: Intel
|
24 |
+
- given-names: Nilesh
|
25 |
+
family-names: Ahuja
|
26 |
+
email: [email protected]
|
27 |
+
affiliation: Intel
|
28 |
+
- given-names: Utku
|
29 |
+
family-names: Genc
|
30 |
+
email: [email protected]
|
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 |
+
email: [email protected]
|
41 |
+
affiliation: Intel
|
42 |
+
- given-names: Dick
|
43 |
+
family-names: Ameln
|
44 |
+
email: [email protected]
|
45 |
+
affiliation: Intel
|
46 |
+
- given-names: Ashwin
|
47 |
+
family-names: Vaidya
|
48 |
+
email: [email protected]
|
49 |
+
affiliation: Intel
|
50 |
+
- given-names: Barath
|
51 |
+
family-names: Lakshmanan
|
52 |
+
email: [email protected]
|
53 |
+
affiliation: Intel
|
54 |
+
- given-names: Nilesh
|
55 |
+
family-names: Ahuja
|
56 |
+
email: [email protected]
|
57 |
+
affiliation: Intel
|
58 |
+
- given-names: Utku
|
59 |
+
family-names: Genc
|
60 |
+
email: [email protected]
|
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 |
+
[]()
|
14 |
+
[]()
|
15 |
+
[]()
|
16 |
+
[]()
|
17 |
+
[](https://github.com/openvinotoolkit/anomalib/actions/workflows/nightly.yml)
|
18 |
+
[](https://github.com/openvinotoolkit/anomalib/actions/workflows/pre_merge.yml)
|
19 |
+
[](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 |
+

|
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 |
+

|
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 |
+

|
46 |
+
|
47 |
+

|
48 |
+
|
49 |
+

|
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"]
|