Initial commit: Speckle-Scanner 3D pipeline with setup README
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Pipeline runner for libSGM stereo disparity.
|
||||
|
||||
Resolves all paths from the project folder structure and drives
|
||||
the stereosgm_new binary for each scan in a session (or all sessions on a date).
|
||||
|
||||
For each scan it takes the LAST matched lc_/rc_ image pair from 02_rect_images/
|
||||
(images sorted by timestamp — highest timestamp = last acquired image).
|
||||
|
||||
Output layout per scan:
|
||||
<processing_dir>/<project>/<date>/<session>/<scan>/
|
||||
02_rect_images/ <- input (lc_ts<last>.png + rc_ts<same>.png)
|
||||
03_sgm_disp_map/ <- disparity.xml + disparity_color.png (created here)
|
||||
05_sgm_pcl/ <- untouched
|
||||
|
||||
Binary:
|
||||
~/Speckle-Scanner/05_disparity/libsgm/build/sample/stereosgm_new
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# Resolve config.py from ~/Speckle-Scanner regardless of CWD
|
||||
sys.path.insert(0, str(Path.home() / "Speckle-Scanner"))
|
||||
import config # noqa: E402
|
||||
|
||||
BINARY = Path(__file__).parent / "build" / "sample" / "stereosgm_new"
|
||||
|
||||
|
||||
def extract_ts_token(filename, prefix="lc_"):
|
||||
"""Extract ts token from lc_ts1634840093.png or lc_ts1634840093_ck....png."""
|
||||
m = re.search(rf"^{re.escape(prefix)}(ts\d+)", filename, re.IGNORECASE)
|
||||
if not m:
|
||||
return None, None
|
||||
ts_token = m.group(1).lower()
|
||||
ts_int = int(re.search(r"\d+", ts_token).group())
|
||||
return ts_token, ts_int
|
||||
|
||||
|
||||
def find_rc_for_ts(rect_dir, ts_token):
|
||||
"""Match rc image by shared ts token (ck suffix optional)."""
|
||||
rc_matches = sorted(rect_dir.glob(f"rc_{ts_token}_*.png"))
|
||||
if not rc_matches:
|
||||
rc_matches = sorted(rect_dir.glob(f"rc_{ts_token}*.png"))
|
||||
return rc_matches[0] if rc_matches else None
|
||||
|
||||
|
||||
def find_last_lc_rc_pair(rect_dir):
|
||||
"""Return (lc_path, rc_path) for the highest-timestamp matched pair in rect_dir."""
|
||||
rect_dir = Path(rect_dir)
|
||||
pairs = []
|
||||
for lc in rect_dir.glob("lc_ts*.png"):
|
||||
ts_token, ts_int = extract_ts_token(lc.name, "lc_")
|
||||
if ts_token is None:
|
||||
continue
|
||||
rc = find_rc_for_ts(rect_dir, ts_token)
|
||||
if rc is None:
|
||||
continue
|
||||
pairs.append((ts_int, lc, rc))
|
||||
|
||||
if not pairs:
|
||||
return None, None
|
||||
|
||||
pairs.sort(key=lambda item: item[0])
|
||||
_, lc, rc = pairs[-1]
|
||||
return lc, rc
|
||||
|
||||
|
||||
def build_cmd(lc, rc, output_dir, sgm_args):
|
||||
cmd = [
|
||||
str(BINARY),
|
||||
str(lc),
|
||||
str(rc),
|
||||
f"--output_dir={output_dir}",
|
||||
"--no_display=1",
|
||||
]
|
||||
for key, val in sgm_args.items():
|
||||
if val is not None:
|
||||
cmd.append(f"--{key}={val}")
|
||||
return cmd
|
||||
|
||||
|
||||
def run_scan(project, date, session, scan, sgm_args):
|
||||
rect_dir = config.PROCESSING_DIR / project / date / session / scan / "02_rect_images"
|
||||
if not rect_dir.exists():
|
||||
print(f"[SKIP] {session}/{scan}: 02_rect_images not found at {rect_dir}")
|
||||
return False
|
||||
|
||||
lc, rc = find_last_lc_rc_pair(rect_dir)
|
||||
if lc is None:
|
||||
print(f"[SKIP] {session}/{scan}: no lc_ts*.png images found in {rect_dir}")
|
||||
return False
|
||||
if rc is None:
|
||||
print(f"[SKIP] {session}/{scan}: no matching rc image for {lc.name}")
|
||||
return False
|
||||
|
||||
output_dir = config.get_processing_step_dir(project, date, session, scan, "03_sgm_disp_map")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[SCAN] {session}/{scan}")
|
||||
print(f" lc : {lc.name}")
|
||||
print(f" rc : {rc.name}")
|
||||
print(f" output : {output_dir}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
cmd = build_cmd(lc, rc, output_dir, sgm_args)
|
||||
result = subprocess.run(cmd)
|
||||
if result.returncode != 0:
|
||||
print(f"[FAIL] {session}/{scan} exited with code {result.returncode}")
|
||||
return False
|
||||
print(f"[DONE] {session}/{scan}")
|
||||
return True
|
||||
|
||||
|
||||
def run_session(project, date, session, scan_arg, sgm_args):
|
||||
if scan_arg:
|
||||
scans = [scan_arg]
|
||||
else:
|
||||
scans = config.list_scan_dirs(project, date, session)
|
||||
if not scans:
|
||||
print(f"[WARN] No scan folders found in {project}/{date}/{session}")
|
||||
return [], []
|
||||
print(f"\n Session {session}: {len(scans)} scan(s) found")
|
||||
|
||||
failed = []
|
||||
for scan in scans:
|
||||
ok = run_scan(project, date, session, scan, sgm_args)
|
||||
if not ok:
|
||||
failed.append(f"{session}/{scan}")
|
||||
return scans, failed
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="libSGM disparity pipeline runner — resolves paths from project structure"
|
||||
)
|
||||
# Project location
|
||||
parser.add_argument("--project", required=True, help="Project name (e.g. Olsen_wings)")
|
||||
parser.add_argument("--date", required=True, help="Date string (e.g. 2026-05-12)")
|
||||
parser.add_argument("--session", default=None, help="Session name (e.g. session1); omit to process ALL sessions on that date")
|
||||
parser.add_argument("--scan", default=None, help="Single scan (e.g. Scan000001); omit to process all scans in the session")
|
||||
|
||||
# SGM parameters — all optional, forwarded to stereosgm_new
|
||||
parser.add_argument("--disp_size", type=int, default=None, help="Maximum disparity value (64, 128, or 256; default 256)")
|
||||
parser.add_argument("--P1", type=int, default=None, help="SGM penalty for disparity change of ±1 (default 10)")
|
||||
parser.add_argument("--P2", type=int, default=None, help="SGM penalty for disparity change >1 (default 120)")
|
||||
parser.add_argument("--uniqueness", type=float, default=None, help="Uniqueness ratio threshold (default 0.80)")
|
||||
parser.add_argument("--num_paths", type=int, default=None, choices=[4, 8], help="Scanlines for cost aggregation: 4 or 8 (default 8)")
|
||||
parser.add_argument("--min_disp", type=int, default=None, help="Minimum disparity value (default -160)")
|
||||
parser.add_argument("--LR_max_diff", type=int, default=None, help="Max left-right disparity difference (default 1)")
|
||||
parser.add_argument("--census_type", type=int, default=None, choices=[0, 1], help="Census transform type: 0=CENSUS_9x7, 1=SYMMETRIC_CENSUS_9x7 (default 1)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not BINARY.exists():
|
||||
print(f"ERROR: stereosgm_new binary not found at {BINARY}")
|
||||
print("Build it first: cd ~/Speckle-Scanner/05_disparity/libsgm/build && make stereosgm_new")
|
||||
sys.exit(1)
|
||||
|
||||
sgm_args = {
|
||||
"disp_size": args.disp_size,
|
||||
"P1": args.P1,
|
||||
"P2": args.P2,
|
||||
"uniqueness": args.uniqueness,
|
||||
"num_paths": args.num_paths,
|
||||
"min_disp": args.min_disp,
|
||||
"LR_max_diff": args.LR_max_diff,
|
||||
"census_type": args.census_type,
|
||||
}
|
||||
|
||||
# Determine sessions to process
|
||||
if args.session:
|
||||
sessions = [args.session]
|
||||
else:
|
||||
sessions = config.list_session_dirs(args.project, args.date)
|
||||
if not sessions:
|
||||
print(f"No session folders found under {args.project}/{args.date}")
|
||||
sys.exit(1)
|
||||
print(f"Found {len(sessions)} session(s): {sessions}")
|
||||
|
||||
total_scans = 0
|
||||
all_failed = []
|
||||
for session in sessions:
|
||||
scans, failed = run_session(
|
||||
args.project, args.date, session, args.scan, sgm_args
|
||||
)
|
||||
total_scans += len(scans)
|
||||
all_failed.extend(failed)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Finished: {total_scans - len(all_failed)}/{total_scans} scans succeeded.")
|
||||
if all_failed:
|
||||
print(f"Failed: {all_failed}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user