Skip to content

Commit 3bba574

Browse files
committed
add some basic filesystem based locks to prevent overwriting dlc/anipose
projects concurrently
1 parent ee81475 commit 3bba574

3 files changed

Lines changed: 189 additions & 182 deletions

File tree

packages/cheese3d/cheese3d/project.py

Lines changed: 87 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import List, Dict, Optional, Any
1414
from collections import namedtuple
1515
from datetime import datetime
16+
from filelock import FileLock
1617

1718
from cheese3d.anatomy import compute_anatomical_measurements
1819
from cheese3d.config import (MultiViewConfig,
@@ -279,12 +280,13 @@ def from_cfg(cls, cfg: ProjectConfig, root: str | Path, model_import = None):
279280
else:
280281
ephys = None
281282
model_cfg = maybe(model_import, cfg.model)
282-
model = build_model_backend(model_cfg,
283-
root=(root / cfg.name /
284-
relative_path(cfg.model_root, root / cfg.name)),
285-
sessions=sessions,
286-
view_cfg=cfg.views,
287-
keypoints=cfg.keypoints)
283+
with FileLock(root / cfg.name / "build_backend.lock"):
284+
model = build_model_backend(model_cfg,
285+
root=(root / cfg.name /
286+
relative_path(cfg.model_root, root / cfg.name)),
287+
sessions=sessions,
288+
view_cfg=cfg.views,
289+
keypoints=cfg.keypoints)
288290
view_regex = get_group_pattern(ProjectConfig.build_regex(cfg.video_regex), "view")
289291

290292
return cls(name=cfg.name,
@@ -410,17 +412,19 @@ def _import_labels(self):
410412
if self.model is None:
411413
raise RuntimeError("Cannot import labels when pose model does not exist "
412414
"(hint: maybe you forgot to set `model.name` in the config?")
413-
self._create_labels()
414-
label_paths = self._label_folder_paths()
415-
self.model.import_c3d_labels(label_paths)
415+
with FileLock(self.path / "labels.lock"):
416+
self._create_labels()
417+
label_paths = self._label_folder_paths()
418+
self.model.import_c3d_labels(label_paths)
416419

417420
def _export_labels(self):
418421
if self.model is None:
419422
raise RuntimeError("Cannot export labels when pose model does not exist "
420423
"(hint: maybe you forgot to set `model.name` in the config?")
421-
self._create_labels()
422-
label_paths = self._label_folder_paths()
423-
self.model.export_c3d_labels(label_paths)
424+
with FileLock(self.path / "labels.lock"):
425+
self._create_labels()
426+
label_paths = self._label_folder_paths()
427+
self.model.export_c3d_labels(label_paths)
424428

425429
def extract_frames(self, sessions: Optional[List[RecordingKey]] = None, manual = False):
426430
self._import_labels()
@@ -475,81 +479,82 @@ def _setup_anipose(self):
475479
if self.model is None:
476480
raise RuntimeError("Cannot setup triangulation when pose model does not exist "
477481
"(hint: maybe you forgot to set `model.name` in the config?")
478-
# make anipose project folder
479-
self.triangulation_path.mkdir(exist_ok=True)
480-
# create session subfolders
481-
for recording, videos in self.sessions.items():
482-
session_path = self.triangulation_path / recording.name
483-
session_path.mkdir(exist_ok=True)
484-
# add raw videos
485-
videos_path = session_path / "videos-raw"
486-
videos_path.mkdir(exist_ok=True)
487-
for video in videos.values():
488-
src = Path(video).resolve()
489-
dst = videos_path / src.name
490-
relpath = Path(os.path.relpath(src, videos_path.resolve()))
491-
if dst.exists():
492-
os.remove(dst)
493-
os.symlink(relpath, dst)
494-
# add calibration
495-
calibration_path = session_path / "calibration"
496-
calibration_path.mkdir(exist_ok=True)
497-
# add calibration files
498-
cal_key = RecordingKey(recording.session, recording.name)
499-
matches = [k for k in self.calibrations.keys() if cal_key.matches(k)]
500-
if len(matches) == 0:
501-
raise RuntimeError(f"No calibration found for {recording} when setting up triangulation")
502-
for match in matches:
503-
for video in self.calibrations[match].values():
482+
with FileLock(self.path / "setup_anipose.lock"):
483+
# make anipose project folder
484+
self.triangulation_path.mkdir(exist_ok=True)
485+
# create session subfolders
486+
for recording, videos in self.sessions.items():
487+
session_path = self.triangulation_path / recording.name
488+
session_path.mkdir(exist_ok=True)
489+
# add raw videos
490+
videos_path = session_path / "videos-raw"
491+
videos_path.mkdir(exist_ok=True)
492+
for video in videos.values():
504493
src = Path(video).resolve()
505-
dst = calibration_path / src.name
506-
relpath = Path(os.path.relpath(src, calibration_path.resolve()))
494+
dst = videos_path / src.name
495+
relpath = Path(os.path.relpath(src, videos_path.resolve()))
507496
if dst.exists():
508497
os.remove(dst)
509498
os.symlink(relpath, dst)
510-
# create anipose config file
511-
kp_schema = keypoints_by_group(self.keypoints)
512-
for group, kps in kp_schema.items():
513-
if len(kps) > 2:
514-
kp_schema[group].append(kps[0])
515-
config = {
516-
"project": self.name,
517-
"model_folder": os.path.relpath(self.model.project_path, os.getcwd()),
518-
"nesting": 1,
519-
"pipeline": {"videos-raw": "videos-raw",},
520-
"labeling": {
521-
"scheme": list(kp_schema.values()),
522-
"ignore": self.ignore_keypoint_labels
523-
},
524-
"filter": {
525-
"enabled": self.triangulation.filter2d,
526-
"type": "medfilt",
527-
"medfilt": 13, # length of median filter
528-
"offset_threshold": 5, # offset from median filter to count as jump
529-
"score_threshold": 0.8, # score below which to count as bad
530-
"spline": False, # interpolate using linearly instead of cubic spline
531-
},
532-
"calibration": {
533-
"board_type": "charuco",
534-
"board_size": [7, 7],
535-
"board_marker_bits": 4,
536-
"board_marker_dict_number": 50,
537-
"board_marker_length": 4.5, # mm
538-
"board_square_side_length": 6 # mm
539-
},
540-
"triangulation": {
541-
"triangulate": True,
542-
"cam_regex": f"({self.view_regex})",
543-
"manually_verify": False,
544-
"axes": self.triangulation.axes,
545-
"reference_point": self.triangulation.ref_point,
546-
"optim": True,
547-
"score_threshold": self.triangulation.score_threshold,
548-
"scale_smooth": 0.0,
499+
# add calibration
500+
calibration_path = session_path / "calibration"
501+
calibration_path.mkdir(exist_ok=True)
502+
# add calibration files
503+
cal_key = RecordingKey(recording.session, recording.name)
504+
matches = [k for k in self.calibrations.keys() if cal_key.matches(k)]
505+
if len(matches) == 0:
506+
raise RuntimeError(f"No calibration found for {recording} when setting up triangulation")
507+
for match in matches:
508+
for video in self.calibrations[match].values():
509+
src = Path(video).resolve()
510+
dst = calibration_path / src.name
511+
relpath = Path(os.path.relpath(src, calibration_path.resolve()))
512+
if dst.exists():
513+
os.remove(dst)
514+
os.symlink(relpath, dst)
515+
# create anipose config file
516+
kp_schema = keypoints_by_group(self.keypoints)
517+
for group, kps in kp_schema.items():
518+
if len(kps) > 2:
519+
kp_schema[group].append(kps[0])
520+
config = {
521+
"project": self.name,
522+
"model_folder": os.path.relpath(self.model.project_path, os.getcwd()),
523+
"nesting": 1,
524+
"pipeline": {"videos-raw": "videos-raw",},
525+
"labeling": {
526+
"scheme": list(kp_schema.values()),
527+
"ignore": self.ignore_keypoint_labels
528+
},
529+
"filter": {
530+
"enabled": self.triangulation.filter2d,
531+
"type": "medfilt",
532+
"medfilt": 13, # length of median filter
533+
"offset_threshold": 5, # offset from median filter to count as jump
534+
"score_threshold": 0.8, # score below which to count as bad
535+
"spline": False, # interpolate using linearly instead of cubic spline
536+
},
537+
"calibration": {
538+
"board_type": "charuco",
539+
"board_size": [7, 7],
540+
"board_marker_bits": 4,
541+
"board_marker_dict_number": 50,
542+
"board_marker_length": 4.5, # mm
543+
"board_square_side_length": 6 # mm
544+
},
545+
"triangulation": {
546+
"triangulate": True,
547+
"cam_regex": f"({self.view_regex})",
548+
"manually_verify": False,
549+
"axes": self.triangulation.axes,
550+
"reference_point": self.triangulation.ref_point,
551+
"optim": True,
552+
"score_threshold": self.triangulation.score_threshold,
553+
"scale_smooth": 0.0,
554+
}
549555
}
550-
}
551-
with open(self.triangulation_path / "config.toml", "w") as f:
552-
toml.dump(config, f)
556+
with open(self.triangulation_path / "config.toml", "w") as f:
557+
toml.dump(config, f)
553558

554559
def _load_anipose_cfg(self):
555560
from anipose.anipose import load_config

packages/cheese3d/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"opencv-contrib-python == 4.7.*",
2626
"pims == 0.6.*",
2727
"seaborn",
28+
"filelock >= 3.24.3",
2829
"open-ephys-python-tools == 0.1.12",
2930
"typer-slim[standard]>=0.15.3,<0.16",
3031
"questionary",

0 commit comments

Comments
 (0)