TrackChain / ml /tests /test_patchcore_enhanced.py
PRiyanshu0-1's picture
Add ml/ codebase and configs
2adb41a verified
Raw History Blame Contribute Delete
4.02 kB
"""
Unit tests for TrackChain Enhanced PatchCore visual anomaly detection, dataset expansion, and calibration.
"""
import os
import sys
import json
import tempfile
from pathlib import Path
import numpy as np
import pytest
from PIL import Image
# Ensure repo root is in sys.path
repo_root = Path(__file__).resolve().parent.parent.parent
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from ml.scripts.expand_datasets import (
generate_track_pattern,
create_normal_augmentation_pipeline,
augment_normal_image,
ensure_diversity,
expand_patchcore_dataset,
)
from ml.models.vision.anomaly import PatchCoreAnomalyDetector, get_default_transform
from ml.training.train_anomaly import greedy_coreset_subsampling, train_patchcore
from ml.calibration.patchcore_scale import SigmoidDistanceCalibrator
from ml.core.schema import CalibratedSignal, SignalType, DefectClass
def test_procedural_synthetic_track_generation():
"""Verify procedural generation creates a valid 3-channel track image."""
img = generate_track_pattern(width=320, height=320)
assert img is not None
assert img.shape == (320, 320, 3)
assert img.dtype == np.uint8
# Non-uniform image
assert img.std() > 5.0
def test_normal_augmentation_pipeline():
"""Verify normal track augmentation produces augmented variants without crashing."""
pipelines = create_normal_augmentation_pipeline()
assert pipelines is not None
assert "lighting" in pipelines
assert "weather" in pipelines
assert "geometric" in pipelines
assert "texture" in pipelines
dummy = np.zeros((100, 100, 3), dtype=np.uint8) + 120
augmented = augment_normal_image(dummy, pipelines, num_augmentations=3)
assert len(augmented) == 3
for a in augmented:
assert a.shape == (100, 100, 3)
def test_diversity_clustering_selection(tmp_path):
"""Verify KMeans feature diversity sampling selects diverse subset."""
img_paths = []
for i in range(10):
img = np.random.randint(0, 255, (64, 64, 3), dtype=np.uint8)
p = tmp_path / f"test_{i}.jpg"
Image.fromarray(img).save(p)
img_paths.append(p)
selected = ensure_diversity(img_paths, target_count=4, num_clusters=3)
assert len(selected) == 4
for s in selected:
assert Path(s).exists()
def test_greedy_coreset_minimax_behavior():
"""Verify greedy coreset selection reduces sample count and spans maximum distance."""
np.random.seed(42)
features = np.random.randn(100, 32).astype(np.float32)
coreset = greedy_coreset_subsampling(features, sampling_ratio=0.15)
assert coreset.shape[0] == 15
assert coreset.shape[1] == 32
def test_multi_scale_feature_extraction():
"""Verify PatchCore multi-scale patch extraction returns correct tensor dimensions."""
detector = PatchCoreAnomalyDetector(backbone_name="resnet18", patch_size=3)
import torch
dummy_tensor = torch.zeros((1, 3, 224, 224), dtype=torch.float32)
with torch.no_grad():
feats, (h, w) = detector.extract_features(dummy_tensor, patch_size=3)
assert feats.ndim == 2
assert feats.shape[1] == detector.feature_dim
assert h > 0 and w > 0
def test_patchcore_full_prediction_and_bounding_box():
"""Verify full PatchCore inference with calibrated output and localized bounding box."""
detector = PatchCoreAnomalyDetector(backbone_name="resnet18")
# Create synthetic memory bank
np.random.seed(42)
mock_bank = np.random.randn(50, detector.feature_dim).astype(np.float32)
detector.set_memory_bank(mock_bank)
detector.calibrator.fit([1.0, 2.0, 3.0, 4.0])
dummy_frame = np.ones((224, 224, 3), dtype=np.uint8) * 100
signals = detector.predict(dummy_frame)
assert len(signals) == 1
sig = signals[0]
assert isinstance(sig, CalibratedSignal)
assert sig.signal_type == SignalType.VISUAL_NOVEL
assert sig.predicted_class == DefectClass.VISUAL_ANOMALY
assert 0.0 <= sig.calibrated_prob <= 1.0