258 lines
8.2 KiB
Python
258 lines
8.2 KiB
Python
"""
|
||
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() |