Visitar URL original
Switch 3D config reads to plain read by C-Achard · Pull Request #3587 · DeepLabCut/DeepLabCut · GitHub
Skip to content
Open
Show file tree
Hide file tree
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
9 changes: 8 additions & 1 deletion deeplabcut/core/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ def get_yaml_dumper() -> YAML:
return yaml


def read_config_as_dict(config_path: str | Path) -> dict:
def read_config_as_dict(config_path: str | Path, repair_project_path: bool = False) -> dict:
"""
Args:
config_path: the path to the configuration file to load
repair_project_path: if True, fix the config key ``project_path`` to match the parent
folder of ``config_path``. Only the return dict is updated, not persisted to disk.

Returns:
The configuration file with pure Python classes
Expand All @@ -75,6 +77,11 @@ def read_config_as_dict(config_path: str | Path) -> dict:
raise ValueError(f"Config {config_path} is empty or null.")
if not isinstance(cfg, dict):
raise ValueError(f"Config {config_path} must be a YAML mapping at the top level, got {type(cfg).__name__}.")
if repair_project_path:
project_path = Path(config_path).parent.absolute()
if cfg.get("project_path") is None or Path(cfg["project_path"]).absolute() != project_path:
logger.info("project_path updated: %s -> %s", cfg.get("project_path"), project_path)
cfg["project_path"] = str(project_path)
return cfg


Expand Down
3 changes: 2 additions & 1 deletion deeplabcut/generate_training_dataset/frame_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ def extract_frames(
from skimage import io
from skimage.util import img_as_ubyte

from deeplabcut.core.config import read_config_as_dict
from deeplabcut.utils import auxiliaryfunctions, frameselectiontools

videos_list = None if videos_list is None else [Path(video) for video in videos_list]
Expand Down Expand Up @@ -506,7 +507,7 @@ def extract_frames(
project_path = Path(config).parents[0]
labels_path = project_path / "labeled-data"
try:
cfg_3d = auxiliaryfunctions.read_config(config3d)
cfg_3d = read_config_as_dict(config3d, repair_project_path=True)
except Exception as e:
raise Exception(
"You must create a 3D project and edit the 3D config file before extracting matched frames. \n"
Expand Down
5 changes: 3 additions & 2 deletions deeplabcut/pose_estimation_3d/camera_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import matplotlib.pyplot as plt
import numpy as np

from deeplabcut.core.config import read_config_as_dict
from deeplabcut.utils import auxiliaryfunctions, auxiliaryfunctions_3d
from deeplabcut.utils.matplotlib_utils import silence_axes_logger

Expand Down Expand Up @@ -78,7 +79,7 @@ def calibrate_cameras(
objp[:, :2] = np.mgrid[0:cbcol, 0:cbrow].T.reshape(-1, 2)

# Read the config file
cfg_3d = auxiliaryfunctions.read_config(config)
cfg_3d = read_config_as_dict(config, repair_project_path=True)
(
img_path,
path_corners,
Expand Down Expand Up @@ -281,7 +282,7 @@ def check_undistortion(config: str | Path, cbrow=8, cbcol=6, plot=True):
"""
# Read the config file
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
cfg_3d = auxiliaryfunctions.read_config(config)
cfg_3d = read_config_as_dict(config, repair_project_path=True)
(
img_path,
path_corners,
Expand Down
4 changes: 2 additions & 2 deletions deeplabcut/pose_estimation_3d/plotting3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from mpl_toolkits.mplot3d.art3d import Line3DCollection
from tqdm import tqdm

from deeplabcut.core.config import read_config_as_dict
from deeplabcut.utils import (
auxiliaryfunctions,
auxiliaryfunctions_3d,
make_labeled_video,
)
Expand Down Expand Up @@ -124,7 +124,7 @@ def create_labeled_video_3d(
)
"""
# Read the config file and related variables
cfg_3d = auxiliaryfunctions.read_config(config)
cfg_3d = read_config_as_dict(config, repair_project_path=True)
cam_names = cfg_3d["camera_names"]
pcutoff = cfg_3d["pcutoff"]
markerSize = cfg_3d["dotsize"]
Expand Down
5 changes: 3 additions & 2 deletions deeplabcut/pose_estimation_3d/triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import numpy as np
import pandas as pd

from deeplabcut.core.config import read_config_as_dict
from deeplabcut.core.trackingutils import TRACK_METHODS
from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions, auxiliaryfunctions_3d

Expand Down Expand Up @@ -95,7 +96,7 @@ def triangulate(
from deeplabcut.compat import analyze_videos
from deeplabcut.post_processing import filtering

cfg_3d = auxiliaryfunctions.read_config(config)
cfg_3d = read_config_as_dict(config, repair_project_path=True)
cam_names = cfg_3d["camera_names"]
pcutoff = cfg_3d["pcutoff"]
scorer_3d = cfg_3d["scorername_3d"]
Expand Down Expand Up @@ -500,7 +501,7 @@ def _undistort_views(df_view_pairs, stereo_params):


def undistort_points(config, dataframe, camera_pair):
cfg_3d = auxiliaryfunctions.read_config(config)
cfg_3d = read_config_as_dict(config, repair_project_path=True)
path_camera_matrix = auxiliaryfunctions_3d.Foldernames3Dproject(cfg_3d)[2]
"""path_undistort = destfolder filename_cam1 = Path(dataframe[0]).stem filename_cam2
= Path(dataframe[1]).stem.
Expand Down
3 changes: 2 additions & 1 deletion examples/testscript_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from pathlib import Path

import deeplabcut
from deeplabcut.core.config import read_config_as_dict

if __name__ == "__main__":
print("Imported DLC!")
Expand Down Expand Up @@ -113,7 +114,7 @@
path_config_file = deeplabcut.create_new_project_3d(task, scorer, num_cameras)

try:
cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file)
cfg = read_config_as_dict(path_config_file, repair_project_path=True)
cfg["config_file_camera-1"] = config
cfg["shuffle_camera-1"] = 1

Expand Down
9 changes: 9 additions & 0 deletions tests/generate_training_dataset/test_frame_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,12 @@ def test_filter_config_videos_matches_string_to_path(self):
def test_filter_config_videos_raises_for_empty_selection(self):
with pytest.raises(ValueError, match="None of the selected videos matched"):
_filter_config_videos(["video-a.mp4"], [])


def test_extract_frames_match_reads_3d_config(valid_project, tmp_path):
"""Regression test for #3585: mode="match" must read the 3D config without 2D validation."""
from deeplabcut.create_project.new_3d import create_new_project_3d

config_3d = create_new_project_3d("trial", "tester", num_cameras=2, working_directory=str(tmp_path / "3d"))

extract_frames(str(valid_project / "config.yaml"), mode="match", config3d=config_3d, userfeedback=False)
108 changes: 108 additions & 0 deletions tests/test_3d_project_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#
# DeepLabCut Toolbox (deeplabcut.org)
# © A. & M.W. Mathis Labs
# https://github.com/DeepLabCut/DeepLabCut
#
# Please see AUTHORS for contributors.
# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS
#
# Licensed under GNU Lesser General Public License v3.0
#
"""Regression tests: 3D entry points must read a 3D config.yaml without 2D validation.

See https://github.com/DeepLabCut/DeepLabCut/issues/3585. Each entry point is run on a
freshly created 3D project; it must get past reading the config and fail later, on
the data that a fresh project does not have yet.
"""

import shutil
from pathlib import Path

import pytest

from deeplabcut.core.config import read_config_as_dict
from deeplabcut.create_project.new_3d import create_new_project_3d
from deeplabcut.pose_estimation_3d import camera_calibration, plotting3D, triangulation
from deeplabcut.utils import auxiliaryfunctions


@pytest.fixture
def config_3d(tmp_path):
return create_new_project_3d("trial", "tester", num_cameras=2, working_directory=str(tmp_path))


@pytest.mark.parametrize(
"run, error, match",
[
(
lambda cfg, tmp: camera_calibration.calibrate_cameras(cfg, cbrow=8, cbcol=6),
Exception,
"No calibration images found",
),
(
lambda cfg, tmp: camera_calibration.check_undistortion(cfg, cbrow=8, cbcol=6),
FileNotFoundError,
"stereo_params.pickle",
),
(
lambda cfg, tmp: triangulation.triangulate(cfg, str(tmp)),
Exception,
"config_file_camera-1 does not exist",
),
(
lambda cfg, tmp: triangulation.undistort_points(cfg, [tmp / "a.h5", tmp / "b.h5"], None),
FileNotFoundError,
"Dataframe path",
),
(
lambda cfg, tmp: plotting3D.create_labeled_video_3d(cfg, [str(tmp)]),
Exception,
"No corresponding video file",
),
],
ids=["calibrate_cameras", "check_undistortion", "triangulate", "undistort_points", "create_labeled_video_3d"],
)
def test_3d_entry_points_read_3d_config(config_3d, tmp_path, run, error, match):
with pytest.raises(error, match=match):
run(config_3d, tmp_path)


def test_calibrate_cameras_keeps_3d_config_keys(config_3d):
before = auxiliaryfunctions.read_plainconfig(config_3d)

with pytest.raises(Exception, match="No calibration images found"):
camera_calibration.calibrate_cameras(config_3d, cbrow=8, cbcol=6)

assert auxiliaryfunctions.read_plainconfig(config_3d) == before


@pytest.fixture
def moved_config_3d(config_3d, tmp_path):
"""Config of a moved 3D project; its project_path still points at the old location."""
return Path(shutil.move(Path(config_3d).parent, tmp_path / "moved")) / "config.yaml"


def test_read_config_as_dict_repairs_project_path(moved_config_3d):
cfg = read_config_as_dict(moved_config_3d, repair_project_path=True)
assert Path(cfg["project_path"]) == moved_config_3d.parent


@pytest.mark.parametrize(
"run, error, match",
[
(
lambda cfg: camera_calibration.calibrate_cameras(cfg, cbrow=8, cbcol=6),
Exception,
"No calibration images",
),
(
lambda cfg: camera_calibration.check_undistortion(cfg, cbrow=8, cbcol=6),
FileNotFoundError,
r"moved\W+camera_matrix",
),
],
ids=["calibrate_cameras", "check_undistortion"],
)
def test_3d_entry_points_use_moved_project_folder(moved_config_3d, run, error, match):
with pytest.raises(error, match=match):
run(moved_config_3d)
Loading