Skip to content

Commit 1b4c68e

Browse files
committed
Add photocollection concept to database (#234)
Adds a concept of photocollections to the database. Photocollections are associated with the specific session_id of a specific subject_id, and they are identified by the reference_number. 'reference_number' was used instead of photocollection_id for consistency with 3D Open Water. 3D Open Water takes a collection of `.jpeg` photos and the goal of photocollections are to store that collection of photos into the database for downstream processing; specifically, the plan is for a photocollection of 70-80 photos of a patient's face to be used to create a mesh (i.e. a "photoscan" object). There is no special object associated with a photocollection -- it's just a collection of photos stored in a directory and identified by the reference_number. - Updates the database to valid state
1 parent 322c75a commit 1b4c68e

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

db_dvc.dvc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
outs:
2-
- md5: cf89344a4eee1e75c5be53d91941815e.dir
3-
size: 803480549
4-
nfiles: 83
2+
- md5: 77217cb7a18172e6db4f130b4931c899.dir
3+
size: 829348265
4+
nfiles: 249
55
hash: md5
66
path: db_dvc

src/openlifu/db/database.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ def write_session(self, subject:Subject, session:Session, on_conflict=OnConflict
190190
self.write_solution_ids(session, [])
191191
if not self.get_runs_filename(subject.id, session_id).exists():
192192
self.write_run_ids(session.subject_id, session.id, [])
193+
if not self.get_photocollections_filename(subject.id, session_id).exists():
194+
self.write_reference_numbers(session.subject_id, session.id, [])
193195
if not self.get_photoscans_filename(subject.id, session_id).exists():
194196
self.write_photoscan_ids(session.subject_id, session.id, [])
195197

@@ -396,6 +398,42 @@ def write_volume(self, subject_id, volume_id, volume_name, volume_data_filepath,
396398

397399
self.logger.info(f"Added volume with ID {volume_id} for subject {subject_id} to the database.")
398400

401+
def write_photocollection(self, subject_id, session_id, reference_number: str, photo_paths: List[PathLike], on_conflict=OnConflictOpts.ERROR):
402+
""" Writes a photocollection to database and copies the associated
403+
photos into the database, specified by the subject, session, and
404+
reference_number of the photocollection."""
405+
406+
photocollection_dir = Path(self.get_session_dir(subject_id, session_id)) / 'photocollections' / reference_number
407+
408+
reference_numbers = self.get_photocollection_reference_numbers(subject_id, session_id)
409+
if reference_number in reference_numbers:
410+
if on_conflict == OnConflictOpts.ERROR:
411+
raise ValueError(f"Photocollection with reference number {reference_number} already exists for session {session_id}.")
412+
elif on_conflict == OnConflictOpts.OVERWRITE:
413+
self.logger.info(f"Overwriting photocollection with reference number {reference_number} for session {session_id}.")
414+
if photocollection_dir.exists():
415+
shutil.rmtree(photocollection_dir)
416+
elif on_conflict == OnConflictOpts.SKIP:
417+
self.logger.info(f"Skipping photocollection with reference number {reference_number} for session {session_id} as it already exists.")
418+
return
419+
else:
420+
raise ValueError("Invalid 'on_conflict' option. Use 'error', 'overwrite', or 'skip'.")
421+
422+
photocollection_dir.mkdir(exist_ok=True)
423+
424+
# Copy each photo into the photocollection directory
425+
for photo_path in photo_paths:
426+
photo_path = Path(photo_path)
427+
if not photo_path.exists():
428+
raise FileNotFoundError(f"Photo file does not exist: {photo_path}")
429+
shutil.copy(photo_path, photocollection_dir)
430+
431+
if reference_number not in reference_numbers:
432+
reference_numbers.append(reference_number)
433+
self.write_reference_numbers(subject_id,session_id, reference_numbers)
434+
435+
self.logger.info(f"Added photocollection with reference number {reference_number} for session {session_id} to the database.")
436+
399437
def write_photoscan(self, subject_id, session_id, photoscan: Photoscan, model_data_filepath: str | None = None, texture_data_filepath: str | None = None, mtl_data_filepath: str | None = None, on_conflict=OnConflictOpts.ERROR):
400438
""" Writes a photoscan object to database and copies the associated model and texture data filepaths that are required for generating a photoscan into the database.
401439
.mtl files are not required for generating a photoscan but can be provided if present.
@@ -600,6 +638,16 @@ def get_solution_ids(self, subject_id:str, session_id:str) -> List[str]:
600638

601639
return json.loads(solutions_filename.read_text())["solution_ids"]
602640

641+
def get_photocollection_reference_numbers(self, subject_id: str, session_id: str) -> List[str]:
642+
"""Get a list of reference numbers of the photocollections associated with the given session"""
643+
photocollection_filename = self.get_photocollections_filename(subject_id, session_id)
644+
645+
if not (photocollection_filename.exists() and photocollection_filename.is_file()):
646+
self.logger.warning("Photocollection file not found for subject %s, session %s.", subject_id, session_id)
647+
return []
648+
649+
return json.loads(photocollection_filename.read_text())["reference_numbers"]
650+
603651
def get_photoscan_ids(self, subject_id: str, session_id: str) -> List[str]:
604652
"""Get a list of IDs of the photoscans associated with the given session"""
605653
photoscan_filename = self.get_photoscans_filename(subject_id, session_id)
@@ -685,6 +733,31 @@ def get_volume_info(self, subject_id, volume_id):
685733
"name": volume["name"],\
686734
"data_abspath": Path(volume_metadata_filepath).parent/volume["data_filename"]}
687735

736+
def get_photocollection_absolute_filepaths(self, subject_id: str, session_id: str, reference_number: str) -> List[Path]:
737+
"""
738+
get the absolute filepaths of all photos in a specific photocollection.
739+
740+
Args:
741+
subject_id (str): The subject ID.
742+
session_id (str): The session ID.
743+
reference_number (str): The reference number of the photocollection.
744+
745+
Returns:
746+
List[Path]: List of absolute file paths to the photos in the photocollection.
747+
"""
748+
photocollection_dir = (
749+
Path(self.get_session_dir(subject_id, session_id)) / 'photocollections' / reference_number
750+
)
751+
752+
if not photocollection_dir.exists() or not photocollection_dir.is_dir():
753+
self.logger.warning(
754+
f"Photocollection directory not found for subject {subject_id}, "
755+
f"session {session_id}, photocollection {reference_number}."
756+
)
757+
return []
758+
759+
return sorted(photocollection_dir.glob("*"))
760+
688761
def get_photoscan_absolute_filepaths_info(self, subject_id, session_id, photoscan_id):
689762
"""Returns the photoscan information with absolute paths to any data"""
690763
photoscan_metadata_filepath = self.get_photoscan_metadata_filepath(subject_id, session_id, photoscan_id)
@@ -949,6 +1022,11 @@ def get_solutions_filename(self, subject_id, session_id) -> Path:
9491022
session_dir = self.get_session_dir(subject_id, session_id)
9501023
return Path(session_dir) / 'solutions' / 'solutions.json'
9511024

1025+
def get_photocollections_filename(self, subject_id, session_id) -> Path:
1026+
"""Get the path to the overall photocollections json file for the requested session"""
1027+
session_dir = self.get_session_dir(subject_id, session_id)
1028+
return Path(session_dir) / 'photocollections' / 'photocollections.json'
1029+
9521030
def get_photoscans_filename(self, subject_id, session_id) -> Path:
9531031
"""Get the path to the overall photoscans json file for the requested session"""
9541032
session_dir = self.get_session_dir(subject_id, session_id)
@@ -1045,6 +1123,13 @@ def write_transducer_ids(self, transducer_ids):
10451123
with open(transducers_filename, 'w') as f:
10461124
json.dump(transducers_data, f)
10471125

1126+
def write_reference_numbers(self, subject_id, session_id, reference_numbers: List[str]):
1127+
photocollection_data = {'reference_numbers': reference_numbers}
1128+
photocollection_filename = self.get_photocollections_filename(subject_id, session_id)
1129+
photocollection_filename.parent.mkdir(exist_ok = True) # Make a photocollection directory in case it does not exist
1130+
with open(photocollection_filename, 'w') as f:
1131+
json.dump(photocollection_data,f)
1132+
10481133
def write_photoscan_ids(self, subject_id, session_id, photoscan_ids: List[str]):
10491134
photoscan_data = {'photoscan_ids': photoscan_ids}
10501135
photoscan_filename = self.get_photoscans_filename(subject_id, session_id)

0 commit comments

Comments
 (0)