Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ anomavision detect --config config.yml --img_path ./test_images --thresh 13.0
anomavision eval --config config.yml --enable_visualization

# Export to ONNX / TorchScript / OpenVINO / all
anomavision export --model padim_model.pt --format all --precision fp16
anomavision export --config config.yml --model model.pt --format all --precision fp16
```

Every command has full `--help`:
Expand Down Expand Up @@ -187,8 +187,8 @@ model = anomavision.Padim(
model.fit(loader)

# --- 3. Save ---
torch.save(model, "padim_model.pt") # full model (for export or further use)
model.save_statistics("padim_model.pth", half=True) # stats-only (smaller, faster to load)
torch.save(model, "model.pt") # full model (for export or further use)
model.save_statistics("model.pth", half=True) # stats-only (smaller, faster to load)

# --- 4. Infer ---
# scores: (batch_size,) β€” scalar anomaly score per image. Higher = more anomalous.
Expand Down Expand Up @@ -302,7 +302,7 @@ Full docs at **http://localhost:8000/docs** once the server is running.
```bash
anomavision export \
--model_data_path ./distributions/anomav_exp \
--model padim_model.pt \
--model model.pt \
--format onnx \
--precision fp16 \
--quantize-dynamic
Expand All @@ -329,7 +329,7 @@ stream_mode: true
stream_source:
type: webcam
camera_id: 0
model: padim_model.onnx
model: model.onnx
thresh: 13.0
enable_visualization: true
```
Expand Down Expand Up @@ -361,11 +361,11 @@ backbone: resnet18
batch_size: 16
feat_dim: 100
layer_indices: [0, 1, 2]
output_model: padim_model.pt
output_model: model.pt
run_name: exp1
model_data_path: ./distributions/anomav_exp

model: padim_model.onnx
model: model.onnx
device: auto # auto | cpu | cuda
thresh: 13.0

Expand Down
12 changes: 6 additions & 6 deletions anomavision/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

Examples:
anomavision train --config config.yml
anomavision export --model padim_model.pt --format onnx
anomavision detect --model padim_model.onnx --img_path ./test_images
anomavision eval --model padim_model.pt --class_name bottle
anomavision export --config config.yml --model model.pt --format onnx
anomavision detect --config config.yml --model model.onnx --img_path ./test_images
anomavision eval --config config.yml --model model.pt --class_name bottle
"""

import argparse
Expand All @@ -35,9 +35,9 @@ def create_parser() -> argparse.ArgumentParser:
epilog="""
Examples:
%(prog)s train --config config.yml --dataset_path /data --class_name bottle
%(prog)s export --model padim_model.pt --format onnx --quantize-dynamic
%(prog)s detect --model padim_model.onnx --img_path ./test --enable_visualization
%(prog)s eval --model padim_model.pt --class_name bottle --dataset_path /data
%(prog)s export --model model.pt --format onnx --quantize-dynamic
%(prog)s detect --model model.onnx --img_path ./test --enable_visualization
%(prog)s eval --model model.pt --class_name bottle --dataset_path /data

For detailed help on each command:
%(prog)s train --help
Expand Down
40 changes: 29 additions & 11 deletions anomavision/detect.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""
Run Anomaly detection inference on images using various model formats.
Usage - formats:
$ python detect.py --model padim_model.pt # PyTorch
padim_model.torchscript # TorchScript
padim_model.onnx # ONNX Runtime
padim_model_openvino # OpenVINO
padim_model.engine # TensorRT
$ python detect.py --model model.pt # PyTorch
model.torchscript # TorchScript
model.onnx # ONNX Runtime
model_openvino # OpenVINO
model.engine # TensorRT
"""

import argparse
Expand Down Expand Up @@ -61,13 +61,19 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
parser.add_argument(
"--model_data_path",
type=str,
default="./distributions/anomav_exp",
default="./distributions",
help="Directory containing model files.",
)
parser.add_argument(
"--algorithm",
type=str,
default=None,
help="Algorithm name (e.g., padim, patchcore).",
)
parser.add_argument(
"--model",
type=str,
default="padim_model.pt",
default=None,
help="Model file (.pt for PyTorch, .onnx for ONNX, .engine for TensorRT)",
)
parser.add_argument(
Expand Down Expand Up @@ -121,7 +127,7 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
)
parser.add_argument(
"--run_name",
default="detect_exp",
default=None,
help="experiment name for this inference run",
)
parser.add_argument(
Expand Down Expand Up @@ -178,7 +184,19 @@ def run_inference(args):
cfg = load_config(str(args.config))
else:
# Fallback to model directory config
cfg = load_config(str(Path(args.model_data_path) / "config.yml"))
potential_paths = []
if args.model_data_path:
base_path = Path(args.model_data_path)
potential_paths.append(base_path / "config.yml")

cfg = {}
for path in potential_paths:
if path.exists():
cfg = load_config(str(path))
break

if not cfg:
cfg = {}

# Merge config with CLI args
config = edict(merge_config(args, cfg))
Expand Down Expand Up @@ -253,7 +271,7 @@ def run_inference(args):

# --- Model Loading Phase ---
with profilers["model_loading"]:
model_path = os.path.join(MODEL_DATA_PATH, config.model)
model_path = os.path.join(MODEL_DATA_PATH, config.algorithm, config.class_name, config.run_name, config.model)
logger.info(f"Loading model: {model_path}")

if not os.path.exists(model_path):
Expand All @@ -273,7 +291,7 @@ def run_inference(args):
run_name = config.run_name
viz_output_dir = config.get("viz_output_dir", "./visualizations/")
RESULTS_PATH = increment_path(
Path(viz_output_dir) / model_type.value.upper() / run_name,
Path(viz_output_dir) / config.algorithm / config.class_name / model_type.value.upper() / run_name,
exist_ok=config.get("overwrite", False),
mkdir=True,
)
Expand Down
73 changes: 33 additions & 40 deletions anomavision/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
get_logger,
merge_config,
setup_logging,
find_best_threshold_f1,
compute_metrics,
find_optimal_threshold
)


Expand All @@ -40,7 +43,6 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
"--dataset_path",
default=None,
type=str,
required=False,
help="Path to the dataset folder containing test images.",
)
parser.add_argument(
Expand All @@ -54,13 +56,19 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
parser.add_argument(
"--model_data_path",
type=str,
default="./distributions/anomav_exp",
default=None,
help="Directory containing AnomaVision model files.",
)
parser.add_argument(
"--algorithm",
type=str,
default=None,
help="Algorithm name (e.g., padim, patchcore).",
)
parser.add_argument(
"--model",
type=str,
default="padim_model.pt",
default=None,
help="Model filename (.pt, .onnx, .engine)",
)
parser.add_argument(
Expand Down Expand Up @@ -110,7 +118,7 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
parser.add_argument(
"--viz_output_dir",
type=str,
default="./eval_visualizations/",
default=None,
help="Directory to save visualization images.",
)

Expand All @@ -131,38 +139,6 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
return parser


def compute_metrics(labels, scores, thresh=None):
"""
Calculate standard anomaly detection metrics.
"""
metrics = {}

# AUROC
try:
metrics['auc_score'] = float(roc_auc_score(labels, scores))
except ValueError:
metrics['auc_score'] = 0.0

# PR-AUC
try:
precision, recall, _ = precision_recall_curve(labels, scores)
metrics['pr_auc'] = float(auc(recall, precision))
except ValueError:
metrics['pr_auc'] = 0.0

# Statistics
metrics['mean_anomaly_score'] = float(np.mean(scores))
metrics['std_anomaly_score'] = float(np.std(scores))

# Accuracy (if thresh provided)
if thresh is not None:
predictions = (scores > thresh).astype(int)
metrics['accuracy'] = float(np.mean(predictions == labels))
metrics['thresh'] = thresh

return metrics


def evaluate_model_with_wrapper(
model_wrapper, test_dataloader, logger, evaluation_profiler, detailed_timing=False
):
Expand Down Expand Up @@ -263,7 +239,7 @@ def run_evaluation(args):

# Load Model Phase
with profilers["model_loading"]:
model_path = os.path.join(MODEL_DATA_PATH, config.model)
model_path = os.path.join(MODEL_DATA_PATH, config.algorithm, config.class_name, config.run_name, config.model)
logger.info(f"Loading model: {model_path}")

if not os.path.exists(model_path):
Expand Down Expand Up @@ -323,7 +299,12 @@ def run_evaluation(args):
model_wrapper.close()

# Compute Metrics
metrics = compute_metrics(labels, scores, thresh=config.thresh)
if config.thresh is None:
best_thresh, _ = find_optimal_threshold(labels, scores)
else:
best_thresh = config.thresh

metrics = compute_metrics(labels, scores, thresh=best_thresh)

# Add timing metrics
total_images = len(test_dataset)
Expand Down Expand Up @@ -368,7 +349,7 @@ def run_evaluation(args):
logger.info(f"Data loading time: {profilers['data_loading'].accumulated_time * 1000:.2f} ms")
logger.info(f"Evaluation time: {profilers['evaluation'].accumulated_time * 1000:.2f} ms")
logger.info(f"Visualization time: {profilers['visualization'].accumulated_time * 1000:.2f} ms")
logger.info("=" * 60)
# logger.info("=" * 60)

# 2. PERFORMANCE METRICS
logger.info("=" * 60)
Expand All @@ -382,7 +363,7 @@ def run_evaluation(args):
if len(test_dataloader) > 0:
images_per_batch = total_images / len(test_dataloader)
logger.info(f"Evaluation throughput: {evaluation_fps * images_per_batch:.1f} images/sec (batch size: {batch_size})")
logger.info("=" * 60)
# logger.info("=" * 60)

# 3. EVALUATION SUMMARY
logger.info("=" * 60)
Expand All @@ -393,6 +374,18 @@ def run_evaluation(args):
logger.info(f"Model type: {model_type.value.upper() if model_type else 'UNKNOWN'}")
logger.info(f"Device: {device_str}")
logger.info(f"Image processing: resize={config.resize}, crop_size={config.crop_size}, normalize={config.normalize}")
# logger.info("=" * 60)

logger.info("=" * 60)
logger.info("ANOMAVISION DETECTION METRICS")
logger.info("=" * 60)

for k, v in metrics.items():
if isinstance(v, float):
logger.info(f"{k.replace('_',' ').title():<28} {v:.6f}")
else:
logger.info(f"{k.replace('_',' ').title():<28} {v}")

logger.info("=" * 60)

logger.info("AnomaVision anomaly detection model evaluation completed successfully")
Expand Down
30 changes: 26 additions & 4 deletions anomavision/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,10 +604,17 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
parser.add_argument(
"--model_data_path",
type=str,
default="./distributions/anomav_exp",
default="./distributions",
help="Directory containing model and output location",
)

parser.add_argument(
"--algorithm",
type=str,
default=None,
help="Algorithm name (e.g., padim, patchcore).",
)

parser.add_argument(
"--model",
type=str,
Expand Down Expand Up @@ -692,16 +699,31 @@ def main(args=None):
if args.config is not None:
cfg = load_config(str(args.config))
else:
cfg = load_config(str(Path(args.model_data_path) / "config.yml"))
# Try to find config in structured path first
potential_paths = []
if args.model_data_path:
# Try to infer from model path if it follows structure
base_path = Path(args.model_data_path)
potential_paths.append(base_path / "config.yml")

# Use first existing config
cfg = {}
for path in potential_paths:
if path.exists():
cfg = load_config(str(path))
break

if not cfg:
cfg = {}

config = edict(merge_config(args, cfg))

# Setup logging & logger
setup_logging(enabled=True, log_level=config.log_level, log_to_file=True)
logger = get_logger("anomavision.export")

model_path = Path(config.model_data_path) / config.model
output_dir = Path(config.model_data_path)
model_path = Path(config.model_data_path) / config.algorithm / config.class_name / config.run_name / config.model
output_dir = Path(config.model_data_path) / config.algorithm / config.class_name / config.run_name
model_stem = Path(config.model).stem

# Generate output names
Expand Down
8 changes: 7 additions & 1 deletion anomavision/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ def create_parser(add_help: bool = True) -> argparse.ArgumentParser:
default=None,
help="Directory to save model distributions and PT file.",
)
parser.add_argument(
"--algorithm",
type=str,
default=None,
help="Algorithm name (e.g., padim, patchcore).",
)
parser.add_argument(
"--log_level",
type=str,
Expand Down Expand Up @@ -176,7 +182,7 @@ def run_training(args):

# Resolve output run dir once
run_dir = increment_path(
Path(config.model_data_path) / config.run_name, exist_ok=True, mkdir=True
Path(config.model_data_path) / config.algorithm / config.class_name / config.run_name, exist_ok=True, mkdir=True
)

# === Dataset ===
Expand Down
Loading
Loading