auto-iter 20260513-2231: GX019817 RoPE skip, 4 PLY done ready for stage06

This commit is contained in:
Poulpe
2026-05-13 23:02:31 +00:00
committed by Ubuntu
parent 091ffeb2f6
commit 38dbcfd46f
14 changed files with 2083 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Loop closure detection via perceptual hashing.
For each frame, compute pHash (DCT-based perceptual hash).
Find pairs (i, j) with |i-j| > MIN_SEPARATION and hash distance < THRESHOLD.
These are loop closures — AUV revisited same physical location.
Then correct DVL trajectory by snapping back at loop closures.
Usage:
python3 loop_closure_phash.py --frames-dir <dir> --dvl-csv <csv> \
--out-corrected /tmp/dvl_loopclosed.csv --plot /tmp/loop_closure.png \
--min-sep 60 --max-dist 8
"""
import argparse, csv, math
from pathlib import Path
import numpy as np
from PIL import Image
import imagehash
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--frames-dir', required=True)
ap.add_argument('--dvl-csv', required=True)
ap.add_argument('--out-corrected', required=True)
ap.add_argument('--plot', default=None)
ap.add_argument('--min-sep', type=int, default=60, help='min frame separation to count as loop')
ap.add_argument('--max-dist', type=int, default=10, help='max pHash Hamming distance for match')
ap.add_argument('--hash-size', type=int, default=8)
args = ap.parse_args()
frames = sorted(Path(args.frames_dir).glob('frame_*.jpg'))
print(f'[loop] hashing {len(frames)} frames (pHash size {args.hash_size})...', flush=True)
hashes = []
for i, f in enumerate(frames):
img = Image.open(f)
h = imagehash.phash(img, hash_size=args.hash_size)
hashes.append(h)
if i % 200 == 0: print(f' hashed {i}/{len(frames)}', flush=True)
print(f'[loop] searching loop closures (min_sep={args.min_sep}, max_dist={args.max_dist})...', flush=True)
loops = [] # list of (i, j, distance)
for i in range(len(hashes)):
for j in range(i + args.min_sep, len(hashes)):
d = hashes[i] - hashes[j]
if d <= args.max_dist:
loops.append((i, j, d))
if i % 200 == 0: print(f' search at {i}, loops found so far: {len(loops)}', flush=True)
print(f'[loop] found {len(loops)} loop closures', flush=True)
# Load DVL trajectory
dvl_rows = list(csv.DictReader(open(args.dvl_csv)))
e = np.array([float(r['east_m']) for r in dvl_rows])
n = np.array([float(r['north_m']) for r in dvl_rows])
# Simple correction: for each loop closure (i, j), interpolate a rigid correction
# over [i, j] to bring j back to i's position
# We'll apply gradual correction: for k in [i, j], offset by linear ramp
e_corr = e.copy(); n_corr = n.copy()
n_corrections = 0
for i, j, d in loops:
if j >= len(e_corr): continue
dx = e_corr[i] - e_corr[j]
dy = n_corr[i] - n_corr[j]
# spread correction linearly over [i+1, j]
nsteps = j - i
for k in range(i+1, j+1):
ratio = (k - i) / nsteps
e_corr[k] += dx * ratio
n_corr[k] += dy * ratio
# carry forward the offset to all frames after j
for k in range(j+1, len(e_corr)):
e_corr[k] += dx
n_corr[k] += dy
n_corrections += 1
print(f'[loop] applied {n_corrections} corrections to trajectory', flush=True)
with open(args.out_corrected, 'w', newline='') as ff:
w = csv.writer(ff)
w.writerow(['frame_idx','ts_s','east_m_orig','north_m_orig','east_m_corr','north_m_corr'])
for k, r in enumerate(dvl_rows):
w.writerow([r['frame_idx'], r['ts_s'], e[k], n[k], e_corr[k], n_corr[k]])
print(f'[out] {args.out_corrected}', flush=True)
if args.plot:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
ax_orig, ax_corr, ax_pairs, ax_dist = axes[0,0], axes[0,1], axes[1,0], axes[1,1]
ax_orig.plot(e, n, '-b', linewidth=1.0); ax_orig.plot(e[0], n[0], 'go', markersize=10); ax_orig.plot(e[-1], n[-1], 'r^', markersize=10)
ax_orig.set_title(f'DVL trajectory ORIGINAL (drift visible)\nbbox={max(e)-min(e):.1f}×{max(n)-min(n):.1f}m')
ax_orig.set_xlabel('East (m)'); ax_orig.set_ylabel('North (m)'); ax_orig.set_aspect('equal'); ax_orig.grid(True, alpha=0.3)
ax_corr.plot(e_corr, n_corr, '-r', linewidth=1.0); ax_corr.plot(e_corr[0], n_corr[0], 'go', markersize=10); ax_corr.plot(e_corr[-1], n_corr[-1], 'r^', markersize=10)
ax_corr.set_title(f'DVL trajectory + LOOP CLOSURE\nbbox={max(e_corr)-min(e_corr):.1f}×{max(n_corr)-min(n_corr):.1f}m\nLoops applied: {n_corrections}')
ax_corr.set_xlabel('East (m)'); ax_corr.set_ylabel('North (m)'); ax_corr.set_aspect('equal'); ax_corr.grid(True, alpha=0.3)
# plot loop pairs as lines on original
ax_pairs.plot(e, n, '-', color='gray', linewidth=0.5, alpha=0.4)
for i, j, d in loops[:200]: # show first 200 pairs
ax_pairs.plot([e[i], e[j]], [n[i], n[j]], '-', color='orange', linewidth=0.4, alpha=0.3)
ax_pairs.set_title(f'Loop closure pairs (first 200, of {len(loops)})')
ax_pairs.set_xlabel('East'); ax_pairs.set_ylabel('North'); ax_pairs.set_aspect('equal'); ax_pairs.grid(True, alpha=0.3)
# histogram of loop distances
dists = [d for _,_,d in loops]
if dists:
ax_dist.hist(dists, bins=range(0, max(dists)+2))
ax_dist.set_xlabel('Hash Hamming distance'); ax_dist.set_ylabel('Count'); ax_dist.set_title('Loop closure hash distance distribution'); ax_dist.grid(True, alpha=0.3)
plt.suptitle(f'Loop closure detection (pHash {args.hash_size}, min_sep={args.min_sep}, max_dist={args.max_dist}) — GX039839')
plt.tight_layout()
plt.savefig(args.plot, dpi=130, bbox_inches='tight')
print(f'[plot] {args.plot}', flush=True)
if __name__ == '__main__': main()