initial commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user