""" Filename: synced_photo_taker.py Description: Takes photos from all cameras at the same time Author: Tyler de Zeeuw License: GPL-3.0 """ # Built-in imports import os import sys import time import threading import subprocess # External library imports import gphoto2 as gp import cv2 #TODO: Ship this with the application ADB_PATH = "C:/Android/platform-tools/adb" 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 threaded_trigger(camera, context, results_dict, cam_key): try: file_path = camera.capture(gp.GP_CAPTURE_IMAGE, context) results_dict[cam_key] = file_path except Exception as e: print(f"Capture error on {cam_key}: {e}") results_dict[cam_key] = None def is_screen_on(): """Return True if the phone screen is currently on.""" result = subprocess.run( [ADB_PATH, "shell", "dumpsys", "power"], capture_output=True, text=True ) for line in result.stdout.split('\n'): if 'Display Power' in line and 'state=ON' in line: return True return False def ensure_phone_camera_open(): """Wake phone if needed, dismiss lock screen, and open camera app.""" if not is_screen_on(): subprocess.run([ADB_PATH, "shell", "input", "keyevent", "26"]) # Power time.sleep(1.0) subprocess.run([ADB_PATH, "shell", "wm", "dismiss-keyguard"], capture_output=True) subprocess.run([ADB_PATH, "shell", "input", "swipe", "500", "1500", "500", "500"], capture_output=True) time.sleep(0.5) subprocess.run([ ADB_PATH, "shell", "am", "start", "-a", "android.media.action.STILL_IMAGE_CAMERA" ], capture_output=True) time.sleep(3.0) def delete_phone_file(remote_path): """Delete a file on the phone to avoid re-pulling it.""" subprocess.run([ADB_PATH, "shell", "rm", remote_path], capture_output=True) def pull_latest_phone_photo(save_dir, suffix, trigger_time=None): """ Pull the most recently created .jpg (by modification time). If trigger_time is given, only consider files newer than that epoch. Returns True on success. """ cmd = str(ADB_PATH) + " shell stat -c '%Y %n' /sdcard/DCIM/Camera/*.jpg 2>/dev/null" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) if result.returncode != 0 or not result.stdout.strip(): print("No photos found on phone.") return False files_with_time = [] for line in result.stdout.strip().split('\n'): parts = line.split(' ', 1) if len(parts) == 2: try: epoch = int(parts[0]) path = parts[1] files_with_time.append((epoch, path)) except: pass if not files_with_time: return False if trigger_time is not None: files_with_time = [(ep, p) for ep, p in files_with_time if ep > trigger_time] if not files_with_time: print("No new photo found after trigger.") return False latest_epoch, latest_remote = max(files_with_time, key=lambda x: x[0]) local_path = os.path.join(save_dir, f"Phone_fullres_{suffix}.jpg") subprocess.run([ADB_PATH, "pull", latest_remote, local_path], capture_output=True) img = cv2.imread(local_path) if img is not None: print(f"Phone photo saved: {local_path} ({img.shape[1]}x{img.shape[0]})") return True else: print("Failed to pull valid photo.") return False def trigger_phone_shutter(): """Trigger shutter and return the epoch time just before firing.""" subprocess.run([ADB_PATH, "shell", "input", "keyevent", "27"]) time.sleep(3.0) return int(time.time()) - 5 def capture_and_save_triplet(camA, camB, contextA, contextB, save_dir, suffix): capture_results = {} tA = threading.Thread(target=threaded_trigger, args=(camA, contextA, capture_results, 'camA')) tB = threading.Thread(target=threaded_trigger, args=(camB, contextB, capture_results, 'camB')) tA.start() tB.start() trigger_phone_shutter() tA.join() tB.join() fileA = capture_results.get('camA') fileB = capture_results.get('camB') if not fileA or not fileB: print("Lumix capture failed.") return False targetA = os.path.join(save_dir, f"Cam_1_fullres_{suffix}.jpg") targetB = os.path.join(save_dir, f"Cam_2_fullres_{suffix}.jpg") try: cfA = gp.CameraFile() camA.file_get(fileA.folder, fileA.name, gp.GP_FILE_TYPE_NORMAL, cfA, contextA) cfA.save(targetA) cfB = gp.CameraFile() camB.file_get(fileB.folder, fileB.name, gp.GP_FILE_TYPE_NORMAL, cfB, contextB) cfB.save(targetB) print(f"Cam1 saved: {targetA}") print(f"Cam2 saved: {targetB}") except Exception as e: print(f"Lumix download error: {e}") return False phone_ok = pull_latest_phone_photo(save_dir, suffix) return phone_ok def main(): 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) adb_check = subprocess.run([ADB_PATH, "devices"], capture_output=True, text=True) if "device" not in adb_check.stdout.split("\n")[1]: print("No ADB device found. Connect phone and enable USB debugging.") sys.exit(1) save_dir = os.path.dirname(os.path.abspath(__file__)) print("\nWaking phone and opening camera...") ensure_phone_camera_open() print("Taking warm‑up photo (discarded)...") trigger_phone_shutter() _ = pull_latest_phone_photo(save_dir, "warmup") if _: cmd = str(ADB_PATH) + " shell stat -c '%Y %n' /sdcard/DCIM/Camera/*.jpg 2>/dev/null" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) if result.stdout.strip(): lines = result.stdout.strip().split('\n') _, latest_path = max((int(l.split()[0]), l.split(' ',1)[1]) for l in lines if l) delete_phone_file(latest_path) print("Warm-up photo deleted from phone.") print("Warm-up complete.\n") try: for i in range(1, 21): print(f"\n--- Calibration triplet {i}/20 ---") success = capture_and_save_triplet(camA, camB, contextA, contextB, save_dir, str(i)) if not success: print("Capture failed for this triplet. You may delete the faulty files later.") if i < 20: print("Waiting a second...") time.sleep(1) input("\nPosition the optode cap, then press Enter to capture...") success = capture_and_save_triplet(camA, camB, contextA, contextB, save_dir, "optode") if success: for cam, base in [("Cam_1", "Cam_1"), ("Cam_2", "Cam_2"), ("Phone", "Cam_3")]: src = os.path.join(save_dir, f"{cam}_fullres_optode.jpg") dst = os.path.join(save_dir, f"{base}.jpg") if os.path.exists(src): os.replace(src, dst) print("Optode photos saved as Cam_1.jpg, Cam_2.jpg, Cam_3.jpg.") else: print("Optode capture failed.") except KeyboardInterrupt: print("\nInterrupted.") finally: camA.exit(contextA) camB.exit(contextB) print("Cameras released.") if __name__ == "__main__": main()