Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/openlifu/nav/photoscan.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import datetime
import importlib
import json
import logging
import re
import shutil
import tempfile
import threading
Expand Down Expand Up @@ -660,3 +662,59 @@ def write_pair_file(image_paths: List[Path], camera_init_file: Path, output_path
with open(output_path, "w") as f:
for row in rows:
f.write(" ".join(map(str, row)) + "\n")


def _get_datetime_taken(image_path: Path) -> datetime.datetime | None:
"""Get the date and time from image metadata."""
with Image.open(image_path) as img:
value = img.getexif().get(306, None)
if value is None:
return None
else:
return datetime.datetime.strptime(value, '%Y:%m:%d %H:%M:%S')

def _extract_numbers(filename: str) -> List[int]:
"""Extracts all sub-strings of numbers as a list of ints."""
return list(map(int, re.findall(r'\d+', filename)))

def preprocess_image_paths(paths: List[Path],
sort_by: str = 'filename',
sampling_rate: int = 1) -> List[Path]:
"""
Sorts and sub-samples a list of image file paths.

Args:
paths (List[Path]): A list of image file paths to be processed.
sort_by (str): Method to sort the images. Options are:
- 'filename': Sort by numeric values extracted from filenames.
- 'metadata': Sort by the DateTimeOriginal field in EXIF metadata.
sampling_rate (int): Return every n-th image after sorting. Must be >= 1.

Returns:
List[Path]: A sorted and sub-sampled list of image paths.

Raises:
ValueError: If `sort_by` is not one of the valid options.
ValueError: If `sampling_rate` is less than 1.
ValueError: If sorting by metadata and no valid EXIF DateTimeOriginal is found.
"""
if sampling_rate < 1:
raise ValueError("sampling_rate must be a positive integer.")

if sort_by == 'metadata':
dated = []

for path in paths:
dt = _get_datetime_taken(path)
if dt is None:
raise ValueError(f"Tried to sort by metadata, but no valid 'DateTimeOriginal' metadata was found in image: {path}")
dated.append((dt, path))

dated.sort(key=lambda x: x[0])
paths = [p for _, p in dated]
elif sort_by == 'filename':
paths = sorted(paths, key=lambda p: _extract_numbers(p.name))
else:
raise ValueError(f"Invalid sort_by value '{sort_by}'. Use 'metadata' or 'filename'.")

return paths[::sampling_rate]
Loading