initial commit

This commit is contained in:
2026-07-12 22:29:30 -07:00
parent accb119ea6
commit 8236a307a0
818 changed files with 996511 additions and 1 deletions
+258
View File
@@ -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 warmup 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()