Skip to content
Snippets Groups Projects
Commit aebdfe0c authored by Lelio Lardon's avatar Lelio Lardon
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 927 additions and 0 deletions
mvtec/
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information, see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact the maintainer of this repository at [lelio.lardon@lis-lab.fr] with any additional questions or comments.
# Contributing Guidelines
Thank you for your interest in contributing to this project. Whether it's a bug report, new feature, correction, or additional
documentation, we greatly value feedback and contributions from the community.
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
information to effectively respond to your bug report or contribution.
## Reporting Bugs/Feature Requests
We welcome you to use the GitHub issue tracker to report bugs or suggest features.
When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
* A reproducible test case or series of steps
* The version of our code being used
* Any modifications you've made relevant to the bug
* Anything unusual about your environment or deployment
## Contributing via Pull Requests
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
1. You are working against the latest source on the *main* branch.
2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
To send us a pull request, please:
1. Fork the repository.
2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
3. Ensure local tests pass.
4. Commit to your fork using clear commit messages.
5. Send us a pull request, answering any default questions in the pull request interface.
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
## Finding contributions to work on
Looking at the existing issues is a great way to find something to contribute on. As this projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq)
or contact the maintainer of this repository at [lelio.lardon@lis-lab.fr] with any additional questions or comments.
## Licensing
See the [LICENSE](LICENSE) file for licensing details. All contributions must comply with the terms of the Apache License, Version 2.0.
\ No newline at end of file
#!/bin/bash
# Get the mvtec directory
current_dir=$(pwd)
datapath="$current_dir/mvtec"
# Variables to modify
Is_Unified=true
modelfolder="results/MVTEC_results/Unified_PCA32_Early_stopping" # Folder where evaluate model is stored
datasets=("bottle" "cable" "capsule" "carpet" "grid" "hazelnut" "leather" "metal_nut" "pill" "screw" "tile" "toothbrush" "transistor" "wood" "zipper")
Name_file_result="Evaluate_result/Unified_PCA32_Early_stopping_eval"
# Define dataset flags dynamically
dataset_flags=($(for dataset in "${datasets[@]}"; do echo '-d '"${dataset}"; done))
# Define model flags based on Is_Unified condition
if $Is_Unified; then
model_flags=("-p${modelfolder}/models/Unified")
else
model_flags=()
for dataset in "${datasets[@]}"; do
model_flags+=("-p${modelfolder}/models/mvtec_${dataset}")
done
fi
# Construct the command
command=(
python3 bin/load_and_evaluate_patchcore.py
--gpu 0
--seed 0
--is_unified # Comment this line to disable unification
"$Name_file_result"
patch_core_loader
"${model_flags[@]}"
--faiss_on_gpu
--is_pca # Comment this line to disable PCA
dataset
--resize 256 --imagesize 224
"${dataset_flags[@]}" \
mvtec "$datapath"
)
# Execute the command
"${command[@]}"
import torch
import numpy as np
from PIL import Image
from torchvision import transforms
import patchcore.patchcore
from matplotlib import pyplot as plt
import os
import argparse
# #Parameters
# path_model = "Inference/models/PCA32"
# path_img = "Inference/image/Hazelnut_hole_004.png"
# is_pca = True # Flag true ou None
# faiss_on_gpu = True
# faiss_num_workers = 8
###
### Parser
parser = argparse.ArgumentParser(description="Inference on an image using a trained model")
# required arguments
parser.add_argument("path_model", type=str, help="Path to the model folder")
parser.add_argument("path_img", type=str, help="Path to the image")
# optional argument
parser.add_argument("--is_pca", action="store_true", default=None, help="Use PCA if activate (default: None)")
parser.add_argument("--faiss_on_gpu", action="store_true", default=True, help="Use FAISS on GPU (default: True)")
parser.add_argument("--faiss_num_workers", type=int, default=8, help="Number of workers for FAISS (default: 8)")
args = parser.parse_args()
path_model = args.path_model
path_img = args.path_img
if args.is_pca is True:
is_pca = True
else:
is_pca = None
faiss_on_gpu = args.faiss_on_gpu
faiss_num_workers = args.faiss_num_workers
#### End Parser
nn_method = patchcore.common.FaissNN(faiss_on_gpu, faiss_num_workers)
# Charger un modèle entraîné (préciser le chemin)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = patchcore.patchcore.PatchCore(device)
model.load_from_path(path_model, device, nn_method, is_pca=is_pca)
# Fonction pour prétraiter une image
def preprocess_image(image_path):
transform = transforms.Compose([
transforms.Resize((224, 224)), # Adapter à l'entrée du modèle
transforms.ToTensor(), # Convertir en tenseur PyTorch
])
image = Image.open(image_path).convert("RGB")
tensor_image = transform(image).unsqueeze(0) # Ajouter une dimension batch
return tensor_image.to(device)
# Charger une image et la passer au modèle
image_path = path_img
image_tensor = preprocess_image(image_path)
# Faire l'inférence
scores, masks = model._predict(image_tensor)
if "result" not in os.listdir("Inference"):
os.mkdir("Inference/result")
name_npy = "Inference/result/" + path_img.split("/")[-1].split(".")[0] + ".npy"
np.save(name_npy, masks[0])
print('Result save in :', name_npy)
\ No newline at end of file
# Inference of a Model on a Single Image
The `Inference.py` file allows loading and running a PatchCore LAD model on an image.
The output is a `.npy` file containing the segmentation map of the processed image.
## Model Folder Structure (path_model)
The model directory (path_model) must contain the following files:
- nnscorer_search_index.faiss – FAISS index for nearest neighbor search.
- patchcore_params.pkl – Model parameters.
If using a PCA-based model, the directory must also include:
- pca.pkl – PCA transformation file.
- scaler.pkl – Data normalization scaler.
## Usage Tutorial
The Python script requires different input parameters:
**Required:**
- path_model : Path to the folder containing the model files.
- path_img : Path to the image on which the model will be applied.
- --is_pca : Required if using a PCA-based model.
**Optional:**
- --faiss_on_gpu : Use FAISS on GPU (default: True)
- --faiss_num_workers : Number of workers for FAISS (default: 8)
You can use `-h` to get help when running the program.
Before running the script, you must set the PYTHONPATH environment variable:
`export PYTHONPATH=src`
**Example Usage**
With PCA:
```
python Inference/Inference.py --is_pca Inference/models/PCA32/ Inference/image/Hazelnut_hole_004.png
```
Without PCA :
```
python Inference/Inference.py Inference/models/No_PCA/ Inference/image/Hazelnut_hole_004.png
```
\ No newline at end of file
LICENSE 0 → 100644
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
NOTICE 0 → 100644
This project is a modified version of PatchCore (https://github.com/amazon-science/patchcore-inspection), originally licensed under the Apache License, Version 2.0.
Modifications by LARDON Lelio, GUIS Vincente and BUSVELLE Eric; Laboratory LIS, UMR 7020 University of Toulon, Aix Marseille Univ., FRANCE, 2025.
README.md 0 → 100644
# LAD : Lightweight Anomaly Detection with PatchCore, Feature Reduction through PCA for Embedded Applications
![Graphical Abstract](images/Graphical_abstract.png)
This repository provides a modified version of PatchCore, as proposed in the LAD article.
The modifications include the addition of three key elements:
- **Unification** of classes for datasets containing multiple classes.
- Incorporation of a **dimensionality reduction technique : PCA**. It significantly reduce the size of the memory bank stored. The features are reduced to a size specified by the user.
- An **early stopping** technique to enhance the efficiency of coreset selection. This technique requires adjusting three parameters: the selection limit percentage `l` , the observation history size `h`, and the distance threshold `tau` at which the process stops. Adjusting these parameters influences the number of retained elements.
For a comprehensive understanding and proper usage of these features, it is recommended to refer to the LAD article.
The implementation is based on the [PatchCore's original repository](https://github.com/amazon-science/patchcore-inspection). Usage remains consistent with this repository, so you can refer to it for any issues unrelated to our modifications.
Our modifications allow you to use these new elements individually or in combination with each other.
# requirements
Python '>=3.7'
# Quick Guide
For this tutorial, we will use the MVTec-AD dataset. The first step is to [download it](https://www.mvtec.com/company/research/datasets/mvtec-ad/downloads).
The second step is to install the necessary libraries using `pip install -r requirements.txt` file, preferably in a virtual environment. Then you need to export and set the PYTHONPATH environment variable with `export PYTHONPATH=src`
If you have an error with the `faiss` module and the `GpuIndexFlatL2` attribute, try uninstalling faiss (faiss-cpu and/or faiss-gpu) and then reinstalling faiss-gpu.
You can find pre-trained LAD-PatchCore models on the MVTec-AD dataset [here](https://sync.lis-lab.fr/index.php/s/yYPm6NC9p4nWpkQ).
## Using bash file
The quickest way to test this method is by using the `Training_LAD.sh` and `Evaluation_LAD.sh` files. These files allow you to execute more friendly the command lines described below. Feel free to modify the execution parameters in the file, but pay attention to the path towards the mvtec folder
### Command line training
The `datapath` variable specifies the local path to this database, and `datasets` indicates the classes to be used. The first step is to set up these variables.
```
datapath=/path_to_mvtec_folder/mvtec datasets=('bottle' 'cable' 'capsule' 'carpet' 'grid' 'hazelnut'
'leather' 'metal_nut' 'pill' 'screw' 'tile' 'toothbrush' 'transistor' 'wood' 'zipper')
dataset_flags=($(for dataset in "${datasets[@]}"; do echo '-d '$dataset; done))
```
Next, initiate the training with the chosen parameters.
```
python bin/run_patchcore.py --gpu 0 --seed 0 --save_patchcore_model --is_unified\
--log_group Unified_PCA32_ES_50_20_2e-6 --log_online --log_project results \
patch_core -b wideresnet50 -le layer2 -le layer3 --faiss_on_gpu \
--pretrain_embed_dimension 1024 --target_embed_dimension 1024 --anomaly_scorer_num_nn 1 --patchsize 3 --is_pca 32 \
sampler -p 0.1 --is_early_stopping -l 0.5 -h 20 -tau 2e-6 approx_greedy_coreset dataset --resize 256 --imagesize 224 "${dataset_flags[@]}" mvtec $datapath
```
Here, we run with `224x224` images, using a `WideResNet50` backbone pretrained on ImageNet. The results are stored in the `MVTecAD_Results/Unified_PCA32_ES_50_20_2e-6` folder
We're in unified mode here, with a `32-dimensionality` reduction and parameters `l=50% h=20 TAU=2e-6` for Early Stopping.
- To choose the **unified** option, specify: `--is_unified`
- To use **PCA dimensionality reduction** and choose the feature reduction size: `--is_pca reduction_size`
- To use **early stopping** and choose the parameters: Early stopping `--is_early_stopping -l l -h h -tau tau`
The `--save_patchcore_model` option allows you to save the memory bank and the PCA eighenvalue for testing the model like pre-trained.
### Command line evaluating pretrained model
For this evaluation, it is crucial to maintain the same configuration as during training. For example, ensure that the unified setting (if used) and the same PCA configuration are applied.
The first step is to specify paths
```
datapath=/path_to_mvtec_folder/mvtec
loadpath=MVTecAD_Results
modelfolder=Unified_PCA32_ES_50_20_2e-6
savefolder=evaluated_results'/'$modelfolder
datasets=('bottle' 'cable' 'capsule' 'carpet' 'grid' 'hazelnut' 'leather' 'metal_nut' 'pill' 'screw' 'tile' 'toothbrush' 'transistor' 'wood' 'zipper')
dataset_flags=($(for dataset in "${datasets[@]}"; do echo '-d '$dataset; done))
```
If we are in unified mode:
```
model_flags=('-p '$loadpath'/'$modelfolder'/models/Unified')
```
Otherwise
```
model_flags=($(for dataset in "${datasets[@]}"; do echo '-p '$loadpath'/'$modelfolder'/models/mvtec_'$dataset; done))
```
Once the paths are correctly configured, you can run inferences as follows:
```
python bin/load_and_evaluate_patchcore.py --gpu 0 --seed 0 --is_unified $savefolder \
patch_core_loader "${model_flags[@]}" --faiss_on_gpu --is_pca \
dataset --resize 256 --imagesize 224 "${dataset_flags[@]}" mvtec $datapath
```
# References
PatchCore : K. Roth, L. Pemula, J. Zepeda, B. Schölkopf, T. Brox, P. Gehler, Towards total recall in industrial anomaly detection (2022). arXiv: 2106.08265. URL https://arxiv.org/abs/2106.08265
# Acknowledgment
This work is supported by Computer Science and Systems Laboratory (LIS/CNRS). We are grateful to Acwa Robotics, the company leading the pipe inspection project. Funds were provided by the FRANCE 2030 investment plan, Région Sud and by Bpifrance. We would also thank ALTEREO, a collaborator on this project.
This work builds upon the open-source project PatchCore, released under the Apache 2.0 License. We gratefully acknowledge the authors for their contribution to the scientific community.
# Citing
If you use our code, please cite
```
@article{lardon2025LADPatchCore,
author = {Lelio Lardon and Vincente Guis and Eric Busvelle},
title = {LAD-PatchCore: Lightweight Anomaly Detection for Embedded
Applications in Water Supply Networks Inspection},
year = {2025},
}
```
# Contributing
See [NOTICE](NOTICE), [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for more information.
# License
This project is licensed under the [Apache-2.0 license](LICENSE).
\ No newline at end of file
#!/bin/bash
# Get the current working directory
current_dir=$(pwd)
datapath="$current_dir/mvtec" # Change this path if the dataset is located elsewhere
# Variables to modify
datasets=("bottle" "cable" "capsule" "carpet" "grid" "hazelnut" "leather" "metal_nut" "pill" "screw" "tile" "toothbrush" "transistor" "wood" "zipper")
name_folder="MVTEC_results" # Name of the folder where results will be saved
name_file="Unified_PCA32_Early_stopping" # Experiment name
backbone="wideresnet50" # Model backbone
# Generate dataset flags dynamically
dataset_flags=($(for dataset in "${datasets[@]}"; do echo '-d '"${dataset}"; done))
# Construct the command
command=(
python3 bin/run_patchcore.py --gpu 0 --seed 0 # --save_segmentation_images
--save_patchcore_model
--is_unified # Comment this line to disable unification
--log_group "$name_file"
--log_project "$name_folder" results
patch_core -b "$backbone" -le layer2 -le layer3
--faiss_on_gpu --pretrain_embed_dimension 1024
--target_embed_dimension 1024 --anomaly_scorer_num_nn 1
--patchsize 3
--is_pca 32 # Comment this line to disable PCA
sampler -p 0.1
--is_early_stopping -l 0.5 -h 20 -tau 2e-6 # Comment this line to disable Early Stopping
approx_greedy_coreset
dataset --resize 256 --imagesize 224
"${dataset_flags[@]}" mvtec "$datapath"
)
# Execute the command
"${command[@]}"
File added
File added
import contextlib
import gc
import logging
import os
import sys
import click
import numpy as np
import torch
import patchcore.common
import patchcore.metrics
import patchcore.patchcore
import patchcore.sampler
import patchcore.utils
LOGGER = logging.getLogger(__name__)
_DATASETS = {"mvtec": ["patchcore.datasets.mvtec", "MVTecDataset"]}
#### Unified
from torch.utils.data import ConcatDataset
# Is_unified = os.getenv('Is_unified', False)
# Is_unified = Is_unified == 'True'
####
@click.group(chain=True)
@click.argument("results_path", type=str)
@click.option("--gpu", type=int, default=[0], multiple=True, show_default=True)
@click.option("--seed", type=int, default=0, show_default=True)
@click.option("--save_segmentation_images", is_flag=True)
@click.option("--is_unified", is_flag=True)
def main(**kwargs):
pass
@main.result_callback()
def run(methods, results_path, gpu, seed, save_segmentation_images, is_unified):
methods = {key: item for (key, item) in methods}
os.makedirs(results_path, exist_ok=True)
device = patchcore.utils.set_torch_device(gpu)
# Device context here is specifically set and used later
# because there was GPU memory-bleeding which I could only fix with
# context managers.
device_context = (
torch.cuda.device("cuda:{}".format(device.index))
if "cuda" in device.type.lower()
else contextlib.suppress()
)
result_collect = []
dataloader_iter, n_dataloaders = methods["get_dataloaders_iter"]
dataloader_iter = dataloader_iter(seed,is_unified)
if is_unified:
n_dataloaders = 1
patchcore_iter, n_patchcores = methods["get_patchcore_iter"]
patchcore_iter = patchcore_iter(device)
if not (n_dataloaders == n_patchcores or n_patchcores == 1):
raise ValueError(
"Please ensure that #PatchCores == #Datasets or #PatchCores == 1!"
)
for dataloader_count, dataloaders in enumerate(dataloader_iter):
LOGGER.info(
"Evaluating dataset [{}] ({}/{})...".format(
dataloaders["testing"].name, dataloader_count + 1, n_dataloaders
)
)
patchcore.utils.fix_seeds(seed, device)
dataset_name = dataloaders["testing"].name
with device_context:
torch.cuda.empty_cache()
if dataloader_count < n_patchcores:
PatchCore_list = next(patchcore_iter)
aggregator = {"scores": [], "segmentations": []}
for i, PatchCore in enumerate(PatchCore_list):
torch.cuda.empty_cache()
LOGGER.info(
"Embedding test data with models ({}/{})".format(
i + 1, len(PatchCore_list)
)
)
scores, segmentations, labels_gt, masks_gt = PatchCore.predict(
dataloaders["testing"]
)
aggregator["scores"].append(scores)
aggregator["segmentations"].append(segmentations)
scores = np.array(aggregator["scores"])
min_scores = scores.min(axis=-1).reshape(-1, 1)
max_scores = scores.max(axis=-1).reshape(-1, 1)
scores = (scores - min_scores) / (max_scores - min_scores)
scores = np.mean(scores, axis=0)
segmentations = np.array(aggregator["segmentations"])
min_scores = (
segmentations.reshape(len(segmentations), -1)
.min(axis=-1)
.reshape(-1, 1, 1, 1)
)
max_scores = (
segmentations.reshape(len(segmentations), -1)
.max(axis=-1)
.reshape(-1, 1, 1, 1)
)
segmentations = (segmentations - min_scores) / (max_scores - min_scores)
segmentations = np.mean(segmentations, axis=0)
if is_unified:
data_to_iterate = []
for subdataset in dataloaders["testing"].dataset.datasets:
if hasattr(subdataset, "data_to_iterate"): # Vérifier si l'attribut existe
data_to_iterate.extend(subdataset.data_to_iterate)
anomaly_labels = [x[1] != "good" for x in data_to_iterate]
else:
anomaly_labels = [
x[1] != "good" for x in dataloaders["testing"].dataset.data_to_iterate
]
# Plot Example Images.
if save_segmentation_images:
if is_unified:
data_to_iterate = []
for subdataset in dataloaders["testing"].dataset.datasets:
if hasattr(subdataset, "data_to_iterate"): # Vérifier si l'attribut existe
data_to_iterate.extend(subdataset.data_to_iterate)
image_paths = [x[2] for x in data_to_iterate]
mask_paths = [x[3] for x in data_to_iterate]
else:
image_paths = [
x[2] for x in dataloaders["testing"].dataset.data_to_iterate
]
mask_paths = [
x[3] for x in dataloaders["testing"].dataset.data_to_iterate
]
def image_transform(image):
if is_unified:
in_std = np.array(
dataloaders["testing"].dataset.datasets[0].transform_std
).reshape(-1, 1, 1)
in_mean = np.array(
dataloaders["testing"].dataset.datasets[0].transform_mean
).reshape(-1, 1, 1)
image = dataloaders["testing"].dataset.datasets[0].transform_img(image)
else:
in_std = np.array(
dataloaders["testing"].dataset.transform_std
).reshape(-1, 1, 1)
in_mean = np.array(
dataloaders["testing"].dataset.transform_mean
).reshape(-1, 1, 1)
image = dataloaders["testing"].dataset.transform_img(image)
return np.clip(
(image.numpy() * in_std + in_mean) * 255, 0, 255
).astype(np.uint8)
def mask_transform(mask):
if is_unified:
return dataloaders["testing"].dataset.datasets[0].transform_mask(mask).numpy()
return dataloaders["testing"].dataset.transform_mask(mask).numpy()
patchcore.utils.plot_segmentation_images(
results_path,
image_paths,
segmentations,
scores,
mask_paths,
image_transform=image_transform,
mask_transform=mask_transform,
)
LOGGER.info("Computing evaluation metrics.")
# Compute Image-level AUROC scores for all images.
auroc = patchcore.metrics.compute_imagewise_retrieval_metrics(
scores, anomaly_labels
)["auroc"]
# Compute PRO score & PW Auroc for all images
pixel_scores = patchcore.metrics.compute_pixelwise_retrieval_metrics(
segmentations, masks_gt
)
full_pixel_auroc = pixel_scores["auroc"]
# Compute PRO score & PW Auroc only for images with anomalies
sel_idxs = []
for i in range(len(masks_gt)):
if np.sum(masks_gt[i]) > 0:
sel_idxs.append(i)
pixel_scores = patchcore.metrics.compute_pixelwise_retrieval_metrics(
[segmentations[i] for i in sel_idxs], [masks_gt[i] for i in sel_idxs]
)
anomaly_pixel_auroc = pixel_scores["auroc"]
result_collect.append(
{
"dataset_name": dataset_name,
"instance_auroc": auroc,
"full_pixel_auroc": full_pixel_auroc,
"anomaly_pixel_auroc": anomaly_pixel_auroc,
}
)
for key, item in result_collect[-1].items():
if key != "dataset_name":
LOGGER.info("{0}: {1:3.3f}".format(key, item))
del PatchCore_list
gc.collect()
LOGGER.info("\n\n-----\n")
result_metric_names = list(result_collect[-1].keys())[1:]
result_dataset_names = [results["dataset_name"] for results in result_collect]
result_scores = [list(results.values())[1:] for results in result_collect]
patchcore.utils.compute_and_store_final_results(
results_path,
result_scores,
column_names=result_metric_names,
row_names=result_dataset_names,
)
@main.command("patch_core_loader")
# Pretraining-specific parameters.
@click.option("--patch_core_paths", "-p", type=str, multiple=True, default=[])
# NN on GPU.
@click.option("--faiss_on_gpu", is_flag=True)
@click.option("--faiss_num_workers", type=int, default=8)
@click.option("--is_pca", is_flag=True)
def patch_core_loader(patch_core_paths, faiss_on_gpu, faiss_num_workers,is_pca):
if not is_pca:
is_pca = None
def get_patchcore_iter(device):
for patch_core_path in patch_core_paths:
loaded_patchcores = []
gc.collect()
n_patchcores = len(
[x for x in os.listdir(patch_core_path) if ".faiss" in x]
)
if n_patchcores == 1:
nn_method = patchcore.common.FaissNN(faiss_on_gpu, faiss_num_workers)
patchcore_instance = patchcore.patchcore.PatchCore(device)
patchcore_instance.load_from_path(
load_path=patch_core_path, device=device, nn_method=nn_method,is_pca=is_pca
)
loaded_patchcores.append(patchcore_instance)
else:
for i in range(n_patchcores):
nn_method = patchcore.common.FaissNN(
faiss_on_gpu, faiss_num_workers
)
patchcore_instance = patchcore.patchcore.PatchCore(device)
patchcore_instance.load_from_path(
load_path=patch_core_path,
device=device,
nn_method=nn_method,
prepend="Ensemble-{}-{}_".format(i + 1, n_patchcores),
is_pca=is_pca,
)
loaded_patchcores.append(patchcore_instance)
yield loaded_patchcores
return ("get_patchcore_iter", [get_patchcore_iter, len(patch_core_paths)])
@main.command("dataset")
@click.argument("name", type=str)
@click.argument("data_path", type=click.Path(exists=True, file_okay=False))
@click.option("--subdatasets", "-d", multiple=True, type=str, required=True)
@click.option("--batch_size", default=1, type=int, show_default=True)
@click.option("--num_workers", default=8, type=int, show_default=True)
@click.option("--resize", default=256, type=int, show_default=True)
@click.option("--imagesize", default=224, type=int, show_default=True)
@click.option("--augment", is_flag=True)
def dataset(
name, data_path, subdatasets, batch_size, resize, imagesize, num_workers, augment
):
dataset_info = _DATASETS[name]
dataset_library = __import__(dataset_info[0], fromlist=[dataset_info[1]])
def get_dataloaders_iter(seed, is_unified=False):
if is_unified:
test_datasets = []
for subdataset in subdatasets:
test_datasets.append(
dataset_library.__dict__[dataset_info[1]](
data_path,
classname=subdataset,
resize=resize,
imagesize=imagesize,
split=dataset_library.DatasetSplit.TEST,
seed=seed,
))
test_dataset = ConcatDataset(test_datasets)
test_dataloader = torch.utils.data.DataLoader(
test_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers,
pin_memory=True,
)
test_dataloader.name = name
if subdataset is not None:
test_dataloader.name += "_Unified"
dataloader_dict = {"testing": test_dataloader}
yield dataloader_dict
else:
for subdataset in subdatasets:
test_dataset = dataset_library.__dict__[dataset_info[1]](
data_path,
classname=subdataset,
resize=resize,
imagesize=imagesize,
split=dataset_library.DatasetSplit.TEST,
seed=seed,
)
test_dataloader = torch.utils.data.DataLoader(
test_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers,
pin_memory=True,
)
test_dataloader.name = name
if subdataset is not None:
test_dataloader.name += "_" + subdataset
dataloader_dict = {"testing": test_dataloader}
yield dataloader_dict
return ("get_dataloaders_iter", [get_dataloaders_iter, len(subdatasets)])
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
LOGGER.info("Command line arguments: {}".format(" ".join(sys.argv)))
main()
This diff is collapsed.
images/Graphical_abstract.png

666 KiB

images/architecture.png

311 KiB

images/patchcore_defect_segmentation.png

1.66 MiB

click>= 8.0.3
# cudatoolkit>= 10.2
matplotlib>= 3.5.0
pillow>= 8.4.0
pretrainedmodels>= 0.7.4
torch>= 2.1.1+cu121
scikit-image>= 0.18.3
scikit-learn>= 1.0.1
scipy>= 1.7.1
torchvision== 0.16.1
tqdm>= 4.62.3
timm>=1.0.14
# faiss-cpu
faiss-gpu
\ No newline at end of file
-r requirements.txt
black>=21.11b0
flake8>=4.0.1
isort>=5.10.1
pytest>=6.2.5
\ No newline at end of file
0.1.0
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment