Gaze-LIPE / scripts /audit_p01_legacy_row_mapping.py
thanhhuyvan's picture
Publish KD reproducibility investigation
178f61f
Raw
History Blame Contribute Delete
7.98 kB
"""Prove and export the recovered p01 legacy-H5-row to MPIIGaze-frame mapping."""
from __future__ import annotations
import csv
import hashlib
import json
from pathlib import Path
import sys
ROOT = Path(r"E:\Gaze_estimation")
sys.path.insert(0, str(ROOT / ".codex_deps"))
import h5py
import numpy as np
LEGACY = ROOT / "data" / "processed" / "p01_v16.h5"
CLEAN = ROOT / "data" / "processed_kd_clean_v1" / "cache" / "p01.full.h5"
OUT = ROOT / "artifacts" / "kd-teacher-trap-diagnostic"
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest().upper()
def decode(values: np.ndarray) -> list[str]:
return [value.decode("utf-8") if isinstance(value, bytes) else str(value) for value in values]
def softmax(values: np.ndarray) -> np.ndarray:
shifted = values.astype(np.float64) - values.max(axis=1, keepdims=True)
exp = np.exp(shifted)
return exp / exp.sum(axis=1, keepdims=True)
def expectation(values: np.ndarray) -> np.ndarray:
return softmax(values) @ np.arange(90, dtype=np.float64) * 2.0 - 90.0
def vectors(pitch_deg: np.ndarray, yaw_deg: np.ndarray) -> np.ndarray:
pitch, yaw = np.deg2rad(pitch_deg), np.deg2rad(yaw_deg)
return np.column_stack(
(-np.cos(pitch) * np.sin(yaw), -np.sin(pitch), -np.cos(pitch) * np.cos(yaw))
)
def angular_error(prediction: np.ndarray, target: np.ndarray) -> np.ndarray:
prediction /= np.linalg.norm(prediction, axis=1, keepdims=True)
target /= np.linalg.norm(target, axis=1, keepdims=True)
return np.rad2deg(np.arccos(np.clip(np.sum(prediction * target, axis=1), -1.0, 1.0)))
def main() -> None:
with h5py.File(LEGACY, "r") as legacy, h5py.File(CLEAN, "r") as clean:
legacy_count = int(legacy["left_gaze"].shape[0])
clean_count = int(clean["left_gaze"].shape[0])
left_equal = bool(np.array_equal(legacy["left_gaze"][:], clean["left_gaze"][:]))
right_equal = bool(np.array_equal(legacy["right_gaze"][:], clean["right_gaze"][:]))
landmark_delta = np.abs(
legacy["landmarks"][:].astype(np.float64) - clean["landmarks"][:].astype(np.float64)
)
row_landmark_max = landmark_delta.reshape(clean_count, -1).max(axis=1)
paths = decode(clean["relative_frame_path"][:])
ids = decode(clean["sample_id"][:])
image_hashes = decode(clean["raw_image_sha256"][:])
source_indices = clean["source_index"][:].astype(np.int64)
target_deg = np.rad2deg(clean["left_gaze"][:].astype(np.float64))
target_vector = vectors(target_deg[:, 0], target_deg[:, 1])
legacy_pitch = expectation(legacy["teacher_pitch_logits"][:])
legacy_yaw = expectation(legacy["teacher_yaw_logits"][:])
corrected_raw_pitch = expectation(clean["teacher_pitch_logits_raw"][:])
corrected_raw_yaw = expectation(clean["teacher_yaw_logits_raw"][:])
corrected_aligned_vector = clean["teacher_aligned_vector"][:].astype(np.float64)
legacy_error = angular_error(vectors(legacy_pitch, legacy_yaw), target_vector.copy())
corrected_raw_error = angular_error(
vectors(corrected_raw_pitch, corrected_raw_yaw), target_vector.copy()
)
corrected_aligned_error = angular_error(corrected_aligned_vector, target_vector.copy())
csv_path = OUT / "p01_legacy_row_to_frame_mapping.csv"
with csv_path.open("w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(
stream,
fieldnames=(
"legacy_h5_row", "source_index", "sample_id", "relative_frame_path",
"raw_image_sha256", "left_gaze_exact", "right_gaze_exact",
"landmark_row_max_abs_diff",
),
)
writer.writeheader()
for index in range(clean_count):
writer.writerow(
{
"legacy_h5_row": index,
"source_index": int(source_indices[index]),
"sample_id": ids[index],
"relative_frame_path": paths[index],
"raw_image_sha256": image_hashes[index],
"left_gaze_exact": bool(
np.array_equal(legacy["left_gaze"][index], clean["left_gaze"][index])
),
"right_gaze_exact": bool(
np.array_equal(legacy["right_gaze"][index], clean["right_gaze"][index])
),
"landmark_row_max_abs_diff": float(row_landmark_max[index]),
}
)
mapping_pass = bool(
legacy_count == clean_count
and left_equal
and right_equal
and np.max(row_landmark_max) <= 1e-6
and np.all(np.diff(source_indices) > 0)
and len(ids) == len(set(ids))
)
report = {
"legacy_h5": str(LEGACY),
"legacy_h5_sha256": file_sha256(LEGACY),
"clean_h5": str(CLEAN),
"clean_h5_sha256": file_sha256(CLEAN),
"legacy_rows": legacy_count,
"clean_rows": clean_count,
"left_gaze_all_rows_exact": left_equal,
"right_gaze_all_rows_exact": right_equal,
"landmarks_global_max_abs_diff": float(np.max(landmark_delta)),
"landmarks_global_mean_abs_diff": float(np.mean(landmark_delta)),
"landmark_rows_with_max_abs_diff_le_1e_6": int(np.sum(row_landmark_max <= 1e-6)),
"source_indices_strictly_increasing": bool(np.all(np.diff(source_indices) > 0)),
"unique_sample_ids": len(set(ids)),
"mapping_csv": str(csv_path),
"mapping_csv_sha256": file_sha256(csv_path),
"mapping_status": "RECOVERED_FOR_P01" if mapping_pass else "NOT_PROVEN",
"first_five": [
{
"legacy_h5_row": index,
"source_index": int(source_indices[index]),
"sample_id": ids[index],
"relative_frame_path": paths[index],
"raw_image_sha256": image_hashes[index],
}
for index in range(min(5, clean_count))
],
"teacher_comparison_against_roll_corrected_left_gaze": {
"legacy_cached_teacher_3d_error_mean_deg": float(legacy_error.mean()),
"corrected_loader_raw_teacher_3d_error_mean_deg": float(corrected_raw_error.mean()),
"corrected_loader_roll_aligned_teacher_3d_error_mean_deg": float(
corrected_aligned_error.mean()
),
"corrected_aligned_minus_legacy_error_deg": float(
corrected_aligned_error.mean() - legacy_error.mean()
),
"legacy_vs_corrected_pitch_prediction_pearson": float(
np.corrcoef(legacy_pitch, corrected_raw_pitch)[0, 1]
),
"legacy_vs_corrected_yaw_prediction_pearson": float(
np.corrcoef(legacy_yaw, corrected_raw_yaw)[0, 1]
),
},
"interpretation": (
"The identical accepted-row count, exact left/right gaze arrays, and <=1e-6 "
"landmark agreement establish the p01 legacy row order. Patch differences do "
"not invalidate identity because patch-processing variants were already known."
),
}
output = OUT / "p01_legacy_row_mapping_audit.json"
output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8", newline="\n")
print(json.dumps(report, indent=2))
if not mapping_pass:
raise SystemExit(1)
if __name__ == "__main__":
main()