initial commit
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
"""
|
||||
Filename: part_A.py
|
||||
Description: First part of processing
|
||||
|
||||
Author: Tyler de Zeeuw
|
||||
License: GPL-3.0
|
||||
"""
|
||||
|
||||
# Built-in imports
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import argparse
|
||||
import traceback
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
# External library imports
|
||||
import gphoto2 as gp #type: ignore
|
||||
|
||||
|
||||
# NOTE: Assumes calibration files have been made
|
||||
PWD = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
MSYS2_BIN = str(PWD) + r"\res_gphoto2\bin"
|
||||
MSYS2_CAMLIBS = str(PWD) + r"\res_gphoto2\lib\libgphoto2\2.5.34"
|
||||
MSYS2_IOLIBS = str(PWD) + r"\res_gphoto2\lib\libgphoto2_port\0.12.2"
|
||||
|
||||
os.environ["IOLIBS"] = MSYS2_IOLIBS
|
||||
os.environ["PATH"] = MSYS2_BIN + os.pathsep + os.environ.get("PATH", "")
|
||||
os.environ["CAMLIBS"] = MSYS2_CAMLIBS
|
||||
|
||||
# ---------- Lumix Configuration ----------
|
||||
OUTPUT_PREFIX_CAM1 = "Left_Camera"
|
||||
OUTPUT_PREFIX_CAM2 = "Right_Camera"
|
||||
TARGET_FRAMES = 5
|
||||
TRIGGER_RETRY_TIMEOUT = 2.0
|
||||
TRIGGER_RETRY_INTERVAL = 0.02 # 20ms between retries
|
||||
|
||||
# ---------- Calibrated S24+ Configuration ----------
|
||||
SHUTTER_X = 720
|
||||
SHUTTER_Y = 2600
|
||||
DROP_DISTANCE = 250
|
||||
HOLD_DURATION_S = 0.55
|
||||
ADB_PATH = "C:/Android/platform-tools/adb"
|
||||
|
||||
MASTER_START_TIME = time.perf_counter()
|
||||
|
||||
|
||||
# def get_precise_timestamp(img_path):
|
||||
# if not os.path.exists(img_path):
|
||||
# return "File not found"
|
||||
# try:
|
||||
# with open(img_path, 'rb') as image_file:
|
||||
# my_image = Image(image_file)
|
||||
# if my_image.has_exif:
|
||||
# base_time = my_image.get("datetime_original")
|
||||
# sub_seconds = my_image.get("subsec_time_original", "000")
|
||||
# if base_time:
|
||||
# return f"{base_time}.{sub_seconds}"
|
||||
# return "No EXIF data found"
|
||||
# except Exception as e:
|
||||
# return f"Error reading EXIF: {e}"
|
||||
|
||||
def run_adb_shell(command_string):
|
||||
try:
|
||||
result = subprocess.run([ADB_PATH, "shell", command_string], capture_output=True, text=True, check=True)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"ADB Shell Error: {e.stderr.strip()}")
|
||||
return None
|
||||
|
||||
def trigger_humanized_chained_burst():
|
||||
mid_x = SHUTTER_Y + int(DROP_DISTANCE / 2)
|
||||
end_x = SHUTTER_Y + DROP_DISTANCE
|
||||
print(f"[Phone] Targeting landscape shutter center at: ({SHUTTER_Y}, {SHUTTER_X})")
|
||||
print("[Phone] Executing humanized landscape slide-and-hold sequence...")
|
||||
|
||||
chained_cmd = (
|
||||
f"input motionevent DOWN {SHUTTER_Y} {SHUTTER_X} ; "
|
||||
f"sleep 0.05 ; "
|
||||
f"input motionevent MOVE {mid_x} {SHUTTER_X} ; "
|
||||
f"input motionevent MOVE {end_x} {SHUTTER_X} ; "
|
||||
f"sleep {HOLD_DURATION_S} ; "
|
||||
f"input motionevent UP {end_x} {SHUTTER_X}"
|
||||
)
|
||||
run_adb_shell(chained_cmd)
|
||||
print("[Phone] Landscape burst gesture completed.")
|
||||
|
||||
def pull_recent_burst_photos(local_dest_dir, max_photos_to_check=10):
|
||||
print("\n[Transfer System] Scanning phone for new burst frames...")
|
||||
os.makedirs(local_dest_dir, exist_ok=True)
|
||||
|
||||
camera_dir = "/storage/emulated/0/DCIM/Camera"
|
||||
list_cmd = f"ls -t {camera_dir}"
|
||||
file_list_raw = run_adb_shell(list_cmd)
|
||||
|
||||
if not file_list_raw:
|
||||
print("No images found on the mobile device.")
|
||||
return []
|
||||
|
||||
lines = file_list_raw.split('\n')
|
||||
recent_files = [l.strip() for l in lines if l.strip().endswith(('.jpg', '.jpeg', '.png'))]
|
||||
|
||||
if not recent_files:
|
||||
print("No image files located on phone.")
|
||||
return []
|
||||
|
||||
files_to_pull = []
|
||||
current_time = time.time()
|
||||
|
||||
print("Filtering phone files captured in the last burst window...")
|
||||
for filename in recent_files[:max_photos_to_check]:
|
||||
remote_path = f"{camera_dir}/{filename}"
|
||||
stat_out = run_adb_shell(f"stat -c %Y {remote_path}")
|
||||
if stat_out and stat_out.isdigit():
|
||||
file_time = int(stat_out)
|
||||
if (current_time - file_time) < 30:
|
||||
files_to_pull.append((remote_path, filename))
|
||||
|
||||
if not files_to_pull:
|
||||
print("No fresh burst frames found on phone from this exact run.")
|
||||
return []
|
||||
|
||||
print(f"Found {len(files_to_pull)} new phone burst frames. Pulling files...")
|
||||
for remote_path, filename in files_to_pull:
|
||||
local_path = os.path.join(local_dest_dir, filename)
|
||||
try:
|
||||
subprocess.run([ADB_PATH, "pull", remote_path, local_path], capture_output=True, check=True)
|
||||
print(f" Successfully transferred from phone: {filename}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" Failed to pull {filename}: {e.stderr.decode().strip()}")
|
||||
|
||||
return files_to_pull
|
||||
|
||||
def delete_phone_photos(files_list):
|
||||
if not files_list:
|
||||
return
|
||||
print(f"\n[Phone Cleanup] Clean Sweep: Deleting {len(files_list)} target frames from phone storage...")
|
||||
for remote_path, _ in files_list:
|
||||
run_adb_shell(f"rm {remote_path}")
|
||||
print("[Phone Cleanup] Phone storage cleanup complete.")
|
||||
|
||||
# ---------- Lumix Subsystem ----------
|
||||
def init_camera_by_path(port_path):
|
||||
camera = gp.Camera()
|
||||
context = gp.Context()
|
||||
port_info_list = gp.PortInfoList()
|
||||
port_info_list.load()
|
||||
try:
|
||||
idx = port_info_list.lookup_path(port_path)
|
||||
camera.set_port_info(port_info_list[idx])
|
||||
camera.init(context)
|
||||
return camera, context
|
||||
except gp.GPhoto2Error as e:
|
||||
print(f"Failed to initialize camera at {port_path}: {e}")
|
||||
return None, None
|
||||
|
||||
def trigger_capture_with_retry(camera, context, timeout, interval, prefix, frame_idx):
|
||||
deadline = time.time() + timeout
|
||||
attempt = 0
|
||||
while time.time() < deadline:
|
||||
attempt += 1
|
||||
try:
|
||||
camera.trigger_capture(context)
|
||||
return True
|
||||
except gp.GPhoto2Error:
|
||||
time.sleep(interval)
|
||||
continue
|
||||
print(f"[{prefix}] Frame {frame_idx}: trigger_capture failed after {attempt} attempts (timeout).")
|
||||
return False
|
||||
|
||||
def derive_preceding_filenames(folder, last_name, count):
|
||||
match = re.match(r'^([A-Za-z]+)(\d+)(\.[A-Za-z0-9]+)$', last_name)
|
||||
if not match:
|
||||
return None
|
||||
prefix, digits, ext = match.groups()
|
||||
width = len(digits)
|
||||
last_num = int(digits)
|
||||
names = []
|
||||
for i in range(count):
|
||||
num = last_num - i
|
||||
if num < 0:
|
||||
break
|
||||
names.append((folder, f"{prefix}{num:0{width}d}{ext}"))
|
||||
names.reverse()
|
||||
return names
|
||||
|
||||
def capture_burst_sequence(camera, context, prefix, count):
|
||||
accepted = 0
|
||||
for i in range(count):
|
||||
ok = trigger_capture_with_retry(
|
||||
camera, context,
|
||||
TRIGGER_RETRY_TIMEOUT, TRIGGER_RETRY_INTERVAL,
|
||||
prefix, i + 1
|
||||
)
|
||||
if ok:
|
||||
accepted += 1
|
||||
return accepted
|
||||
|
||||
def gather_file_events(camera, context, prefix, expected_count):
|
||||
file_events = []
|
||||
start_time = time.time()
|
||||
timeout_seconds = 4.0
|
||||
while time.time() - start_time < timeout_seconds:
|
||||
event_type, event_data = camera.wait_for_event(10, context)
|
||||
if event_type == gp.GP_EVENT_FILE_ADDED:
|
||||
file_events.append((event_data.folder, event_data.name))
|
||||
if len(file_events) >= expected_count:
|
||||
break
|
||||
elif event_type == gp.GP_EVENT_TIMEOUT:
|
||||
continue
|
||||
return file_events
|
||||
|
||||
def gather_and_wipe_buffer(camera, context, prefix):
|
||||
deleted_count = 0
|
||||
timeout_seconds = 5.0
|
||||
start_time = time.time()
|
||||
print(f"[{prefix}] Flushing file queue...")
|
||||
while time.time() - start_time < timeout_seconds:
|
||||
event_type, event_data = camera.wait_for_event(500, context)
|
||||
if event_type == gp.GP_EVENT_FILE_ADDED:
|
||||
folder = event_data.folder
|
||||
name = event_data.name
|
||||
try:
|
||||
camera.file_delete(folder, name)
|
||||
print(f"[{prefix}] Deleted junk frame: {folder}/{name}")
|
||||
deleted_count += 1
|
||||
except gp.GPhoto2Error:
|
||||
print(f"[{prefix}] Failed to delete: {folder}/{name} (System busy)")
|
||||
start_time = time.time()
|
||||
elif event_type == gp.GP_EVENT_TIMEOUT:
|
||||
continue
|
||||
return deleted_count
|
||||
|
||||
def threaded_fast_burst(camera, context, save_dir, prefix, timing_shared, barrier, abort_event, save_barrier, delete_barrier):
|
||||
print(f"[{prefix}] Starting initial burst of {TARGET_FRAMES} frames...")
|
||||
burst_start = time.time()
|
||||
accepted_first = capture_burst_sequence(camera, context, prefix, TARGET_FRAMES)
|
||||
burst_end_time = time.time()
|
||||
|
||||
timing_shared[prefix] = burst_end_time
|
||||
burst_elapsed = burst_end_time - burst_start
|
||||
fps = accepted_first / burst_elapsed if burst_elapsed > 0 else 0
|
||||
print(f"[{prefix}] {accepted_first}/{TARGET_FRAMES} triggers accepted in {burst_elapsed:.3f}s (~{fps:.2f} fps).")
|
||||
|
||||
barrier.wait()
|
||||
sync_failed = abort_event.is_set()
|
||||
files_to_delete = []
|
||||
|
||||
if sync_failed:
|
||||
print(f"[{prefix}] !! Out of Sync !! Discarding batch 1 events...")
|
||||
save_barrier.wait()
|
||||
delete_barrier.wait() # Keeps timeline aligned even on drop branches
|
||||
time.sleep(1.5)
|
||||
deleted_count = gather_and_wipe_buffer(camera, context, prefix)
|
||||
print(f"[{prefix}] Aborted successfully. Cleaned up {deleted_count} files.")
|
||||
return
|
||||
else:
|
||||
file_events = gather_file_events(camera, context, prefix, accepted_first)
|
||||
if not file_events:
|
||||
print(f"Error: [{prefix}] did not report any new file creations.")
|
||||
save_barrier.wait()
|
||||
delete_barrier.wait()
|
||||
return
|
||||
last_folder, last_name = file_events[-1]
|
||||
targets_to_download = derive_preceding_filenames(last_folder, last_name, accepted_first)
|
||||
files_to_delete = targets_to_download
|
||||
|
||||
if not targets_to_download:
|
||||
print(f"[{prefix}] Falling back to downloading only the anchor frame.")
|
||||
last_folder, last_name = file_events[-1]
|
||||
targets_to_download = [(last_folder, last_name)]
|
||||
|
||||
print(f"[{prefix}] Downloading {len(targets_to_download)} frame(s)...")
|
||||
for idx, (folder, name) in enumerate(targets_to_download):
|
||||
target = os.path.join(save_dir, f"{prefix}_{idx + 1}.jpg")
|
||||
try:
|
||||
cf = camera.file_get(folder, name, gp.GP_FILE_TYPE_NORMAL)
|
||||
cf.save(target)
|
||||
print(f"-> Saved: {target} (source: {folder}/{name})")
|
||||
except gp.GPhoto2Error as e:
|
||||
print(f"Failed downloading {folder}/{name} for {prefix}: {e}")
|
||||
|
||||
print(f"[{prefix}] Done saving. Signaling coordinator...")
|
||||
save_barrier.wait()
|
||||
|
||||
delete_barrier.wait()
|
||||
|
||||
if files_to_delete:
|
||||
print(f"[{prefix}] Clean Sweep: Deleting {len(files_to_delete)} files from card...")
|
||||
for folder, name in files_to_delete:
|
||||
try:
|
||||
camera.file_delete(folder, name)
|
||||
except gp.GPhoto2Error:
|
||||
continue
|
||||
print(f"[{prefix}] Storage cleanup complete.")
|
||||
print_elapsed()
|
||||
|
||||
# ---------- Pipeline Worker ----------
|
||||
# def standalone_pipeline_worker(directory):
|
||||
# print(f"\n[Separate Process] Worker process started with PID: {os.getpid()}")
|
||||
# print(f"{'Frame':<10} | {'Left Camera Time':<30} | {'Right Camera Time':<30}")
|
||||
# print("-" * 78)
|
||||
|
||||
# for i in range(1, 6):
|
||||
# left_img = os.path.join(directory, f"Left_Camera_{i}.jpg")
|
||||
# right_img = os.path.join(directory, f"Right_Camera_{i}.jpg")
|
||||
|
||||
# left_time = get_precise_timestamp(left_img)
|
||||
# right_time = get_precise_timestamp(right_img)
|
||||
|
||||
# print(f"Shot {i:<5} | {left_time:<30} | {right_time:<30}")
|
||||
|
||||
# print("\n[Separate Process] EXIF analysis complete. Process exiting gracefully.\n")
|
||||
|
||||
def print_elapsed(label="Timestamp"):
|
||||
"""Helper function to print time elapsed since the script started."""
|
||||
elapsed = time.perf_counter() - MASTER_START_TIME
|
||||
print(f"[{label}] {elapsed:.3f}s total elapsed time")
|
||||
|
||||
|
||||
|
||||
def gui_entry(initial_snirf=None, bypass_2d=False):
|
||||
# 1. Android Initialization
|
||||
if initial_snirf is not None:
|
||||
print(initial_snirf)
|
||||
|
||||
if not bypass_2d:
|
||||
print("Waking up phone camera via ADB...")
|
||||
run_adb_shell("am start -a android.media.action.STILL_IMAGE_CAMERA")
|
||||
time.sleep(1.5)
|
||||
|
||||
# 2. Lumix Hardware Initialization
|
||||
print("Searching for connected Lumix cameras...")
|
||||
all_detected = gp.Camera.autodetect()
|
||||
lumix_detected = [
|
||||
(name, value) for name, value in all_detected.items()
|
||||
if "panasonic" in name.lower()
|
||||
]
|
||||
|
||||
print(f"Found {len(all_detected)} total USB devices.")
|
||||
print(f"Lumix devices found: {lumix_detected}")
|
||||
|
||||
if len(lumix_detected) < 2:
|
||||
print("Error: Need at least 2 Lumix cameras connected.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\nConnecting to Lumix cameras...")
|
||||
camA, contextA = init_camera_by_path(lumix_detected[0][1])
|
||||
camB, contextB = init_camera_by_path(lumix_detected[1][1])
|
||||
if not camA or not camB:
|
||||
print("Error: Lumix initialization failed.")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Directory Setup
|
||||
unique_folder_name = "session_1"
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
save_dir = os.path.join(base_dir, unique_folder_name)
|
||||
|
||||
if os.path.exists(save_dir):
|
||||
print(f"Forcibly purging existing directory: {save_dir}")
|
||||
shutil.rmtree(save_dir, ignore_errors=True)
|
||||
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
print(f"Created isolated workspace folder: {save_dir}")
|
||||
|
||||
print("\n!!! Camera drive mode should be set to Burst 1 / H !!!")
|
||||
print("Waiting for GUI trigger click...", flush=True)
|
||||
|
||||
# wait for button press from the gui
|
||||
input("")
|
||||
|
||||
print("\nExecuting synchronized multi-system burst...")
|
||||
|
||||
timing_shared = {}
|
||||
barrier = threading.Barrier(3)
|
||||
save_barrier = threading.Barrier(3)
|
||||
delete_barrier = threading.Barrier(3)
|
||||
abort_event = threading.Event()
|
||||
|
||||
# Create Lumix threads
|
||||
tA = threading.Thread(target=threaded_fast_burst, args=(camA, contextA, save_dir, OUTPUT_PREFIX_CAM1, timing_shared, barrier, abort_event, save_barrier, delete_barrier))
|
||||
tB = threading.Thread(target=threaded_fast_burst, args=(camB, contextB, save_dir, OUTPUT_PREFIX_CAM2, timing_shared, barrier, abort_event, save_barrier, delete_barrier))
|
||||
t_phone = threading.Thread(target=trigger_humanized_chained_burst)
|
||||
|
||||
MASTER_START_TIME = time.perf_counter()
|
||||
|
||||
# Fire the ADB Burst first
|
||||
print("[Coordinator] Launching phone burst thread...")
|
||||
t_phone.start()
|
||||
time.sleep(0.1)
|
||||
|
||||
# Fire the Lumix camera bursts
|
||||
print("[Coordinator] Launching Lumix camera burst threads...")
|
||||
tA.start()
|
||||
tB.start()
|
||||
|
||||
print_elapsed()
|
||||
# Coordinator evaluates Lumix synchronization constraints
|
||||
while len(timing_shared) < 2:
|
||||
time.sleep(0.01)
|
||||
|
||||
delta = abs(timing_shared[OUTPUT_PREFIX_CAM1] - timing_shared[OUTPUT_PREFIX_CAM2])
|
||||
print(f"\n[Coordinator] Lumix burst sync time difference: {delta:.4f}s")
|
||||
if delta > 0.05:
|
||||
print("[Coordinator] Sync target missed (>0.05s)! Activating recovery plan across all nodes...")
|
||||
abort_event.set()
|
||||
else:
|
||||
print("[Coordinator] Perfect alignment achieved (<0.05s). Proceeding with regular downloads...")
|
||||
|
||||
print_elapsed()
|
||||
# Release burst execution barrier
|
||||
barrier.wait()
|
||||
print_elapsed("b")
|
||||
# Wait for both Lumix threads to finish saving local copies completely
|
||||
# Wait until phone physical gesture thread finishes before running ADB pulls
|
||||
t_phone.join()
|
||||
phone_files_captured = pull_recent_burst_photos(local_dest_dir=save_dir)
|
||||
|
||||
save_barrier.wait()
|
||||
print("\n[Coordinator] Both Lumix cameras finished downloading. Pulling phone media...")
|
||||
print_elapsed("S")
|
||||
|
||||
|
||||
if 'abort_event' in locals() and abort_event.is_set():
|
||||
|
||||
print("\n[Coordinator] Out of sync state detected. Skipping independent 3D Pipeline invocation.", flush=True)
|
||||
else:
|
||||
print("\n[Coordinator] All device frames pulled successfully! Spawning completely independent 3D Pipeline...", flush=True)
|
||||
MASTER_START_TIME = time.perf_counter()
|
||||
|
||||
script_path = os.path.join(os.path.dirname(__file__), "part_B.py")
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
venv_python = os.path.join(current_dir, ".venv", "Scripts", "python.exe")
|
||||
|
||||
cmd_args = [venv_python, "-u", script_path, "--start-time", str(MASTER_START_TIME)]
|
||||
if initial_snirf:
|
||||
cmd_args.extend(["--snirf", str(initial_snirf)])
|
||||
|
||||
subprocess.Popen(
|
||||
cmd_args,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
cwd=os.path.join(current_dir)
|
||||
)
|
||||
|
||||
|
||||
if not bypass_2d:
|
||||
|
||||
print("\n[Coordinator] Entering synchronized deletion sweep across all hardware nodes...")
|
||||
# Release the delete barrier to let Lumix cameras clean card blocks alongside the ADB script
|
||||
delete_barrier.wait()
|
||||
delete_phone_photos(phone_files_captured)
|
||||
|
||||
# Cleanup hardware bindings
|
||||
tA.join()
|
||||
tB.join()
|
||||
camA.exit(contextA)
|
||||
camB.exit(contextB)
|
||||
|
||||
print("\nComplete multi-device burst capture sequence finished.")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------- Main Execution Pipeline ----------
|
||||
def main():
|
||||
# 1. Android Initialization
|
||||
print("Waking up phone camera via ADB...")
|
||||
run_adb_shell("am start -a android.media.action.STILL_IMAGE_CAMERA")
|
||||
time.sleep(1.5)
|
||||
|
||||
# 2. Lumix Hardware Initialization
|
||||
print("Searching for connected Lumix cameras...")
|
||||
all_detected = gp.Camera.autodetect()
|
||||
lumix_detected = [
|
||||
(name, value) for name, value in all_detected.items()
|
||||
if "panasonic" in name.lower()
|
||||
]
|
||||
|
||||
print(f"Found {len(all_detected)} total USB devices.")
|
||||
print(f"Lumix devices found: {lumix_detected}")
|
||||
|
||||
if len(lumix_detected) < 2:
|
||||
print("Error: Need at least 2 Lumix cameras connected.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\nConnecting to Lumix cameras...")
|
||||
camA, contextA = init_camera_by_path(lumix_detected[0][1])
|
||||
camB, contextB = init_camera_by_path(lumix_detected[1][1])
|
||||
if not camA or not camB:
|
||||
print("Error: Lumix initialization failed.")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Directory Setup
|
||||
unique_folder_name = "session_1"
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
save_dir = os.path.join(base_dir, unique_folder_name)
|
||||
|
||||
if os.path.exists(save_dir):
|
||||
print(f"Forcibly purging existing directory: {save_dir}")
|
||||
shutil.rmtree(save_dir, ignore_errors=True)
|
||||
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
print(f"Created isolated workspace folder: {save_dir}")
|
||||
|
||||
print("\n!!! Camera drive mode should be set to Burst 1 / H !!!")
|
||||
input("Press Enter to execute all synced burst captures (Lumix + S24+)...")
|
||||
|
||||
print("\nExecuting synchronized multi-system burst...")
|
||||
|
||||
timing_shared = {}
|
||||
barrier = threading.Barrier(3)
|
||||
save_barrier = threading.Barrier(3)
|
||||
delete_barrier = threading.Barrier(3) # Controls deletion phase entry
|
||||
abort_event = threading.Event()
|
||||
|
||||
# Create Lumix threads
|
||||
tA = threading.Thread(target=threaded_fast_burst, args=(camA, contextA, save_dir, OUTPUT_PREFIX_CAM1, timing_shared, barrier, abort_event, save_barrier, delete_barrier))
|
||||
tB = threading.Thread(target=threaded_fast_burst, args=(camB, contextB, save_dir, OUTPUT_PREFIX_CAM2, timing_shared, barrier, abort_event, save_barrier, delete_barrier))
|
||||
t_phone = threading.Thread(target=trigger_humanized_chained_burst)
|
||||
|
||||
MASTER_START_TIME = time.perf_counter()
|
||||
|
||||
# Fire the ADB Burst first
|
||||
print("[Coordinator] Launching phone burst thread...")
|
||||
t_phone.start()
|
||||
time.sleep(0.1)
|
||||
|
||||
# Fire the Lumix camera bursts
|
||||
print("[Coordinator] Launching Lumix camera burst threads...")
|
||||
tA.start()
|
||||
tB.start()
|
||||
|
||||
print_elapsed()
|
||||
# Coordinator evaluates Lumix synchronization constraints
|
||||
while len(timing_shared) < 2:
|
||||
time.sleep(0.01)
|
||||
|
||||
delta = abs(timing_shared[OUTPUT_PREFIX_CAM1] - timing_shared[OUTPUT_PREFIX_CAM2])
|
||||
print(f"\n[Coordinator] Lumix burst sync time difference: {delta:.4f}s")
|
||||
if delta > 0.05:
|
||||
print("[Coordinator] Sync target missed (>0.05s)! Activating recovery plan across all nodes...")
|
||||
abort_event.set()
|
||||
else:
|
||||
print("[Coordinator] Perfect alignment achieved (<0.05s). Proceeding with regular downloads...")
|
||||
|
||||
print_elapsed()
|
||||
# Release burst execution barrier
|
||||
barrier.wait()
|
||||
print_elapsed("b")
|
||||
# Wait for both Lumix threads to finish saving local copies completely
|
||||
# Wait until phone physical gesture thread finishes before running ADB pulls
|
||||
t_phone.join()
|
||||
phone_files_captured = pull_recent_burst_photos(local_dest_dir=save_dir)
|
||||
|
||||
save_barrier.wait()
|
||||
print("\n[Coordinator] Both Lumix cameras finished downloading. Pulling phone media...")
|
||||
print_elapsed("S")
|
||||
|
||||
|
||||
print("\n[Coordinator] All device frames pulled successfully! Spawning completely independent 3D Pipeline...")
|
||||
|
||||
print_elapsed()
|
||||
|
||||
script_path = os.path.join(os.path.dirname(__file__), "part_B.py")
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
venv_python = os.path.join(current_dir, "..", ".venv", "Scripts", "python.exe")
|
||||
|
||||
subprocess.Popen(
|
||||
[venv_python, "-u", script_path, str(MASTER_START_TIME)],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
cwd=os.path.join(current_dir)
|
||||
)
|
||||
|
||||
print("\n[Coordinator] Entering synchronized deletion sweep across all hardware nodes...")
|
||||
# Release the delete barrier to let Lumix cameras clean card blocks alongside the ADB script
|
||||
delete_barrier.wait()
|
||||
delete_phone_photos(phone_files_captured)
|
||||
|
||||
# Cleanup hardware bindings
|
||||
tA.join()
|
||||
tB.join()
|
||||
camA.exit(contextA)
|
||||
camB.exit(contextB)
|
||||
print("\nComplete multi-device burst capture sequence finished.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="gphoto2 isolated subprocess bridge")
|
||||
parser.add_argument("--cmd", type=str, required=False, help="Command execution choice (e.g., detect)")
|
||||
parser.add_argument("--snirf", type=str, required=False, help="Path to a SNIRF file to load on startup")
|
||||
parser.add_argument("--bypass-2d", action="store_true", help="Bypass 2D processing pipelines")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.cmd == "gui":
|
||||
gui_entry(bypass_2d=True, initial_snirf=args.snirf)
|
||||
else:
|
||||
main()
|
||||
|
||||
except Exception as e:
|
||||
print(f"BRIDGE_ERROR: {type(e).__name__}: {str(e)}")
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user