-
Notifications
You must be signed in to change notification settings - Fork 8
Home
Welcome to the AnomaVision documentation! This wiki provides comprehensive guides, API references, and examples for using AnomaVision - a production-ready visual anomaly detection library.
AnomaVision is a high-performance, production-ready visual anomaly detection library built on the state-of-the-art PaDiM algorithm. It provides enterprise-grade performance with research-level accuracy, supporting multiple export formats for deployment anywhere from edge devices to cloud infrastructure.
- π― Unmatched Performance: Optimized PaDiM implementation with CPU-first design
- π Multi-Format Support: PyTorch, ONNX, TorchScript, OpenVINO, and more
- π¦ Production Ready: Enterprise-grade deployment capabilities
- π¨ Rich Visualizations: Comprehensive anomaly visualization tools
- π Flexible Image Dimensions: Support for any image size and aspect ratio
- β‘ Edge-Ready: Optimized for edge device deployment
-
2-4x smaller model files with statistics-only
.pthformat - CPU-optimized pipeline that works without GPU
- Multi-format export from a single trained model
- Plug-and-play loading with unified inference interface
# Clone and install git clone https://github.com/DeepKnowledge1/AnomaVision.git cd AnomaVision poetry install poetry shell # Verify installation python -c "import anodet; print('π AnomaVision installed successfully!')"
import anodet import torch from torch.utils.data import DataLoader # Load your "good" training images dataset = anodet.AnodetDataset( "path/to/train/good", resize=[256, 192], # Flexible width/height crop_size=[224, 224], # Final crop size normalize=True # ImageNet normalization ) dataloader = DataLoader(dataset, batch_size=4) # Initialize PaDiM with optimal settings device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = anodet.Padim( backbone='resnet18', # Fast and accurate device=device, layer_indices=[0, 1], # Multi-scale features feat_dim=100 # Optimal feature dimension ) # Train the model print("π Training model...") model.fit(dataloader) # Save for production deployment torch.save(model, "anomaly_detector.pt") model.save_statistics("compact_model.pth", half=True) # 4x smaller! print("β Model trained and saved!")
# Load test data and detect anomalies test_dataset = anodet.AnodetDataset("path/to/test/images") test_dataloader = DataLoader(test_dataset, batch_size=4) for batch, images, _, _ in test_dataloader: # Get anomaly scores and detailed heatmaps image_scores, score_maps = model.predict(batch) # Classify anomalies (threshold=13 works great for most cases) predictions = anodet.classification(image_scores, threshold=13) print(f"π₯ Anomaly scores: {image_scores.tolist()}") print(f"π Predictions: {predictions.tolist()}") break
- Python: 3.9+
- CUDA: 11.7+ for GPU acceleration (optional)
- PyTorch: 2.0+ (automatically installed)
git clone https://github.com/DeepKnowledge1/AnomaVision.git
cd AnomaVision
poetry install
poetry shellgit clone https://github.com/DeepKnowledge1/AnomaVision.git
cd AnomaVision
pip install -r requirements.txtgit clone https://github.com/DeepKnowledge1/AnomaVision.git
cd AnomaVision
poetry install --dev
pre-commit installpython -c "import anodet; print('π AnomaVision installed successfully!')"
For full functionality, install additional backends:
# ONNX Runtime pip install onnxruntime-gpu # or onnxruntime for CPU # OpenVINO pip install openvino # Development tools pip install pytest black flake8 pre-commit
import anodet import torch from torch.utils.data import DataLoader # 1. Create dataset dataset = anodet.AnodetDataset( "path/to/normal/images", resize=[224, 224], crop_size=[224, 224], normalize=True ) dataloader = DataLoader(dataset, batch_size=8) # 2. Initialize model model = anodet.Padim( backbone='resnet18', device=torch.device('cuda'), layer_indices=[0, 1], feat_dim=100 ) # 3. Train model.fit(dataloader) # 4. Save torch.save(model, "model.pt") model.save_statistics("model.pth", half=True)
from anodet.inference.model.wrapper import ModelWrapper # Load any supported format model = ModelWrapper("model.onnx", device='cuda') # Predict scores, maps = model.predict(batch) # Classify predictions = anodet.classification(scores, threshold=13) # Cleanup model.close()
# Train a model python train.py --config config.yml # Run inference python detect.py --model model.onnx --img_path test_images/ # Evaluate performance python eval.py --model model.pt --dataset_path data/mvtec --class_name bottle # Export to multiple formats python export.py --model model.pt --format all
AnomaVision is built with a modular architecture designed for production deployment:
AnomaVision/
βββ π§ anodet/ # Core AI library
β βββ π padim.py # PaDiM implementation
β βββ π padim_lite.py # Lightweight runtime module
β βββ π feature_extraction.py # ResNet feature extraction
β βββ π mahalanobis.py # Distance computation
β βββ π datasets/ # Dataset loaders with flexible sizing
β βββ π visualization/ # Rich visualization tools
β βββ π inference/ # Multi-format inference engine
β β βββ π wrapper.py # Universal model wrapper
β β βββ π modelType.py # Format detection
β β βββ π backends/ # Format-specific backends
β β βββ π torch_backend.py # PyTorch support
β β βββ π onnx_backend.py # ONNX Runtime support
β β βββ π torchscript_backend.py # TorchScript support
β β βββ π openvino_backend.py # OpenVINO support
β βββ π config/ # Configuration management
βββ π train.py # Training script
βββ π detect.py # Inference script
βββ π eval.py # Evaluation script
βββ π export.py # Multi-format export utilities
βββ π config.yml # Default configuration
- Full-featured PaDiM implementation
- Training and inference capabilities
- Statistics saving for deployment
- Lightweight runtime for deployment
- Loads from statistics-only files
- Minimal memory footprint
- ModelWrapper: Unified interface for all formats
- Backends: Format-specific implementations
- ModelType: Automatic format detection
- ResNet backbone support (ResNet18, Wide-ResNet50)
- Multi-scale feature extraction
- Optimized concatenation and processing
- Efficient Mahalanobis distance calculation
- Memory-optimized chunked computation
- ONNX export compatibility
PaDiM (Patch Distribution Modeling) is a state-of-the-art anomaly detection algorithm that models the distribution of features at each spatial location.
- Feature Extraction: Extract multi-scale features from pre-trained ResNet
- Statistical Modeling: Compute mean and covariance for each spatial location
- Anomaly Scoring: Calculate Mahalanobis distance to detect deviations
- No additional training: Uses pre-trained features
- Pixel-level detection: Provides detailed anomaly maps
- Robust performance: Works across various domains
- Interpretable results: Clear statistical foundation
class Padim(torch.nn.Module): def __init__(self, backbone='resnet18', layer_indices=[0, 1], feat_dim=100): # Initialize ResNet feature extractor self.embeddings_extractor = ResnetEmbeddingsExtractor(backbone, device) # Set up feature selection self.layer_indices = layer_indices self.channel_indices = get_dims_indices(layer_indices, feat_dim, ...) def fit(self, dataloader): # Extract features from training data features = self.embeddings_extractor.from_dataloader(dataloader) # Compute statistics mean = torch.mean(features, dim=0) cov = pytorch_cov(features) + 0.01 * torch.eye(features.shape[2]) cov_inv = torch.inverse(cov) # Store for inference self.mahalanobisDistance = MahalanobisDistance(mean, cov_inv) def predict(self, batch): # Extract features features, w, h = self.embeddings_extractor(batch) # Compute distances distances = self.mahalanobisDistance(features, w, h) # Return scores and maps image_scores = distances.flatten(1).max(1).values score_maps = F.interpolate(distances.unsqueeze(1), size=batch.shape[-2:]) return image_scores, score_maps.squeeze(1)
AnomaVision supports multiple model formats for flexible deployment:
| Format | Status | Use Case | Language Support |
|---|---|---|---|
| PyTorch | β Ready | Development & Research | Python |
| Statistics (.pth) | β Ready | Ultra-compact deployment (2-4x smaller) | Python |
| ONNX | β Ready | Cross-platform deployment | Python, C++ |
| TorchScript | β Ready | Production Python deployment | Python |
| OpenVINO | β Ready | Intel hardware optimization | Python |
| TensorRT | π§ Coming Soon | NVIDIA GPU acceleration | Python |
# Full model with training capabilities model = torch.load("model.pt") scores, maps = model.predict(batch)
# Compact statistics-only (2-4x smaller) model = ModelWrapper("model.pth", device='cpu') scores, maps = model.predict(batch)
# Cross-platform deployment model = ModelWrapper("model.onnx", device='cuda') scores, maps = model.predict(batch)
# Optimized Python deployment model = ModelWrapper("model.torchscript", device='cuda') scores, maps = model.predict(batch)
# Intel hardware optimization model = ModelWrapper("model_openvino/model.xml", device='cpu') scores, maps = model.predict(batch)
All formats use the same interface through ModelWrapper:
from anodet.inference.model.wrapper import ModelWrapper # Load any format model = ModelWrapper(model_path, device) # Same prediction interface scores, maps = model.predict(batch) # Same cleanup model.close()
AnomaVision provides flexible image processing with configurable dimensions:
# Square resize and crop dataset = anodet.AnodetDataset( image_path, resize=[224, 224], crop_size=[224, 224] ) # Flexible width/height dataset = anodet.AnodetDataset( image_path, resize=[256, 192], # Width, Height crop_size=[224, 224] # Final crop ) # No cropping dataset = anodet.AnodetDataset( image_path, resize=[320, 240], crop_size=None # Keep resize dimensions )
# ImageNet normalization (recommended) dataset = anodet.AnodetDataset( image_path, normalize=True, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) # Custom normalization dataset = anodet.AnodetDataset( image_path, normalize=True, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5] ) # No normalization dataset = anodet.AnodetDataset( image_path, normalize=False )
The processing pipeline follows this order:
- Load: PIL.Image.open() and convert to RGB
- Resize: Resize to specified dimensions
- Crop: Center crop to final size (optional)
- ToTensor: Convert to PyTorch tensor
- Normalize: Apply normalization (optional)
from torchvision import transforms as T # Create custom transforms custom_transforms = T.Compose([ T.Resize([256, 256]), T.CenterCrop([224, 224]), T.ColorJitter(brightness=0.1), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # Use with dataset dataset = anodet.AnodetDataset( image_path, image_transforms=custom_transforms )
# Basic training python train.py \ --dataset_path "data/bottle" \ --class_name "bottle" \ --model_data_path "./models/" \ --backbone resnet18 \ --batch_size 8 \ --layer_indices 0 1 \ --feat_dim 100 # Using config file (recommended) python train.py --config config.yml
Create config.yml:
# Dataset configuration dataset_path: "D:/01-DATA" class_name: "bottle" resize: [256, 224] # Width, Height crop_size: [224, 224] # Final square crop normalize: true norm_mean: [0.485, 0.456, 0.406] norm_std: [0.229, 0.224, 0.225] # Model configuration backbone: "resnet18" feat_dim: 100 layer_indices: [0, 1] batch_size: 8 # Output configuration model_data_path: "./distributions/bottle_exp" output_model: "padim_model.pt" run_name: "bottle_experiment"
import anodet import torch from torch.utils.data import DataLoader # Dataset setup dataset = anodet.AnodetDataset( "path/to/train/good", resize=[224, 224], crop_size=[224, 224], normalize=True ) dataloader = DataLoader(dataset, batch_size=8, shuffle=False) # Model initialization device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = anodet.Padim( backbone='resnet18', device=device, layer_indices=[0, 1], feat_dim=100 ) # Training print("Training model...") model.fit(dataloader, extractions=1) # Save multiple formats torch.save(model, "full_model.pt") model.save_statistics("stats_fp32.pth", half=False) model.save_statistics("stats_fp16.pth", half=True) print("Training completed!")
| Parameter | Description | Default | Recommended |
|---|---|---|---|
backbone |
Feature extractor | resnet18 |
resnet18 for speed, wide_resnet50 for accuracy |
layer_indices |
ResNet layers | [0] |
[0, 1] for best balance |
feat_dim |
Feature dimensions | 50 |
100-200 depending on complexity |
batch_size |
Training batch size | 2 |
Largest that fits in memory |
extractions |
Dataset passes | 1 |
2-3 for data augmentation |
# MVTec dataset structure dataset = anodet.MVTecDataset( "path/to/mvtec", class_name="bottle", is_train=True, resize=[224, 224], crop_size=[224, 224], normalize=True )
Expected structure:
mvtec/
βββ bottle/
β βββ train/
β β βββ good/ # Normal training images
β βββ test/
β β βββ good/ # Normal test images
β β βββ broken_large/ # Anomalous test images
β βββ ground_truth/
β βββ broken_large/ # Pixel-level masks
# Basic inference python detect.py \ --model_data_path "./models/" \ --model "padim_model.onnx" \ --img_path "test_images/" \ --batch_size 16 \ --thresh 13 \ --enable_visualization # With config file python detect.py --config config.yml
from anodet.inference.model.wrapper import ModelWrapper from torch.utils.data import DataLoader import anodet # Load model (any format) model = ModelWrapper("model.onnx", device='cuda') # Prepare test data test_dataset = anodet.AnodetDataset("test_images/") test_dataloader = DataLoader(test_dataset, batch_size=4) # Run inference for batch, images, _, _ in test_dataloader: # Get predictions image_scores, score_maps = model.predict(batch) # Classify anomalies predictions = anodet.classification(image_scores, threshold=13) print(f"Scores: {image_scores}") print(f"Predictions: {predictions}") # Cleanup model.close()
def process_directory(model_path, image_dir, threshold=13): """Process all images in a directory.""" model = ModelWrapper(model_path, device='cuda') dataset = anodet.AnodetDataset(image_dir) dataloader = DataLoader(dataset, batch_size=8) all_scores = [] all_predictions = [] for batch, _, _, _ in dataloader: scores, maps = model.predict(batch) predictions = anodet.classification(scores, threshold) all_scores.extend(scores.tolist()) all_predictions.extend(predictions.tolist()) model.close() return all_scores, all_predictions
import cv2 import numpy as np from PIL import Image def real_time_detection(model_path): """Real-time anomaly detection from webcam.""" model = ModelWrapper(model_path, device='cuda') cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # Preprocess frame image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) batch = anodet.to_batch([np.array(image)]) # Detect anomalies scores, maps = model.predict(batch) prediction = anodet.classification(scores, threshold=13)[0] # Display result color = (0, 255, 0) if prediction == 1 else (0, 0, 255) cv2.putText(frame, f"Score: {scores[0]:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2) cv2.imshow('Anomaly Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() model.close()
# Warmup for consistent timing model.warmup(sample_batch, runs=3) # Batch processing for throughput dataloader = DataLoader(dataset, batch_size=32, pin_memory=True) # Use appropriate device device = 'cuda' if torch.cuda.is_available() else 'cpu' model = ModelWrapper(model_path, device=device)
# Export to all supported formats python export.py \ --model_data_path "./models/" \ --model "padim_model.pt" \ --format all \ --opset 17 \ --dynamic_batch # Export specific format python export.py \ --model_data_path "./models/" \ --model "padim_model.pt" \ --format onnx \ --opset 17
from export import ModelExporter from pathlib import Path # Initialize exporter model_path = Path("models/padim_model.pt") output_dir = Path("exported_models/") exporter = ModelExporter(model_path, output_dir, logger) # Export ONNX onnx_path = exporter.export_onnx( input_shape=(1, 3, 224, 224), output_name="model.onnx", opset_version=17, dynamic_batch=True ) # Export TorchScript ts_path = exporter.export_torchscript( input_shape=(1, 3, 224, 224), output_name="model.torchscript", optimize=True ) # Export OpenVINO ov_path = exporter.export_openvino( input_shape=(1, 3, 224, 224), output_name="model_openvino", fp16=True, dynamic_batch=False )
# export section in config.yml export: format: "all" # onnx, torchscript, openvino, all opset: 17 # ONNX opset version dynamic_batch: true # Allow dynamic batch size fp16: true # Use FP16 for OpenVINO optimize: true # TorchScript mobile optimization
onnx_path = exporter.export_onnx( input_shape=(1, 3, 224, 224), output_name="model.onnx", opset_version=17, # ONNX opset (11, 13, 15, 17) dynamic_batch=True # Allow variable batch size )
ts_path = exporter.export_torchscript( input_shape=(1, 3, 224, 224), output_name="model.torchscript", optimize=True # Mobile optimization )
ov_path = exporter.export_openvino( input_shape=(1, 3, 224, 224), output_name="model_openvino", fp16=True, # Use FP16 precision dynamic_batch=False # Static batch for optimization )
# Save compact statistics (2-4x smaller) model.save_statistics("model_fp32.pth", half=False) # Full precision model.save_statistics("model_fp16.pth", half=True) # Half precision # Load statistics stats = model.load_statistics("model_fp16.pth", device='cpu', force_fp32=True)
# Evaluate on MVTec dataset python eval.py \ --model_data_path "./models/" \ --model "padim_model.onnx" \ --dataset_path "data/mvtec" \ --class_name "bottle" \ --batch_size 8 # With config file python eval.py --config config.yml
import anodet from torch.utils.data import DataLoader # Load test dataset test_dataset = anodet.MVTecDataset( "data/mvtec", class_name="bottle", is_train=False, # Test set resize=[224, 224], crop_size=[224, 224], normalize=True ) test_dataloader = DataLoader(test_dataset, batch_size=8) # Load model model = torch.load("padim_model.pt") # Run evaluation results = model.evaluate(test_dataloader) images, targets, masks, scores, maps = results # Visualize results anodet.visualize_eval_data(targets, masks, scores, maps)
from sklearn.metrics import roc_auc_score, precision_recall_curve # Image-level metrics image_auroc = roc_auc_score(targets, scores) precision, recall, thresholds = precision_recall_curve(targets, scores) # Pixel-level metrics pixel_auroc = roc_auc_score(masks.flatten(), maps.flatten()) print(f"Image AUROC: {image_auroc:.4f}") print(f"Pixel AUROC: {pixel_auroc:.4f}")
from anodet.test import optimal_threshold # Find optimal threshold precision, recall, threshold = optimal_threshold(targets, scores) print(f"Optimal threshold: {threshold:.2f}") print(f"Precision: {precision:.4f}") print(f"Recall: {recall:.4f}")
# For large datasets results = model.evaluate_memory_efficient(test_dataloader) images, targets, masks, scores, maps = results
from anodet.inference.model.wrapper import ModelWrapper # Compare different formats formats = { 'pytorch': 'model.pt', 'onnx': 'model.onnx', 'torchscript': 'model.torchscript', 'openvino': 'model_openvino/model.xml' } results = {} for name, path in formats.items(): model = ModelWrapper(path, device='cpu') all_scores = [] for batch, _, _, _ in test_dataloader: scores, _ = model.predict(batch) all_scores.extend(scores.tolist()) auroc = roc_auc_score(targets, all_scores) results[name] = auroc model.close() for name, auroc in results.items(): print(f"{name}: {auroc:.4f}")
AnomaVision uses a flexible YAML-based configuration system that supports both command-line arguments and configuration files.
# ========================= # Dataset / preprocessing # ========================= dataset_path: "D:/01-DATA" class_name: "bottle" resize: [224, 224] # [width, height] or single int crop_size: [224, 224] # [width, height] or single int normalize: true norm_mean: [0.485, 0.456, 0.406] # ImageNet stats norm_std: [0.229, 0.224, 0.225] # ========================= # Model / training # ========================= backbone: "resnet18" # resnet18, wide_resnet50 feat_dim: 100 # Feature dimension layer_indices: [0, 1] # ResNet layers to use model_data_path: "./distributions/exp" output_model: "padim_model.pt" batch_size: 8 device: "auto" # cpu, cuda, auto # ========================= # Inference # ========================= img_path: "test_images/" thresh: 13.0 # Anomaly threshold enable_visualization: true save_visualizations: true viz_output_dir: "./visualizations/" viz_alpha: 0.6 # Heatmap transparency viz_padding: 40 # Boundary padding viz_color: "128,0,128" # RGB highlight color # ========================= # Export # ========================= format: "all" # onnx, torchscript, openvino, all opset: 17 # ONNX opset version dynamic_batch: true # Allow dynamic batch size fp16: true # Use FP16 for OpenVINO optimize: true # TorchScript optimization # ========================= # Evaluation # ========================= metrics: ["auroc", "pixel_auroc"] val_batch_size: 8 memory_efficient: true # ========================= # Logging # ========================= log_level: "INFO" # DEBUG, INFO, WARNING, ERROR detailed_timing: false
# All scripts support config files
python train.py --config config.yml
python detect.py --config config.yml
python eval.py --config config.yml
python export.py --config config.yml# Config file + CLI overrides
python train.py --config config.yml --batch_size 16 --feat_dim 200from anodet.config import load_config from anodet.utils import merge_config from easydict import EasyDict as edict # Load configuration config = load_config("config.yml") # Merge with arguments args = parse_args() final_config = edict(merge_config(args, config)) # Use configuration dataset = anodet.AnodetDataset( final_config.dataset_path, resize=final_config.resize, crop_size=final_config.crop_size, normalize=final_config.normalize )
def validate_config(config): """Validate configuration parameters.""" required_fields = ['dataset_path', 'backbone', 'batch_size'] for field in required_fields: if not hasattr(config, field) or getattr(config, field) is None: raise ValueError(f"Required field '{field}' is missing") if config.backbone not in ['resnet18', 'wide_resnet50']: raise ValueError(f"Unsupported backbone: {config.backbone}") if config.batch_size <= 0: raise ValueError("Batch size must be positive")
AnomaVision provides a unified interface for all supported model formats through the ModelWrapper class.
from anodet.inference.model.wrapper import ModelWrapper # All formats use the same interface models = { 'pytorch': ModelWrapper("model.pt", device='cuda'), 'statistics': ModelWrapper("model.pth", device='cuda'), 'onnx': ModelWrapper("model.onnx", device='cuda'), 'torchscript': ModelWrapper("model.torchscript", device='cuda'), 'openvino': ModelWrapper("model_openvino/model.xml", device='cpu') } # Same prediction interface for all for name, model in models.items(): scores, maps = model.predict(batch) print(f"{name}: {scores.mean():.4f}") model.close()
# Supports full models and statistics files backend = TorchBackend("model.pt", device='cuda', use_amp=True) # Automatic mixed precision for faster inference scores, maps = backend.predict(batch)
# Cross-platform deployment backend = OnnxBackend( "model.onnx", device='cuda', intra_threads=4, # Parallel processing inter_threads=2 # Thread management ) # Optimized execution providers scores, maps = backend.predict(batch)
# Optimized Python deployment backend = TorchScriptBackend( "model.torchscript", device='cuda', num_threads=8 # CPU threading ) # JIT compilation benefits scores, maps = backend.predict(batch)
# Intel hardware optimization backend = OpenVinoBackend( "model_openvino/model.xml", device='CPU', # CPU, GPU, AUTO num_threads=4 # CPU optimization ) # Hardware-specific acceleration scores, maps = backend.predict(batch)
import time from anodet.general import Profiler def benchmark_formats(batch, formats, runs=10): """Benchmark different model formats.""" results = {} for name, model_path in formats.items(): model = ModelWrapper(model_path, device='cuda') # Warmup model.warmup(batch, runs=3) # Benchmark profiler = Profiler() for _ in range(runs): with profiler: scores, maps = model.predict(batch) avg_time = profiler.get_avg_time_ms(runs) fps = profiler.get_fps(len(batch) * runs) results[name] = { 'avg_time_ms': avg_time, 'fps': fps, 'scores_mean': scores.mean() } model.close() return results # Run benchmark formats = { 'PyTorch': 'model.pt', 'ONNX': 'model.onnx', 'TorchScript': 'model.torchscript', 'OpenVINO': 'model_openvino/model.xml' } results = benchmark_formats(test_batch, formats) for name, metrics in results.items(): print(f"{name}: {metrics['avg_time_ms']:.2f}ms, {metrics['fps']:.1f} FPS")
def get_optimal_backend(model_path, device): """Select optimal backend based on device.""" if device.startswith('cuda'): # NVIDIA GPU if model_path.endswith('.onnx'): return OnnxBackend(model_path, device='cuda') elif model_path.endswith('.engine'): return TensorRTBackend(model_path, device='cuda') else: return TorchBackend(model_path, device='cuda', use_amp=True) elif device == 'cpu': # CPU optimization if 'openvino' in model_path: return OpenVinoBackend(model_path, device='CPU', num_threads=8) elif model_path.endswith('.onnx'): return OnnxBackend(model_path, device='cpu', intra_threads=8) else: return TorchBackend(model_path, device='cpu') else: # Auto-select return ModelWrapper(model_path, device='auto')
# Use statistics files for minimal memory model = ModelWrapper("model.pth", device='cpu') # 2-4x smaller # Batch processing for throughput dataloader = DataLoader(dataset, batch_size=32, pin_memory=True, num_workers=4) # Memory-efficient evaluation results = model.evaluate_memory_efficient(test_dataloader)
# Enable mixed precision model = ModelWrapper("model.pt", device='cuda') # Automatic AMP # Optimize GPU memory torch.backends.cudnn.benchmark = True # Optimize for fixed input sizes # Pin memory for faster transfers dataloader = DataLoader(dataset, pin_memory=True)
# Use optimized backends model = ModelWrapper("model_openvino/model.xml", device='CPU') # Set thread count torch.set_num_threads(8) # Use ONNX with threading backend = OnnxBackend("model.onnx", device='cpu', intra_threads=8, inter_threads=2)
def find_optimal_batch_size(model_path, sample_batch, device='cuda'): """Find optimal batch size for throughput.""" model = ModelWrapper(model_path, device=device) batch_sizes = [1, 2, 4, 8, 16, 32, 64] best_fps = 0 best_batch_size = 1 for bs in batch_sizes: try: # Create test batch test_batch = sample_batch[:bs] if bs <= len(sample_batch) else \ torch.cat([sample_batch] * (bs // len(sample_batch) + 1))[:bs] # Benchmark profiler = Profiler() for _ in range(10): with profiler: scores, maps = model.predict(test_batch) fps = profiler.get_fps(bs * 10) print(f"Batch size {bs}: {fps:.1f} FPS") if fps > best_fps: best_fps = fps best_batch_size = bs except RuntimeError: # OOM print(f"Batch size {bs}: OOM") break model.close() return best_batch_size, best_fps
from anodet.general import Profiler # Detailed profiling profilers = { 'inference': Profiler(), 'postprocessing': Profiler(), 'visualization': Profiler() } for batch, images, _, _ in dataloader: # Core inference with profilers['inference']: scores, maps = model.predict(batch) # Postprocessing with profilers['postprocessing']: predictions = anodet.classification(scores, threshold=13) maps = anodet.utils.adaptive_gaussian_blur(maps) # Visualization with profilers['visualization']: heatmaps = anodet.visualization.heatmap_images(images, maps) # Print timing summary for name, prof in profilers.items(): print(f"{name}: {prof.accumulated_time*1000:.2f}ms")
import os from torch.utils.data import Dataset from PIL import Image import anodet class CustomAnomalyDataset(Dataset): """Custom dataset for anomaly detection.""" def __init__(self, root_dir, transform=None, is_train=True): self.root_dir = root_dir self.transform = transform or anodet.utils.create_image_transform() self.is_train = is_train # Load image paths self.image_paths = [] self.labels = [] if is_train: # Training: only normal images normal_dir = os.path.join(root_dir, 'normal') for img_name in os.listdir(normal_dir): if img_name.lower().endswith(('.png', '.jpg', '.jpeg')): self.image_paths.append(os.path.join(normal_dir, img_name)) self.labels.append(0) # Normal = 0 else: # Testing: both normal and anomalous for class_name in ['normal', 'anomaly']: class_dir = os.path.join(root_dir, class_name) if os.path.exists(class_dir): for img_name in os.listdir(class_dir): if img_name.lower().endswith(('.png', '.jpg', '.jpeg')): self.image_paths.append(os.path.join(class_dir, img_name)) self.labels.append(0 if class_name == 'normal' else 1) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # Load image image_path = self.image_paths[idx] image = Image.open(image_path).convert('RGB') label = self.labels[idx] # Transform tensor = self.transform(image) # Return format compatible with AnomaVision return tensor, np.array(image), label, torch.zeros(1, tensor.shape[1], tensor.shape[2]) # Usage train_dataset = CustomAnomalyDataset('data/custom', is_train=True) test_dataset = CustomAnomalyDataset('data/custom', is_train=False)
custom_dataset/
βββ normal/ # Normal training images
β βββ image1.jpg
β βββ image2.jpg
β βββ ...
βββ anomaly/ # Anomalous test images
βββ defect1.jpg
βββ defect2.jpg
βββ ...
dataset/
βββ class1/
β βββ train/
β β βββ good/
β βββ test/
β β βββ good/
β β βββ defect_type/
β βββ ground_truth/
β βββ defect_type/
βββ class2/
βββ ...
class IndustrialDataset(Dataset): """Dataset for industrial anomaly detection.""" def __init__(self, root_dir, product_type, transform=None): self.root_dir = root_dir self.product_type = product_type self.transform = transform # Load metadata metadata_path = os.path.join(root_dir, 'metadata.csv') self.metadata = pd.read_csv(metadata_path) # Filter by product type self.data = self.metadata[self.metadata['product'] == product_type] def __getitem__(self, idx): row = self.data.iloc[idx] # Load image image_path = os.path.join(self.root_dir, 'images', row['filename']) image = Image.open(image_path).convert('RGB') # Load mask if available if pd.notna(row['mask_filename']): mask_path = os.path.join(self.root_dir, 'masks', row['mask_filename']) mask = Image.open(mask_path).convert('L') else: mask = Image.new('L', image.size, 0) # Apply transforms if self.transform: image = self.transform(image) mask = self.transform(mask) return image, np.array(image), row['label'], mask
from torchvision import transforms as T # Training augmentation train_transform = T.Compose([ T.Resize([256, 256]), T.RandomRotation(10), T.ColorJitter(brightness=0.1, contrast=0.1), T.RandomHorizontalFlip(0.5), T.CenterCrop([224, 224]), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # Test transform (no augmentation) test_transform = T.Compose([ T.Resize([224, 224]), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # Create datasets train_dataset = CustomAnomalyDataset('data/', transform=train_transform, is_train=True) test_dataset = CustomAnomalyDataset('data/', transform=test_transform, is_train=False)
AnomaVision provides comprehensive visualization tools for anomaly detection results.
import anodet.visualization as viz import matplotlib.pyplot as plt # Load test data and run inference model = anodet.Padim(backbone='resnet18', device='cpu') # ... train model ... test_dataset = anodet.AnodetDataset("test_images/") dataloader = DataLoader(test_dataset, batch_size=4) for batch, images, _, _ in dataloader: # Get predictions image_scores, score_maps = model.predict(batch) # Apply Gaussian blur to score maps score_maps = anodet.utils.adaptive_gaussian_blur(score_maps, kernel_size=33, sigma=4) # Classify score_map_classifications = anodet.classification(score_maps, threshold=13) image_classifications = anodet.classification(image_scores, threshold=13) # Generate visualizations boundary_images = viz.framed_boundary_images( images, score_map_classifications, image_classifications, padding=40 ) heatmap_images = viz.heatmap_images( images, score_maps, alpha=0.6 ) highlighted_images = viz.highlighted_images( images, score_map_classifications, color=(255, 0, 0), alpha=0.5 ) break
def create_anomaly_visualization(model, test_image_path, threshold=13): """Create comprehensive anomaly visualization.""" # Load and preprocess image image = Image.open(test_image_path).convert('RGB') transform = anodet.utils.create_image_transform(resize=[224, 224], normalize=True) batch = transform(image).unsqueeze(0) # Run inference image_scores, score_maps = model.predict(batch) score_maps = anodet.utils.adaptive_gaussian_blur(score_maps) # Classifications score_map_class = anodet.classification(score_maps, threshold) image_class = anodet.classification(image_scores, threshold) # Create visualization fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # Original image axes[0, 0].imshow(image) axes[0, 0].set_title('Original Image') axes[0, 0].axis('off') # Score map axes[0, 1].imshow(score_maps[0], cmap='hot') axes[0, 1].set_title(f'Anomaly Score Map\nMax Score: {score_maps[0].max():.2f}') axes[0, 1].axis('off') # Heatmap overlay heatmap = viz.heatmap_image(np.array(image), score_maps[0], alpha=0.6) axes[0, 2].imshow(heatmap) axes[0, 2].set_title('Heatmap Overlay') axes[0, 2].axis('off') # Boundary detection boundary = viz.boundary_image(np.array(image), score_map_class[0]) axes[1, 0].imshow(boundary) axes[1, 0].set_title('Boundary Detection') axes[1, 0].axis('off') # Highlighted anomalies highlighted = viz.highlighted_image(np.array(image), score_map_class[0]) axes[1, 1].imshow(highlighted) axes[1, 1].set_title('Highlighted Anomalies') axes[1, 1].axis('off') # Classification result result_color = 'red' if image_class[0] == 0 else 'green' result_text = 'ANOMALY' if image_class[0] == 0 else 'NORMAL' axes[1, 2].text(0.5, 0.5, result_text, transform=axes[1, 2].transAxes, fontsize=24, ha='center', va='center', color=result_color, bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)) axes[1, 2].set_title(f'Classification\nScore: {image_scores[0]:.2f}') axes[1, 2].axis('off') plt.tight_layout() return fig
import cv2 import numpy as np def real_time_visualization(model_path, camera_index=0): """Real-time anomaly detection with visualization.""" model = ModelWrapper(model_path, device='cuda') cap = cv2.VideoCapture(camera_index) while True: ret, frame = cap.read() if not ret: break # Preprocess rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image = Image.fromarray(rgb_frame) batch = anodet.to_batch([np.array(image)]) # Inference scores, maps = model.predict(batch) prediction = anodet.classification(scores, threshold=13)[0] # Create heatmap overlay heatmap = viz.heatmap_image(rgb_frame, maps[0], alpha=0.4) heatmap_bgr = cv2.cvtColor(heatmap, cv2.COLOR_RGB2BGR) # Add text overlay color = (0, 255, 0) if prediction == 1 else (0, 0, 255) text = f"Score: {scores[0]:.2f} - {'NORMAL' if prediction == 1 else 'ANOMALY'}" cv2.putText(heatmap_bgr, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2, cv2.LINE_AA) # Display cv2.imshow('Anomaly Detection', heatmap_bgr) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() model.close()
def visualize_batch_results(model, dataloader, save_dir=None): """Visualize results for a batch of images.""" for batch_idx, (batch, images, _, _) in enumerate(dataloader): # Run inference image_scores, score_maps = model.predict(batch) score_maps = anodet.utils.adaptive_gaussian_blur(score_maps) # Classifications score_map_classifications = anodet.classification(score_maps, threshold=13) image_classifications = anodet.classification(image_scores, threshold=13) # Create grid visualization batch_size = len(images) fig, axes = plt.subplots(batch_size, 4, figsize=(16, 4*batch_size)) for i in range(batch_size): # Original axes[i, 0].imshow(images[i]) axes[i, 0].set_title(f'Original {i+1}') axes[i, 0].axis('off') # Heatmap heatmap = viz.heatmap_image(images[i], score_maps[i], alpha=0.6) axes[i, 1].imshow(heatmap) axes[i, 1].set_title(f'Heatmap\nScore: {image_scores[i]:.2f}') axes[i, 1].axis('off') # Boundary boundary = viz.boundary_image(images[i], score_map_classifications[i]) axes[i, 2].imshow(boundary) axes[i, 2].set_title('Boundaries') axes[i, 2].axis('off') # Highlighted highlighted = viz.highlighted_image(images[i], score_map_classifications[i]) axes[i, 3].imshow(highlighted) axes[i, 3].set_title('Highlighted') axes[i, 3].axis('off') plt.tight_layout() if save_dir: os.makedirs(save_dir, exist_ok=True) plt.savefig(f"{save_dir}/batch_{batch_idx}.png", dpi=150, bbox_inches='tight') plt.show() break # Only visualize first batch
def create_custom_heatmap(image, score_map, colormap='jet', alpha=0.6): """Create custom heatmap with different colormaps.""" import cv2 # Normalize score map normalized_scores = (score_map - score_map.min()) / (score_map.max() - score_map.min()) normalized_scores = (normalized_scores * 255).astype(np.uint8) # Apply colormap colormap_dict = { 'jet': cv2.COLORMAP_JET, 'hot': cv2.COLORMAP_HOT, 'cool': cv2.COLORMAP_COOL, 'viridis': cv2.COLORMAP_VIRIDIS, 'plasma': cv2.COLORMAP_PLASMA } colored_heatmap = cv2.applyColorMap(normalized_scores, colormap_dict[colormap]) colored_heatmap = cv2.cvtColor(colored_heatmap, cv2.COLOR_BGR2RGB) # Blend with original image blended = cv2.addWeighted(image, 1-alpha, colored_heatmap, alpha, 0) return blended def plot_score_distribution(scores, threshold=13, title="Score Distribution"): """Plot distribution of anomaly scores.""" plt.figure(figsize=(10, 6)) # Histogram plt.hist(scores, bins=50, alpha=0.7, edgecolor='black') # Threshold line plt.axvline(threshold, color='red', linestyle='--', linewidth=2, label=f'Threshold: {threshold}') # Statistics plt.axvline(np.mean(scores), color='green', linestyle='-', linewidth=2, label=f'Mean: {np.mean(scores):.2f}') plt.axvline(np.median(scores), color='blue', linestyle='-', linewidth=2, label=f'Median: {np.median(scores):.2f}') plt.xlabel('Anomaly Score') plt.ylabel('Frequency') plt.title(title) plt.legend() plt.grid(True, alpha=0.3) plt.show()
class Padim(torch.nn.Module): """ PaDiM anomaly detection model. Args: backbone (str): ResNet architecture ('resnet18', 'wide_resnet50') device (torch.device): Computation device layer_indices (List[int]): ResNet layers to extract features from [0-3] feat_dim (int): Target feature dimension after channel selection channel_indices (torch.Tensor): Specific channel indices (optional) layer_hook (Callable): Function to apply to extracted features (optional) """ def __init__(self, backbone='resnet18', device=torch.device('cpu'), layer_indices=[0, 1], feat_dim=50, **kwargs): pass def fit(self, dataloader, extractions=1): """ Fit model to normal training data. Args: dataloader: PyTorch DataLoader with normal images extractions: Number of passes through data (for augmentation) """ pass def predict(self, batch, export=False): """ Predict anomaly scores and maps. Args: batch (torch.Tensor): Input images (B, C, H, W) export (bool): Use export-friendly computation paths Returns: Tuple[torch.Tensor, torch.Tensor]: (image_scores, score_maps) """ pass def evaluate(self, dataloader): """ Evaluate model on test data. Args: dataloader: Test data loader Returns: Tuple: (images, targets, masks, scores, maps) """ pass def save_statistics(self, path, half=False): """ Save model statistics for deployment. Args: path (str): Output file path half (bool): Use FP16 precision for smaller files """ pass @staticmethod def load_statistics(path, device='cpu', force_fp32=True): """ Load model statistics from file. Args: path (str): Statistics file path device (str): Target device force_fp32 (bool): Convert to FP32 for computation Returns: dict: Statistics dictionary """ pass
class ModelWrapper: """ Universal model wrapper for all supported formats. Args: model_path (str): Path to model file device (str): Target device ('cpu', 'cuda', 'auto') """ def __init__(self, model_path, device='cuda'): pass def predict(self, batch): """ Run inference on input batch. Args: batch: Input tensor or numpy array Returns: Tuple[np.ndarray, np.ndarray]: (scores, maps) """ pass def close(self): """Release model resources.""" pass def warmup(self, batch=None, runs=2): """ Warm up model for consistent performance. Args: batch: Sample input for warmup runs (int): Number of warmup iterations """ pass
class AnodetDataset(Dataset): """ Flexible dataset for anomaly detection with configurable image processing. Args: image_directory_path (str): Path to directory containing images mask_directory_path (str, optional): Path to masks directory resize (Union[int, Tuple[int, int]]): Resize dimensions crop_size (Union[int, Tuple[int, int]], optional): Crop dimensions normalize (bool): Apply ImageNet normalization mean (List[float]): Normalization mean values std (List[float]): Normalization std values """ def __init__(self, image_directory_path, mask_directory_path=None, resize=224, crop_size=224, normalize=True, **kwargs): pass def __len__(self): """Return dataset size.""" pass def __getitem__(self, idx): """ Get item by index. Returns: Tuple: (tensor, image_array, classification, mask) """ pass class MVTecDataset(Dataset): """ MVTec anomaly detection dataset loader. Args: dataset_path (str): Path to MVTec dataset root class_name (str): Class name (must be in CLASS_NAMES) is_train (bool): Load training or test data resize (Union[int, Tuple[int, int]]): Resize dimensions crop_size (Union[int, Tuple[int, int]], optional): Crop dimensions normalize (bool): Apply ImageNet normalization """ def __init__(self, dataset_path, class_name, is_train=True, **kwargs): pass
def create_image_transform(resize=224, crop_size=None, normalize=True, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]): """ Create configurable image transform pipeline. Args: resize: Size to resize to crop_size: Size to crop to (optional) normalize: Whether to apply normalization mean: Normalization mean values std: Normalization std values Returns: torchvision.transforms.Compose: Transform pipeline """ pass def to_batch(images, transforms=None, device='cpu', **kwargs): """ Convert list of numpy images to PyTorch tensor batch. Args: images (List[np.ndarray]): List of images transforms: Optional transforms to apply device: Target device Returns: torch.Tensor: Batch tensor """ pass def adaptive_gaussian_blur(input_array, kernel_size=33, sigma=4): """ Apply Gaussian blur with automatic backend selection. Args: input_array: Input tensor or array kernel_size (int): Blur kernel size sigma (float): Gaussian sigma Returns: Blurred array (same type as input) """ pass
def classification(image_scores, thresh): """ Classify images based on anomaly scores. Args: image_scores: Anomaly scores (tensor or numpy array) thresh (float): Classification threshold Returns: Classifications (same type as input): 0=anomaly, 1=normal """ pass def image_score(patch_scores): """ Calculate image-level scores from patch scores. Args: patch_scores (torch.Tensor): Patch-level scores Returns: torch.Tensor: Image-level scores """ pass
def pytorch_cov(tensor, rowvar=True, bias=False): """ Estimate covariance matrix (equivalent to np.cov). Args: tensor (torch.Tensor): Input tensor rowvar (bool): Whether rows are variables bias (bool): Whether to use bias correction Returns: torch.Tensor: Covariance matrix """ pass def mahalanobis(mean, cov_inv, batch): """ Calculate Mahalanobis distance. Args: mean (torch.Tensor): Mean vectors cov_inv (torch.Tensor): Inverse covariance matrices batch (torch.Tensor): Input batch Returns: torch.Tensor: Mahalanobis distances """ pass
def setup_logging(log_level="INFO"): """ Setup logging configuration. Args: log_level (str): Logging level Returns: logging.Logger: Configured logger """ pass def get_logger(name=None): """ Get logger for specific module. Args: name (str): Logger name (use __name__ from calling module) Returns: logging.Logger: Module logger """ pass class Profiler: """ Performance profiler for timing measurements. Usage: with Profiler() as prof: # code to profile print(f"Elapsed: {prof.elapsed_time:.3f}s") """ def __init__(self, accumulated_time=0.0): pass def __enter__(self): """Start timing.""" pass def __exit__(self, exc_type, exc_value, traceback): """Stop timing and accumulate.""" pass def get_fps(self, num_samples): """Calculate FPS from accumulated time.""" pass def get_avg_time_ms(self, num_operations): """Get average time per operation in milliseconds.""" pass
# ========================= # Dataset Configuration # ========================= dataset_path: "D:/01-DATA" # Root dataset directory class_name: "bottle" # MVTec class name img_path: "test_images/" # Test images path (for inference) # ========================= # Image Processing # ========================= resize: [224, 224] # Resize dimensions [width, height] crop_size: [224, 224] # Crop dimensions [width, height] normalize: true # Enable ImageNet normalization norm_mean: [0.485, 0.456, 0.406] # RGB normalization means norm_std: [0.229, 0.224, 0.225] # RGB normalization stds # ========================= # Model Configuration # ========================= backbone: "resnet18" # resnet18, wide_resnet50 feat_dim: 100 # Feature dimension after selection layer_indices: [0, 1] # ResNet layers to extract [0-3] device: "auto" # cpu, cuda, auto batch_size: 8 # Batch size for training/inference # ========================= # Training Configuration # ========================= model_data_path: "./distributions/exp" # Model output directory output_model: "padim_model.pt" # Model filename run_name: "experiment_1" # Experiment name epochs: 1 # Training epochs (usually 1 for PaDiM) extractions: 1 # Dataset passes # ========================= # Inference Configuration # ========================= thresh: 13.0 # Anomaly classification threshold num_workers: 1 # DataLoader workers pin_memory: true # Enable pinned memory # ========================= # Visualization Configuration # ========================= enable_visualization: true # Enable result visualization save_visualizations: false # Save visualization images viz_output_dir: "./visualizations/" # Visualization output directory viz_alpha: 0.6 # Heatmap overlay transparency viz_padding: 40 # Boundary visualization padding viz_color: "128,0,128" # RGB highlight color # ========================= # Export Configuration # ========================= format: "onnx" # onnx, torchscript, openvino, all opset: 17 # ONNX opset version dynamic_batch: true # Allow dynamic batch sizes fp16: true # Use FP16 precision (OpenVINO) optimize: false # TorchScript mobile optimization # ========================= # Evaluation Configuration # ========================= metrics: ["auroc", "pixel_auroc"] # Evaluation metrics memory_efficient: true # Use memory-efficient evaluation # ========================= # System Configuration # ========================= log_level: "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL detailed_timing: false # Enable detailed performance timing overwrite: false # Overwrite existing experiment directories
def validate_config(config): """Validate configuration parameters.""" # Required parameters required = ['dataset_path', 'backbone', 'batch_size'] for param in required: if not hasattr(config, param) or getattr(config, param) is None: raise ValueError(f"Required parameter '{param}' is missing") # Backbone validation valid_backbones = ['resnet18', 'wide_resnet50'] if config.backbone not in valid_backbones: raise ValueError(f"backbone must be one of {valid_backbones}") # Layer indices validation if config.layer_indices: valid_layers = [0, 1, 2, 3] for layer in config.layer_indices: if layer not in valid_layers: raise ValueError(f"layer_indices must be subset of {valid_layers}") # Batch size validation if config.batch_size <= 0: raise ValueError("batch_size must be positive") # Feature dimension validation if config.feat_dim <= 0: raise ValueError("feat_dim must be positive") # Threshold validation if hasattr(config, 'thresh') and config.thresh < 0: raise ValueError("thresh must be non-negative") # Image processing validation if config.resize: if isinstance(config.resize, (list, tuple)): if len(config.resize) != 2 or any(x <= 0 for x in config.resize): raise ValueError("resize must be [width, height] with positive values") elif not isinstance(config.resize, int) or config.resize <= 0: raise ValueError("resize must be positive integer or [width, height]") return True
# Development configuration development: log_level: "DEBUG" batch_size: 2 detailed_timing: true enable_visualization: true save_visualizations: true # Production configuration production: log_level: "WARNING" batch_size: 32 detailed_timing: false enable_visualization: false save_visualizations: false device: "cuda" # Edge deployment configuration edge: backbone: "resnet18" feat_dim: 50 batch_size: 1 device: "cpu" format: "onnx" fp16: true
We welcome contributions to AnomaVision! Here's how to get involved:
# Fork and clone git clone https://github.com/yourusername/AnomaVision.git cd AnomaVision # Setup development environment poetry install --dev pre-commit install # Create feature branch git checkout -b feature/awesome-improvement
- Follow PEP 8 with 88-character line limit
- Use Black for code formatting:
black . - Use flake8 for linting:
flake8 anodet/ - Use isort for import sorting:
isort .
# Add type hints to all new functions def process_batch( batch: torch.Tensor, model: Padim, threshold: float = 13.0 ) -> Tuple[np.ndarray, np.ndarray]: """Process batch with type hints.""" pass
def new_function(param1: str, param2: int = 10) -> bool: """ Brief description of the function. Longer description explaining the purpose, behavior, and any important details about the function implementation. Args: param1 (str): Description of parameter 1 param2 (int, optional): Description of parameter 2. Defaults to 10. Returns: bool: Description of return value Raises: ValueError: When parameter validation fails RuntimeError: When operation cannot be completed Example: >>> result = new_function("test", 20) >>> print(result) True """ pass
# Add pytest tests for new functionality def test_new_function(): """Test new function with various inputs.""" # Test normal case result = new_function("valid_input") assert result is True # Test edge cases with pytest.raises(ValueError): new_function("") # Test with different parameters result = new_function("test", param2=5) assert isinstance(result, bool)
- Create Issue: Describe the bug or feature request
- Fork Repository: Create your own fork
-
Create Branch: Use descriptive branch names
git checkout -b feature/add-tensorrt-backend git checkout -b bugfix/fix-memory-leak git checkout -b docs/update-api-reference
- Make Changes: Implement your improvements
- Add Tests: Ensure adequate test coverage
- Update Docs: Update documentation as needed
-
Run Tests: Ensure all tests pass
poetry run pytest poetry run black . poetry run flake8 anodet/ -
Commit Changes: Use conventional commit messages
git commit -m "feat: add TensorRT backend support" git commit -m "fix: resolve memory leak in batch processing" git commit -m "docs: update API reference for new features"
- Push Branch: Push to your fork
- Create PR: Submit pull request with detailed description
- All tests pass
- Code follows style guidelines
- Documentation updated
- Type hints added
- Changelog updated (for significant changes)
- Performance impact considered
- Code quality and readability
- Test coverage and quality
- Documentation completeness
- Performance implications
- Backward compatibility
- Security considerations
- TensorRT Backend: Complete TensorRT implementation
- Performance Optimization: Memory and speed improvements
- Additional Algorithms: Beyond PaDiM (PatchCore, etc.)
- Mobile Deployment: iOS/Android optimization
- Visualization Enhancements: Interactive visualizations
- Data Augmentation: Advanced augmentation techniques
- Distributed Training: Multi-GPU support
- Model Compression: Quantization and pruning
- Additional Datasets: Support for more dataset formats
- Cloud Integration: AWS/Azure/GCP deployment tools
- Web Interface: Browser-based demo
- Benchmarking Suite: Comprehensive performance benchmarks
- Discord/Slack: Join our community chat (if available)
- GitHub Discussions: Ask questions and discuss ideas
- Email: Contact maintainers directly for complex issues
- Documentation: Check existing docs before asking
AnomaVision uses pytest for comprehensive testing across all components.
# Run all tests poetry run pytest # Run with coverage poetry run pytest --cov=anodet --cov-report=html # Run specific test files poetry run pytest tests/test_padim.py poetry run pytest tests/test_backends.py # Run with verbose output poetry run pytest -v # Run tests matching pattern poetry run pytest -k "test_inference"
tests/
βββ conftest.py # Shared fixtures and configuration
βββ test_padim.py # Core PaDiM functionality
βββ test_backends.py # Multi-format backend tests
βββ test_datasets.py # Dataset loading tests
βββ test_feature_extraction.py # Feature extraction tests
βββ test_mahalanobis.py # Distance computation tests
βββ test_export_load_model.py # Export functionality tests
βββ test_visualization.py # Visualization tests
βββ test_inference_utils.py # Utility function tests
# From conftest.py @pytest.fixture def test_device(): """Provide test device (CPU for CI compatibility).""" return torch.device("cpu") @pytest.fixture def sample_images_dir(): """Create temporary directory with sample images.""" pass @pytest.fixture def trained_padim_model(sample_dataloader, test_device): """Train a minimal PaDiM model for testing.""" pass @pytest.fixture def sample_batch(sample_dataloader): """Get a single batch for testing.""" pass
def test_padim_initialization(test_device): """Test PaDiM model initialization.""" model = anodet.Padim( backbone="resnet18", device=test_device, layer_indices=[0, 1], feat_dim=50 ) assert model.device == test_device assert model.layer_indices == [0, 1] assert model.embeddings_extractor.backbone_name == "resnet18"
@pytest.mark.parametrize( "backbone,expected_layers", [ ("resnet18", [0, 1]), ("wide_resnet50", [0, 1, 2]), ] ) def test_different_backbones(test_device, backbone, expected_layers): """Test PaDiM with different backbone architectures.""" model = anodet.Padim( backbone=backbone, device=test_device, layer_indices=expected_layers ) assert model.embeddings_extractor.backbone_name == backbone
def test_full_pipeline(sample_dataloader, test_device): """Test complete training and inference pipeline.""" # Initialize model model = anodet.Padim(backbone="resnet18", device=test_device) # Train model.fit(sample_dataloader) # Test inference batch, _, _, _ = next(iter(sample_dataloader)) scores, maps = model.predict(batch) # Validate outputs assert scores.shape == (batch.size(0),) assert maps.shape == (batch.size(0), batch.size(2), batch.size(3))
- Individual function testing
- Component isolation
- Edge case validation
- Error handling verification
- End-to-end pipeline testing
- Component interaction validation
- Format compatibility testing
- Performance regression detection
from hypothesis import given, strategies as st @given( batch_size=st.integers(min_value=1, max_value=16), height=st.integers(min_value=32, max_value=512), width=st.integers(min_value=32, max_value=512) ) def test_predict_with_random_inputs(trained_padim_model, batch_size, height, width): """Test prediction with random input dimensions.""" batch = torch.randn(batch_size, 3, height, width) scores, maps = trained_padim_model.predict(batch) assert scores.shape == (batch_size,) assert maps.shape == (batch_size, height, width) assert torch.all(scores >= 0) assert torch.all(maps >= 0)
# .github/workflows/ci.yml name: CI on: push: branches: [develop, main] pull_request: branches: [develop, main] jobs: tests: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install poetry poetry install - name: Run tests run: poetry run pytest --cov=anodet - name: Upload coverage uses: codecov/codecov-action@v3
def test_inference_performance(trained_padim_model, sample_batch): """Test inference performance meets requirements.""" from anodet.general import Profiler batch, _, _, _ = sample_batch # Warmup for _ in range(3): _ = trained_padim_model.predict(batch) # Benchmark profiler = Profiler() runs = 10 for _ in range(runs): with profiler: scores, maps = trained_padim_model.predict(batch) avg_time = profiler.get_avg_time_ms(runs) fps = profiler.get_fps(len(batch) * runs) # Performance assertions assert avg_time < 1000, f"Inference too slow: {avg_time:.2f}ms" assert fps > 1, f"FPS too low: {fps:.2f}"
- Python 3.9+
- Git
- Poetry (recommended) or pip
- CUDA 11.7+ (optional, for GPU development)
# Clone repository git clone https://github.com/DeepKnowledge1/AnomaVision.git cd AnomaVision # Install development dependencies poetry install --dev # Activate virtual environment poetry shell # Install pre-commit hooks pre-commit install # Verify installation python -c "import anodet; print('β Development setup complete')"
# Format code with Black black . # Sort imports with isort isort . # Lint with flake8 flake8 anodet/ # Type checking with mypy (optional) mypy anodet/
# .pre-commit-config.yaml repos: - repo: https://github.com/psf/black rev: 23.9.1 hooks: - id: black language_version: python3.9 - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: - id: flake8 args: [--max-line-length=88, --extend-ignore=E203,W503]
{
"python.defaultInterpreterPath": ".venv/bin/python",
"python.formatting.provider": "black",
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests/"],
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
".pytest_cache": true,
".coverage": true,
"htmlcov": true
}
}- Set interpreter to poetry virtual environment
- Enable Black as code formatter
- Configure pytest as test runner
- Set up flake8 as external tool
# Debug training script python -m pdb train.py --config debug_config.yml # Debug with VS Code # Add to launch.json: { "name": "Debug Training", "type": "python", "request": "launch", "program": "train.py", "args": ["--config", "debug_config.yml"], "console": "integratedTerminal" }
# 1. Create feature branch git checkout -b feature/new-awesome-feature # 2. Make changes and test frequently poetry run pytest tests/test_new_feature.py # 3. Run full test suite poetry run pytest # 4. Check code quality black . flake8 anodet/ isort . # 5. Commit changes git add . git commit -m "feat: add awesome new feature" # 6. Push and create PR git push origin feature/new-awesome-feature
# Install documentation dependencies poetry install --extras docs # Build docs locally cd docs/ make html # Serve docs locally python -m http.server 8000 -d _build/html/
# Build package poetry build # Check package poetry run twine check dist/* # Install locally for testing pip install dist/anomavision-*.whl
Import errors after installation:
# Verify Python path python -c "import sys; print(sys.path)" # Reinstall in development mode poetry install --dev
CUDA-related errors:
# Check CUDA availability python -c "import torch; print(torch.cuda.is_available())" # Use CPU-only for development export CUDA_VISIBLE_DEVICES=""
Test failures:
# Run tests with verbose output poetry run pytest -v -s # Run specific failing test poetry run pytest tests/test_specific.py::test_function -v
Memory issues during testing:
# Run tests with limited parallelism poetry run pytest -x --tb=short # Use smaller test datasets export ANOMAVISION_TEST_SIZE=small
Q: What makes AnomaVision different from other anomaly detection libraries?
A: AnomaVision is specifically designed for production deployment with:
- 2-4x smaller model files through statistics-only storage
- Multi-format export from a single trained model
- CPU-first design that works without GPU requirements
- Unified inference interface across all formats
- Enterprise-grade performance with research-level accuracy
Q: Can I use AnomaVision without a GPU?
A: Yes! AnomaVision is designed with a CPU-first approach. All functionality works on CPU-only machines, and we provide optimized backends like OpenVINO for Intel hardware acceleration.
Q: How does AnomaVision compare to Anomalib?
A: AnomaVision wins on 10/10 performance metrics in our benchmarks. Key advantages:
- Faster inference times
- Smaller model files
- Better memory efficiency
- More deployment options
- Simpler API
Q: I'm getting import errors after installation. What should I do?
A: Try these solutions:
# Reinstall with poetry poetry install --dev poetry shell # Or use pip in a fresh environment pip uninstall anomavision pip install -r requirements.txt
Q: How do I install optional dependencies like OpenVINO?
A: Install additional backends as needed:
pip install openvino # Intel optimization pip install onnxruntime-gpu # ONNX with GPU support pip install tensorrt # NVIDIA optimization (future)
Q: How much training data do I need?
A: PaDiM requires only normal (non-anomalous) training data. Typical requirements:
- Minimum: 50-100 normal images
- Recommended: 200-500 normal images
- Optimal: 1000+ normal images
Q: What image sizes does AnomaVision support?
A: AnomaVision supports flexible image dimensions:
# Any aspect ratio and size dataset = anodet.AnodetDataset( path, resize=[640, 480], # Width x Height crop_size=[224, 224] # Final processing size )
Q: Can I use custom datasets?
A: Yes! AnomaVision supports:
- Custom directory structures
- Any image format (PNG, JPG, JPEG)
- Flexible preprocessing pipelines
- Custom normalization parameters
Q: Which export format should I use for production?
A: Choose based on your deployment target:
- ONNX: Universal deployment, cross-platform
- OpenVINO: Intel hardware (CPU/GPU)
- TorchScript: Python production environments
- Statistics (.pth): Smallest files, Python-only
Q: How do I optimize inference speed?
A: Use these optimization strategies:
# 1. Use appropriate backend model = ModelWrapper("model_openvino.xml", device='CPU') # 2. Batch processing dataloader = DataLoader(dataset, batch_size=32) # 3. Warmup model model.warmup(sample_batch, runs=3) # 4. Pin memory for GPU dataloader = DataLoader(dataset, pin_memory=True)
Q: Can I run AnomaVision on edge devices?
A: Yes! AnomaVision is optimized for edge deployment:
- Compact
.pthfiles (2-4x smaller) - CPU-optimized inference
- Low memory footprint
- ONNX export for embedded systems
Q: My inference is slow. How can I speed it up?
A: Try these optimizations:
- Use optimal model format:
# For Intel hardware model = ModelWrapper("model_openvino.xml", device='CPU') # For NVIDIA GPUs model = ModelWrapper("model.onnx", device='cuda')
- Optimize batch size:
# Find optimal batch size optimal_bs = find_optimal_batch_size(model_path, sample_batch)
- Use statistics files:
# 2-4x smaller, faster loading model = ModelWrapper("model.pth", device='cpu')
Q: How much memory does AnomaVision use?
A: Memory usage depends on configuration:
- Statistics files: ~10-50MB
- Full models: ~100-500MB
- Runtime memory: ~200MB-2GB (depends on batch size)
Q: I'm getting CUDA out of memory errors.
A: Reduce memory usage:
# Use smaller batch size dataloader = DataLoader(dataset, batch_size=1) # Use CPU backend model = ModelWrapper(model_path, device='cpu') # Use memory-efficient evaluation results = model.evaluate_memory_efficient(dataloader)
Q: My exported ONNX model gives different results than PyTorch.
A: This is usually due to precision differences:
# Ensure consistent precision model.save_statistics("model.pth", half=False) # Use FP32 # Load with force_fp32 stats = Padim.load_statistics("model.pth", force_fp32=True) # Use consistent export settings exporter.export_onnx(dynamic_batch=False, opset_version=17)
Q: Tests are failing in CI/CD. What should I check?
A: Common CI issues and solutions:
- CUDA not available:
# Use CPU-only tests @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") def test_gpu_function(): pass
- Memory limitations:
# Use smaller test datasets @pytest.fixture def small_dataset(): return create_small_test_dataset(size=10)
- Missing dependencies:
# Install all test dependencies poetry install --dev --extras test
Q: How do I debug model predictions?
A: Use these debugging techniques:
- Visualize intermediate results:
# Check feature extraction features, w, h = model.embeddings_extractor(batch) print(f"Features shape: {features.shape}") # Check distance computation distances = model.mahalanobisDistance(features, w, h) print(f"Distance range: {distances.min():.3f} - {distances.max():.3f}")
- Compare with reference:
# Load reference model ref_model = torch.load("reference_model.pt") ref_scores, ref_maps = ref_model.predict(batch) # Compare results score_diff = torch.abs(scores - ref_scores).max() print(f"Max score difference: {score_diff:.6f}")
Q: Can I modify the PaDiM algorithm?
A: Yes! AnomaVision is designed for customization:
# Custom layer hook def custom_hook(features): # Apply custom processing return F.normalize(features, dim=1) model = anodet.Padim( backbone='resnet18', layer_hook=custom_hook, layer_indices=[0, 1, 2] ) # Custom distance computation class CustomDistance(MahalanobisDistance): def forward(self, features, width, height, **kwargs): # Custom distance calculation return custom_distance_function(features) # Replace distance module model.mahalanobisDistance = CustomDistance(mean, cov_inv)
Q: How do I integrate AnomaVision with MLOps pipelines?
A: AnomaVision integrates well with MLOps tools:
# MLflow integration import mlflow with mlflow.start_run(): # Train model model = anodet.Padim() model.fit(dataloader) # Log metrics scores, maps = model.predict(test_batch) mlflow.log_metric("avg_score", scores.mean().item()) # Log model mlflow.pytorch.log_model(model, "anomaly_model") # Weights & Biases integration import wandb wandb.init(project="anomaly-detection") wandb.log({"avg_score": scores.mean().item()}) wandb.save("model.pt")
Q: Can I use AnomaVision for video anomaly detection?
A: Currently, AnomaVision focuses on image anomaly detection. For video:
# Process video frame by frame import cv2 def process_video(video_path, model_path): model = ModelWrapper(model_path, device='cuda') cap = cv2.VideoCapture(video_path) frame_scores = [] while True: ret, frame = cap.read() if not ret: break # Convert frame to batch rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) batch = anodet.to_batch([rgb_frame]) # Detect anomalies scores, maps = model.predict(batch) frame_scores.append(scores[0]) return frame_scores
Q: How can I contribute to AnomaVision?
A: We welcome contributions! See our Contributing section for details:
- Code contributions: Bug fixes, new features, optimizations
- Documentation: Improve docs, add examples, write tutorials
- Testing: Add test cases, improve coverage
- Feedback: Report bugs, suggest features, share use cases
Q: Where can I get help?
A: Multiple support channels available:
- GitHub Issues: Bug reports and feature requests
- GitHub Discussions: Questions and community support
- Email: Direct contact with maintainers
- Documentation: Comprehensive guides and examples
Q: Is AnomaVision suitable for commercial use?
A: Yes! AnomaVision is released under the MIT License, allowing commercial use. We also offer:
- Enterprise support: Custom development and consulting
- Training workshops: Team training and best practices
- Performance optimization: Custom optimizations for your use case
-
Statistics-only models: 2-4x smaller
.pthfiles withPadimLiteruntime - CPU-first design: Full functionality without GPU requirements
- Multi-format export: Single trained model β ONNX, TorchScript, OpenVINO
-
Unified inference:
ModelWrapperprovides consistent API across formats - Flexible image processing: Support for any aspect ratio and dimensions
- Optimized feature extraction: Faster ResNet processing
- Memory-efficient evaluation: Reduced memory usage for large datasets
- Chunked distance computation: Better memory management
- GPU memory optimization: Automatic mixed precision support
- Enhanced testing: Comprehensive test suite with 90%+ coverage
- CI/CD pipeline: Automated testing and quality checks
- Configuration system: YAML-based configuration with CLI override
- Profiling tools: Built-in performance measurement
- Complete API reference: Detailed documentation for all classes
- Usage examples: Real-world examples and tutorials
- Performance guides: Optimization recommendations
- Deployment guides: Production deployment best practices
- Fixed memory leaks in batch processing
- Resolved ONNX export compatibility issues
- Fixed visualization rendering problems
- Corrected device placement inconsistencies
AnomaVision is released under the MIT License.
MIT License
Copyright (c) 2025 DeepKnowledge Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
AnomaVision builds upon excellent open-source projects:
- PyTorch: BSD-style license
- torchvision: BSD 3-Clause license
- NumPy: BSD license
- Pillow: HPND license
- OpenCV: Apache 2.0 license
- ONNX Runtime: MIT license
- OpenVINO: Apache 2.0 license
If AnomaVision helps your research or project, please cite:
@software{anomavision2025, title={AnomaVision: Edge-Ready Visual Anomaly Detection}, author={DeepKnowledge Contributors}, year={2025}, url={https://github.com/DeepKnowledge1/AnomaVision}, version={2.0.46}, note={High-performance anomaly detection library optimized for edge deployment} }
- π¬ GitHub Discussions: Community Forum
- π Issues: Bug Reports & Features
- π§ Email: deepp.knowledge@gmail.com
- π Documentation: Wiki
For enterprise deployments, custom integrations, or commercial support:
- π’ Enterprise Consulting: Custom development and optimization
- π Training Workshops: Team training and best practices
- π§ Custom Development: Tailored solutions for your use case
- β‘ Performance Optimization: Hardware-specific optimizations
Special thanks to:
- PaDiM Authors: For the original algorithm (Defard et al.)
- PyTorch Team: For the excellent deep learning framework
- ONNX Community: For cross-platform deployment standards
- Intel: For OpenVINO optimization toolkit
- Contributors: All community members who help improve AnomaVision
Stop settling for slow, bloated solutions. Experience the future of edge-ready anomaly detection.
Get Started Documentation Star Us
π Benchmark Results Don't Lie: AnomaVision Wins 10/10 Metrics
Deploy fast. Detect better. Scale everywhere.
Made with β€οΈ for the edge AI community
Last updated: January 2025 | AnomaVision v2.0.46