305 lines
11 KiB
Python
305 lines
11 KiB
Python
|
|
import json
|
|||
|
|
import pickle
|
|||
|
|
import shutil
|
|||
|
|
from datetime import datetime
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
import numpy as np
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# User-configurable params
|
|||
|
|
# =========================
|
|||
|
|
# Single experiment to plot (stem of the .bag file, e.g. "3_smoke_human_walking_2023-01-23")
|
|||
|
|
EXPERIMENT_NAME = "3_smoke_human_walking_2023-01-23"
|
|||
|
|
|
|||
|
|
# Directory that contains {EXPERIMENT_NAME}_{method}_scores.npy for methods in {"deepsad","ocsvm","isoforest"}
|
|||
|
|
# Adjust this to where you save your per-method scores.
|
|||
|
|
methods_scores_path = Path(
|
|||
|
|
"/home/fedex/mt/projects/thesis-kowalczyk-jan/Deep-SAD-PyTorch/infer/DeepSAD/test/inference"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Root data path containing .bag files used to build the cached stats
|
|||
|
|
all_data_path = Path("/home/fedex/mt/data/subter")
|
|||
|
|
|
|||
|
|
# Output base directory (timestamped subfolder will be created here, then archived and copied to "latest/")
|
|||
|
|
output_path = Path("/home/fedex/mt/plots/results_inference_timeline")
|
|||
|
|
|
|||
|
|
# Cache (stats + labels) directory — same as your original script
|
|||
|
|
cache_path = output_path
|
|||
|
|
|
|||
|
|
# Assumed LiDAR frame resolution to convert counts -> percent (unchanged from original)
|
|||
|
|
data_resolution = 32 * 2048
|
|||
|
|
|
|||
|
|
# Frames per second for x-axis time
|
|||
|
|
FPS = 10.0
|
|||
|
|
|
|||
|
|
# Whether to try to align score sign so that higher = more degraded.
|
|||
|
|
# If manual labels exist for this experiment, alignment uses anomaly window mean vs. outside.
|
|||
|
|
ALIGN_SCORE_DIRECTION = True
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Setup output folders
|
|||
|
|
# =========================
|
|||
|
|
datetime_folder_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|||
|
|
latest_folder_path = output_path / "latest"
|
|||
|
|
archive_folder_path = output_path / "archive"
|
|||
|
|
output_datetime_path = output_path / datetime_folder_name
|
|||
|
|
|
|||
|
|
output_path.mkdir(exist_ok=True, parents=True)
|
|||
|
|
output_datetime_path.mkdir(exist_ok=True, parents=True)
|
|||
|
|
latest_folder_path.mkdir(exist_ok=True, parents=True)
|
|||
|
|
archive_folder_path.mkdir(exist_ok=True, parents=True)
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Discover experiments to reconstruct indices consistent with caches
|
|||
|
|
# =========================
|
|||
|
|
normal_experiment_paths, anomaly_experiment_paths = [], []
|
|||
|
|
if not all_data_path.exists():
|
|||
|
|
raise FileNotFoundError(f"all_data_path does not exist: {all_data_path}")
|
|||
|
|
|
|||
|
|
for bag_file_path in all_data_path.iterdir():
|
|||
|
|
if bag_file_path.suffix != ".bag":
|
|||
|
|
continue
|
|||
|
|
if "smoke" in bag_file_path.name:
|
|||
|
|
anomaly_experiment_paths.append(bag_file_path)
|
|||
|
|
else:
|
|||
|
|
normal_experiment_paths.append(bag_file_path)
|
|||
|
|
|
|||
|
|
# Sort by filesize to match original ordering used when caches were generated
|
|||
|
|
normal_experiment_paths = sorted(
|
|||
|
|
normal_experiment_paths, key=lambda p: p.stat().st_size
|
|||
|
|
)
|
|||
|
|
anomaly_experiment_paths = sorted(
|
|||
|
|
anomaly_experiment_paths, key=lambda p: p.stat().st_size
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Find the path for the requested experiment
|
|||
|
|
exp_path = None
|
|||
|
|
exp_is_anomaly = None
|
|||
|
|
for p in anomaly_experiment_paths:
|
|||
|
|
if p.stem == EXPERIMENT_NAME:
|
|||
|
|
exp_path = p
|
|||
|
|
exp_is_anomaly = True
|
|||
|
|
break
|
|||
|
|
if exp_path is None:
|
|||
|
|
for p in normal_experiment_paths:
|
|||
|
|
if p.stem == EXPERIMENT_NAME:
|
|||
|
|
exp_path = p
|
|||
|
|
exp_is_anomaly = False
|
|||
|
|
break
|
|||
|
|
if exp_path is None:
|
|||
|
|
raise FileNotFoundError(
|
|||
|
|
f"Experiment '{EXPERIMENT_NAME}' not found as a .bag in {all_data_path}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Get the index within the appropriate list
|
|||
|
|
if exp_is_anomaly:
|
|||
|
|
exp_index = anomaly_experiment_paths.index(exp_path)
|
|||
|
|
else:
|
|||
|
|
exp_index = normal_experiment_paths.index(exp_path)
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Load cached statistical data
|
|||
|
|
# =========================
|
|||
|
|
missing_points_cache = Path(cache_path / "missing_points.pkl")
|
|||
|
|
near_sensor_cache = Path(cache_path / "particles_near_sensor_counts_500.pkl")
|
|||
|
|
|
|||
|
|
if not missing_points_cache.exists():
|
|||
|
|
raise FileNotFoundError(f"Missing points cache not found: {missing_points_cache}")
|
|||
|
|
if not near_sensor_cache.exists():
|
|||
|
|
raise FileNotFoundError(f"Near-sensor cache not found: {near_sensor_cache}")
|
|||
|
|
|
|||
|
|
with open(missing_points_cache, "rb") as f:
|
|||
|
|
missing_points_normal, missing_points_anomaly = pickle.load(f)
|
|||
|
|
with open(near_sensor_cache, "rb") as f:
|
|||
|
|
near_sensor_normal, near_sensor_anomaly = pickle.load(f)
|
|||
|
|
|
|||
|
|
if exp_is_anomaly:
|
|||
|
|
missing_points_series = np.asarray(missing_points_anomaly[exp_index], dtype=float)
|
|||
|
|
near_sensor_series = np.asarray(near_sensor_anomaly[exp_index], dtype=float)
|
|||
|
|
else:
|
|||
|
|
missing_points_series = np.asarray(missing_points_normal[exp_index], dtype=float)
|
|||
|
|
near_sensor_series = np.asarray(near_sensor_normal[exp_index], dtype=float)
|
|||
|
|
|
|||
|
|
# Convert counts to percentages of total points
|
|||
|
|
missing_points_pct = (missing_points_series / data_resolution) * 100.0
|
|||
|
|
near_sensor_pct = (near_sensor_series / data_resolution) * 100.0
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Load manual anomaly frame borders (optional; used for sign alignment + vertical markers)
|
|||
|
|
# =========================
|
|||
|
|
manually_labeled_anomaly_frames = {}
|
|||
|
|
labels_json_path = cache_path / "manually_labeled_anomaly_frames.json"
|
|||
|
|
if labels_json_path.exists():
|
|||
|
|
with open(labels_json_path, "r") as frame_borders_file:
|
|||
|
|
manually_labeled_anomaly_frames_json = json.load(frame_borders_file)
|
|||
|
|
for file in manually_labeled_anomaly_frames_json.get("files", []):
|
|||
|
|
manually_labeled_anomaly_frames[file["filename"]] = (
|
|||
|
|
file.get("semi_target_begin_frame", None),
|
|||
|
|
file.get("semi_target_end_frame", None),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# The JSON uses .npy filenames (as in original script). Create this experiment’s key.
|
|||
|
|
exp_npy_filename = exp_path.with_suffix(".npy").name
|
|||
|
|
anomaly_window = manually_labeled_anomaly_frames.get(exp_npy_filename, (None, None))
|
|||
|
|
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Load method scores and z-score normalize per method
|
|||
|
|
# =========================
|
|||
|
|
def zscore_1d(x: np.ndarray, eps=1e-12):
|
|||
|
|
x = np.asarray(x, dtype=float)
|
|||
|
|
mu = np.mean(x)
|
|||
|
|
sigma = np.std(x, ddof=0)
|
|||
|
|
if sigma < eps:
|
|||
|
|
return np.zeros_like(x)
|
|||
|
|
return (x - mu) / sigma
|
|||
|
|
|
|||
|
|
|
|||
|
|
def maybe_align_direction(z: np.ndarray, window):
|
|||
|
|
"""Flip sign so that the anomaly window mean is higher than the outside mean, if labels exist."""
|
|||
|
|
start, end = window
|
|||
|
|
if start is None or end is None:
|
|||
|
|
return z # no labels → leave as-is
|
|||
|
|
start = int(max(0, start))
|
|||
|
|
end = int(min(len(z), end))
|
|||
|
|
if end <= start or end > len(z):
|
|||
|
|
return z
|
|||
|
|
inside_mean = float(np.mean(z[start:end]))
|
|||
|
|
# outside: everything except [start:end]; handle edge cases
|
|||
|
|
if start == 0 and end == len(z):
|
|||
|
|
return z
|
|||
|
|
outside_parts = []
|
|||
|
|
if start > 0:
|
|||
|
|
outside_parts.append(z[:start])
|
|||
|
|
if end < len(z):
|
|||
|
|
outside_parts.append(z[end:])
|
|||
|
|
if not outside_parts:
|
|||
|
|
return z
|
|||
|
|
outside_mean = float(np.mean(np.concatenate(outside_parts)))
|
|||
|
|
return z if inside_mean >= outside_mean else -z
|
|||
|
|
|
|||
|
|
|
|||
|
|
methods = ["deepsad", "ocsvm", "isoforest"]
|
|||
|
|
method_scores = {}
|
|||
|
|
method_zscores = {}
|
|||
|
|
|
|||
|
|
if not methods_scores_path.exists():
|
|||
|
|
raise FileNotFoundError(
|
|||
|
|
f"Methods scores path does not exist: {methods_scores_path}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
for m in methods:
|
|||
|
|
file_path = methods_scores_path / f"{EXPERIMENT_NAME}_{m}_scores.npy"
|
|||
|
|
if not file_path.exists():
|
|||
|
|
raise FileNotFoundError(f"Missing scores file for method '{m}': {file_path}")
|
|||
|
|
s = np.load(file_path)
|
|||
|
|
s = np.asarray(s, dtype=float).reshape(-1)
|
|||
|
|
# If needed, truncate or pad to match stats length (should match if generated consistently)
|
|||
|
|
n = min(len(s), len(missing_points_pct))
|
|||
|
|
if len(s) != len(missing_points_pct):
|
|||
|
|
# Align by truncation to the shortest length
|
|||
|
|
s = s[:n]
|
|||
|
|
# Also truncate stats to match
|
|||
|
|
missing_points_pct = missing_points_pct[:n]
|
|||
|
|
near_sensor_pct = near_sensor_pct[:n]
|
|||
|
|
z = zscore_1d(s)
|
|||
|
|
if ALIGN_SCORE_DIRECTION:
|
|||
|
|
z = maybe_align_direction(z, anomaly_window)
|
|||
|
|
method_scores[m] = s
|
|||
|
|
method_zscores[m] = z
|
|||
|
|
|
|||
|
|
# Common time axis in seconds
|
|||
|
|
num_frames = len(missing_points_pct)
|
|||
|
|
t = np.arange(num_frames) / FPS
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Plot 1: Missing points (%) vs. method z-scores
|
|||
|
|
# =========================
|
|||
|
|
fig1, axz1 = plt.subplots(figsize=(14, 6), constrained_layout=True)
|
|||
|
|
axy1 = axz1.twinx()
|
|||
|
|
|
|||
|
|
# plot z-scores
|
|||
|
|
for m in methods:
|
|||
|
|
axz1.plot(t, method_zscores[m], label=f"{m} (z)", alpha=0.9)
|
|||
|
|
|
|||
|
|
# plot missing points (%)
|
|||
|
|
axy1.plot(t, missing_points_pct, linestyle="--", alpha=0.7, label="Missing points (%)")
|
|||
|
|
|
|||
|
|
# vertical markers for anomaly window if available
|
|||
|
|
start, end = anomaly_window
|
|||
|
|
if start is not None and end is not None and 0 <= start < end <= num_frames:
|
|||
|
|
axz1.axvline(x=start / FPS, linestyle=":", alpha=0.6)
|
|||
|
|
axz1.axvline(x=end / FPS, linestyle=":", alpha=0.6)
|
|||
|
|
|
|||
|
|
axz1.set_xlabel("Time (s)")
|
|||
|
|
axz1.set_ylabel("Anomaly score (z-score, ↑ = more degraded)")
|
|||
|
|
axy1.set_ylabel("Missing points (%)")
|
|||
|
|
axz1.set_title(f"{EXPERIMENT_NAME}\nDegradation vs. Missing Points")
|
|||
|
|
|
|||
|
|
# Build a combined legend
|
|||
|
|
lines1, labels1 = axz1.get_legend_handles_labels()
|
|||
|
|
lines2, labels2 = axy1.get_legend_handles_labels()
|
|||
|
|
axz1.legend(lines1 + lines2, labels1 + labels2, loc="upper right")
|
|||
|
|
|
|||
|
|
axz1.grid(True, alpha=0.3)
|
|||
|
|
fig1.savefig(
|
|||
|
|
output_datetime_path / f"{EXPERIMENT_NAME}_zscores_vs_missing_points.png", dpi=150
|
|||
|
|
)
|
|||
|
|
plt.close(fig1)
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Plot 2: Near-sensor (%) vs. method z-scores
|
|||
|
|
# =========================
|
|||
|
|
fig2, axz2 = plt.subplots(figsize=(14, 6), constrained_layout=True)
|
|||
|
|
axy2 = axz2.twinx()
|
|||
|
|
|
|||
|
|
for m in methods:
|
|||
|
|
axz2.plot(t, method_zscores[m], label=f"{m} (z)", alpha=0.9)
|
|||
|
|
|
|||
|
|
axy2.plot(t, near_sensor_pct, linestyle="--", alpha=0.7, label="Near-sensor <0.5m (%)")
|
|||
|
|
|
|||
|
|
start, end = anomaly_window
|
|||
|
|
if start is not None and end is not None and 0 <= start < end <= num_frames:
|
|||
|
|
axz2.axvline(x=start / FPS, linestyle=":", alpha=0.6)
|
|||
|
|
axz2.axvline(x=end / FPS, linestyle=":", alpha=0.6)
|
|||
|
|
|
|||
|
|
axz2.set_xlabel("Time (s)")
|
|||
|
|
axz2.set_ylabel("Anomaly score (z-score, ↑ = more degraded)")
|
|||
|
|
axy2.set_ylabel("Near-sensor points (%)")
|
|||
|
|
axz2.set_title(f"{EXPERIMENT_NAME}\nDegradation vs. Near-Sensor Points (<0.5 m)")
|
|||
|
|
|
|||
|
|
lines1, labels1 = axz2.get_legend_handles_labels()
|
|||
|
|
lines2, labels2 = axy2.get_legend_handles_labels()
|
|||
|
|
axz2.legend(lines1 + lines2, labels1 + labels2, loc="upper right")
|
|||
|
|
|
|||
|
|
axz2.grid(True, alpha=0.3)
|
|||
|
|
fig2.savefig(
|
|||
|
|
output_datetime_path / f"{EXPERIMENT_NAME}_zscores_vs_near_sensor.png", dpi=150
|
|||
|
|
)
|
|||
|
|
plt.close(fig2)
|
|||
|
|
|
|||
|
|
# =========================
|
|||
|
|
# Preserve latest/, archive/, copy script
|
|||
|
|
# =========================
|
|||
|
|
|
|||
|
|
# delete current latest folder
|
|||
|
|
shutil.rmtree(latest_folder_path, ignore_errors=True)
|
|||
|
|
|
|||
|
|
# create new latest folder
|
|||
|
|
latest_folder_path.mkdir(exist_ok=True, parents=True)
|
|||
|
|
|
|||
|
|
# copy contents of output folder to the latest folder
|
|||
|
|
for file in output_datetime_path.iterdir():
|
|||
|
|
shutil.copy2(file, latest_folder_path)
|
|||
|
|
|
|||
|
|
# copy this python script to preserve the code used
|
|||
|
|
shutil.copy2(__file__, output_datetime_path)
|
|||
|
|
shutil.copy2(__file__, latest_folder_path)
|
|||
|
|
|
|||
|
|
# move output date folder to archive
|
|||
|
|
shutil.move(output_datetime_path, archive_folder_path)
|
|||
|
|
|
|||
|
|
print("Done. Plots saved and archived.")
|