Commit 2b1e81b3 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: fopen_turn_finder.py — add AGL from elevations_histogram.csv

*-elevations_histogram.csv (row 2, col 1) gives flight altitude above
ground level for each sequence.  Low AGL (<25-50 m) correlates with
algorithm failures in dense-canopy areas.

Changes:
- find_xml_files: also yields hist_path (*-elevations_histogram.csv)
- parse_agl(): reads AGL from second row, first column (tab-separated)
- agl_m added to sequence metrics dict and TSV output
- --min_agl filter: hide sequences below given AGL from console output
- AGL column shown in console tables
Co-authored-by: 's avatarClaude <claude@elphel.com>
parent 1d5fdb15
......@@ -197,9 +197,12 @@ def sequence_metrics(pose_arr, dt_arr):
def find_xml_files(root):
"""
Yield (scene_ts_str, xml_path) for every scene directory under root.
Picks the latest version subdirectory by name (e.g. v88 > v80).
Yield (scene_ts_str, xml_path, hist_path_or_None) for every scene dir
under root. Picks the latest version subdirectory by name (e.g. v88 > v80).
Skips directories whose name ends with '-index'.
hist_path is the *-elevations_histogram.csv inside the version subdir,
or None if not present.
"""
try:
scene_dirs = sorted(os.listdir(root))
......@@ -225,10 +228,32 @@ def find_xml_files(root):
continue
ver = sorted(subdirs)[-1]
ver_path = os.path.join(scene_path, ver)
xml_path = os.path.join(ver_path, f"{scene}-INTERFRAME.corr-xml")
ver_path = os.path.join(scene_path, ver)
xml_path = os.path.join(ver_path, f"{scene}-INTERFRAME.corr-xml")
hist_path = os.path.join(ver_path, f"{scene}-elevations_histogram.csv")
if os.path.isfile(xml_path):
yield scene, xml_path
yield scene, xml_path, (hist_path if os.path.isfile(hist_path) else None)
def parse_agl(hist_path):
"""
Read AGL (flight altitude above ground level, metres) from
*-elevations_histogram.csv.
Format:
Row 1 (header): AGL ELEVATION FRACTION_LOWER
Row 2: <agl_value> <elev> <frac> ← AGL is here
Row 3+: (empty first col, cumulative histogram continues)
Returns float AGL, or nan if the file cannot be parsed.
"""
try:
with open(hist_path) as f:
next(f) # skip header
first_data = next(f).split('\t')
return float(first_data[0])
except Exception:
return float('nan')
# ─────────────────────────────── main ────────────────────────────────────── #
......@@ -248,17 +273,21 @@ def main():
ap.add_argument('--max_glitch', type=float, default=None,
help='Hide sequences whose R glitch ratio exceeds this '
'(e.g. 5.0 keeps only clean data; default: show all)')
ap.add_argument('--min_agl', type=float, default=None,
help='Hide sequences with AGL below this value in metres '
'(e.g. 50 skips low-altitude failure-prone sequences)')
args = ap.parse_args()
print(f"Scanning {args.root} …", flush=True)
rows = []
for scene_ts, xml_path in find_xml_files(args.root):
for scene_ts, xml_path, hist_path in find_xml_files(args.root):
pose_arr, dt_arr = parse_xml(xml_path)
if pose_arr is None:
continue
m = sequence_metrics(pose_arr, dt_arr)
m['scene_ts'] = scene_ts
m['xml_path'] = xml_path
m['agl_m'] = parse_agl(hist_path) if hist_path else float('nan')
rows.append(m)
print(f"Parsed {len(rows)} sequences.", flush=True)
......@@ -275,18 +304,24 @@ def main():
}
sort_key = sort_map[args.sort]
# apply glitch filter
# apply filters
display = rows
if args.max_glitch is not None:
display = [r for r in rows
display = [r for r in display
if (r.get('r_glitch') or 0) <= args.max_glitch]
print(f"After glitch filter (r_glitch ≤ {args.max_glitch}): "
f"{len(display)} sequences remain.")
if args.min_agl is not None:
display = [r for r in display
if np.isfinite(r.get('agl_m', float('nan')))
and r['agl_m'] >= args.min_agl]
print(f"After AGL filter (≥ {args.min_agl} m): "
f"{len(display)} sequences remain.")
display.sort(key=lambda r: r.get(sort_key) or 0, reverse=True)
# ── console report ────────────────────────────────────────────────────
hdr = (f" {'Scene':>22} {'fr':>4} {'range(°)':>8} "
hdr = (f" {'Scene':>22} {'fr':>4} {'AGL(m)':>7} {'range(°)':>8} "
f"{'dt_max(°/s)':>11} {'at timestamp':>20} "
f"{'glitch':>7} {'fd_max(°/s)':>11}")
......@@ -302,7 +337,9 @@ def main():
print(f"{'═'*90}")
print(hdr)
for r in ranked[:args.top]:
print(f" {r['scene_ts']:>22} {r['n_frames']:>4} "
agl = r.get('agl_m', float('nan'))
agl_s = f"{agl:7.1f}" if np.isfinite(agl) else " n/a"
print(f" {r['scene_ts']:>22} {r['n_frames']:>4} {agl_s} "
f"{r[f'{axis}_range(deg)']:>8.2f} "
f"{r[f'{axis}_dt_max(deg/s)']:>11.3f} "
f"{r[f'{axis}_dt_ts']:>20.3f} "
......@@ -312,7 +349,7 @@ def main():
# ── TSV output ────────────────────────────────────────────────────────
if args.out:
fieldnames = [
'scene_ts', 'n_frames', 'duration_s',
'scene_ts', 'n_frames', 'duration_s', 'agl_m',
'r_dt_max(deg/s)', 'r_dt_ts', 'r_range(deg)', 'r_glitch', 'r_fd_max(deg/s)',
't_dt_max(deg/s)', 't_dt_ts', 't_range(deg)', 't_glitch', 't_fd_max(deg/s)',
'a_dt_max(deg/s)', 'a_dt_ts', 'a_range(deg)', 'a_glitch', 'a_fd_max(deg/s)',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment