Skip to content

deeplabcut.core.config.project_config

Project configuration classes for DeepLabCut pose estimation models.

Classes:

Name Description
ProjectConfig

Complete project configuration.

ProjectConfig

Bases: DLCVersionedConfig

Complete project configuration.

Mirrors the structure of the project config.yaml (and metadata in pose config). Field names match the old dictionary keys for round-trip compatibility.

Attributes:

Name Type Description
Task str

Project task identifier (do not edit).

scorer str

Scorer name (do not edit).

date str

Project date (do not edit).

multianimalproject bool

Whether the project is multi-animal.

identity bool | None

Whether identity tracking is enabled (project config.yaml key).

project_path Path

Path to the DeepLabCut project.

pose_config_path Path | None

Path to the pose configuration file (metadata only).

engine Literal['pytorch', 'tensorflow']

Default DeepLabCut engine (e.g. pytorch).

video_sets dict[str, Any]

Video set configuration.

bodyparts UniqueStrList | Literal['MULTI!']

List of body parts.

individuals UniqueStrList

List of individual animal identities (multi-animal).

uniquebodyparts UniqueStrList

List of unique body parts (multi-animal project key).

multianimalbodyparts UniqueStrList

List of multi-animal body parts (multi-animal key).

start Fraction

Fraction of video to start extracting frames.

stop Fraction

Fraction of video to stop extracting frames.

numframes2pick NonNegativeInt

Number of frames to pick for labeling.

skeleton list[BodypartPair]

Skeleton connectivity for plotting.

skeleton_color str

Skeleton color for plotting.

pcutoff Fraction

Confidence cutoff for plotting.

dotsize NonNegativeInt

Dot size for visualization.

alphavalue Fraction

Alpha value for visualization.

colormap str

Colormap for visualization.

TrainingFraction list[Fraction]

Training fractions for dataset splits.

iteration NonNegativeInt | None

Training iteration.

default_net_type str

Default network architecture.

default_augmenter str | None

Default data augmenter.

default_track_method str | None

Default tracking method.

snapshotindex Literal['all'] | int

Snapshot index for evaluation.

detector_snapshotindex int

Detector snapshot index.

batch_size StrictPositiveInt

Training batch size.

detector_batch_size StrictPositiveInt

Detector batch size.

cropping bool

Whether cropping is enabled for analysis.

x1 NonNegativeInt | None

Cropping x1 coordinate.

x2 NonNegativeInt | None

Cropping x2 coordinate.

y1 NonNegativeInt | None

Cropping y1 coordinate.

y2 NonNegativeInt | None

Cropping y2 coordinate.

corner2move2 list[NonNegativeInt] | None

Refinement corner configuration.

move2corner bool | None

Refinement move-to-corner flag.

SuperAnimalConversionTables dict[str, Any] | None

Conversion tables for SuperAnimal weights.

Methods:

Name Description
validate_project_path

Repair project_path from yaml location; optionally persist.

Source code in deeplabcut/core/config/project_config.py
class ProjectConfig(DLCVersionedConfig):
    """Complete project configuration.

    Mirrors the structure of the project config.yaml (and metadata in pose config).
    Field names match the old dictionary keys for round-trip compatibility.

    Attributes:
        Task: Project task identifier (do not edit).
        scorer: Scorer name (do not edit).
        date: Project date (do not edit).
        multianimalproject: Whether the project is multi-animal.
        identity: Whether identity tracking is enabled (project config.yaml key).
        project_path: Path to the DeepLabCut project.
        pose_config_path: Path to the pose configuration file (metadata only).
        engine: Default DeepLabCut engine (e.g. pytorch).
        video_sets: Video set configuration.
        bodyparts: List of body parts.
        individuals: List of individual animal identities (multi-animal).
        uniquebodyparts: List of unique body parts (multi-animal project key).
        multianimalbodyparts: List of multi-animal body parts (multi-animal key).
        start: Fraction of video to start extracting frames.
        stop: Fraction of video to stop extracting frames.
        numframes2pick: Number of frames to pick for labeling.
        skeleton: Skeleton connectivity for plotting.
        skeleton_color: Skeleton color for plotting.
        pcutoff: Confidence cutoff for plotting.
        dotsize: Dot size for visualization.
        alphavalue: Alpha value for visualization.
        colormap: Colormap for visualization.
        TrainingFraction: Training fractions for dataset splits.
        iteration: Training iteration.
        default_net_type: Default network architecture.
        default_augmenter: Default data augmenter.
        default_track_method: Default tracking method.
        snapshotindex: Snapshot index for evaluation.
        detector_snapshotindex: Detector snapshot index.
        batch_size: Training batch size.
        detector_batch_size: Detector batch size.
        cropping: Whether cropping is enabled for analysis.
        x1: Cropping x1 coordinate.
        x2: Cropping x2 coordinate.
        y1: Cropping y1 coordinate.
        y2: Cropping y2 coordinate.
        corner2move2: Refinement corner configuration.
        move2corner: Refinement move-to-corner flag.
        SuperAnimalConversionTables: Conversion tables for SuperAnimal weights.
    """

    # Project definitions (do not edit)
    Task: str = Field(default="", json_schema_extra={"comment": "Project definitions (do not edit)"})
    scorer: str = ""
    date: str = ""
    multianimalproject: bool = False
    identity: bool | None = Field(default=None, json_schema_extra={"aliases": ["with_identity"]})

    # Project path
    project_path: Path = Field(
        default_factory=Path,
        json_schema_extra={"comment": "\nProject path (change when moving around)"},
    )
    pose_config_path: Path | None = None

    # Engine
    engine: Literal["pytorch", "tensorflow"] = Field(
        default="pytorch",
        json_schema_extra={
            "comment": "\nDefault DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow)"
        },
    )

    # Annotation dataset configuration (and individual video cropping parameters)
    video_sets: dict[str, Any] = Field(
        default_factory=dict,
        json_schema_extra={"comment": "\nAnnotation data set configuration (and individual video cropping parameters)"},
    )
    # VV TODO @deruyter92 2026-01-30: following the old original config.yaml template for now. VV
    # VV We should change this to a list[str] in the future. VV
    bodyparts: UniqueStrList | Literal["MULTI!"] = Field(default_factory=list)

    # TODO @deruyter92 2026-02-06: The current pipeline requires at least one individual defined in the
    # default configuration. This will be removed in the future.
    individuals: UniqueStrList = Field(default_factory=lambda: ["individual_1"])
    uniquebodyparts: UniqueStrList = Field(default_factory=list, json_schema_extra={"aliases": ["unique_bodyparts"]})
    multianimalbodyparts: UniqueStrList = Field(default_factory=list)  # multi-animal project key

    # Fraction of video to start/stop when extracting frames for labeling/refinement
    start: Fraction = Field(
        default=0.0,
        json_schema_extra={
            "comment": "\nFraction of video to start/stop when extracting frames for labeling/refinement"
        },
    )
    stop: Fraction = 1.0
    numframes2pick: NonNegativeInt = 20

    # Plotting configuration
    skeleton: list[BodypartPair] = Field(
        default_factory=list,
        json_schema_extra={"comment": "\nPlotting configuration"},
    )
    skeleton_color: str = "black"
    pcutoff: Fraction = 0.6
    dotsize: NonNegativeInt = 12
    alphavalue: Fraction = 0.7
    colormap: str = "rainbow"

    # Training, evaluation and analysis configuration
    TrainingFraction: list[Fraction] = Field(
        default_factory=lambda: [0.95],
        json_schema_extra={"comment": "\nTraining,Evaluation and Analysis configuration"},
    )
    iteration: NonNegativeInt | None = None
    default_net_type: str = "resnet_50"
    default_augmenter: str | None = None
    default_track_method: str | None = None
    snapshotindex: Literal["all"] | int = "all"
    detector_snapshotindex: int = -1
    batch_size: StrictPositiveInt = 8
    detector_batch_size: StrictPositiveInt = 1

    # Cropping parameters (for analysis and outlier frame detection)
    cropping: bool = Field(
        default=False,
        json_schema_extra={"comment": "\nCropping Parameters (for analysis and outlier frame detection)"},
    )
    x1: NonNegativeInt | None = Field(
        default=None,
        json_schema_extra={"comment": "if cropping is true for analysis, then set the values here:"},
    )
    x2: NonNegativeInt | None = None
    y1: NonNegativeInt | None = None
    y2: NonNegativeInt | None = None

    # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage)
    corner2move2: list[NonNegativeInt] | None = Field(
        default=None,
        json_schema_extra={
            "comment": (
                "\nRefinement configuration (parameters from annotation dataset "
                "configuration also relevant in this stage)"
            )
        },
    )
    move2corner: bool | None = None

    # Conversion tables to fine-tune SuperAnimal weights
    SuperAnimalConversionTables: dict[str, Any] | None = Field(
        default=None,
        json_schema_extra={"comment": "\nConversion tables to fine-tune SuperAnimal weights"},
    )

    # TODO @deruyter92 2026-02-06: These parameters are no longer used in the new pipeline.
    # We should remove them in config schema v1. They are needed now to support reading old configs.
    resnet: int | None = Field(
        default=None,
        json_schema_extra={
            "comment": "\nThese are very old parameters that are no longer used They are simply ignored."
        },
    )
    croppedtraining: bool | None = None

    @property
    def bodyparts_list(self) -> list[str]:
        # Animal-count agnostic; Always return a list (never "MULTI!", None, etc.)
        if self.multianimalproject:
            return list(self.multianimalbodyparts)
        if self.bodyparts == "MULTI!":
            raise ValueError("bodyparts must be a list of strings when multianimalproject is False, got 'MULTI!'")
        return list(self.bodyparts)

    @property
    def config_yaml_path(self) -> Path:
        return self.project_path / "config.yaml"

    def validate_project_path(self, *, yaml_path: str | Path | None = None, write: bool = False) -> Self:
        """Repair project_path from yaml location; optionally persist."""
        path = Path(yaml_path) if yaml_path is not None else self.config_yaml_path
        if not path.is_file():
            raise FileNotFoundError(f"config.yaml not found: {path}")
        self._post_yaml_load_updates(yaml_path=path)
        if write and "project_path" in self.dirty_fields:
            self.to_yaml(path, log_changes=True, mark_clean=True)
        return self

    @classmethod
    def from_any(cls, config: Self | dict | str | Path, *, repair_path: bool = False) -> Self:
        cfg = super().from_any(config)
        if repair_path:
            cfg.validate_project_path(yaml_path=config if isinstance(config, (str, Path)) else None, write=True)
        return cfg

    def _post_yaml_load_updates(self, *, yaml_path: Path) -> None:
        """
        Override method for post-yaml load updates. Called automatically by from_yaml().
        These are logged but not written to disk -- call to_yaml() explicitly if needed.
        """
        project_path = yaml_path.parent
        if project_path.absolute() != self.project_path.absolute():
            old = self.project_path
            self.project_path = project_path
            self.record_change_note(
                "project_path",
                f"project_path updated: {old} -> {project_path} (resolved from YAML location when reading config.yaml)",
            )

    @model_validator(mode="before")
    @classmethod
    def normalize_legacy_empty_values(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        data = dict(data)

        # Some old configs used empty strings for unset fields
        for fieldname in ("skeleton", "TrainingFraction", "video_sets", "bodyparts"):
            if data.get(fieldname) == "":
                data.pop(fieldname)

        # NOTE @deruyter92 2026-06-15: This should be removed in v1.
        if data.get("multianimalproject") and not data.get("bodyparts"):
            data["bodyparts"] = "MULTI!"

        return data

    @model_validator(mode="after")
    def validate_start_before_stop(self) -> Self:
        less_than(self.start, self.stop, name="start", threshold_name="stop")
        return self

    @model_validator(mode="after")
    def validate_bodyparts_single_animal(self) -> Self:
        if not self.multianimalproject and self.bodyparts == "MULTI!":
            raise ValueError("bodyparts must be a list of strings when multianimalproject is False, got 'MULTI!'")

        # TODO @deruyter92 2026-06-15: This sentinel should be removed in v1.
        elif self.multianimalproject and self.bodyparts != "MULTI!":
            raise ValueError(f"bodyparts must be 'MULTI!' when multianimalproject is True, got {self.bodyparts}")

        return self

    @model_validator(mode="after")
    def validate_cropping_bounds(self) -> Self:
        if self.cropping and any(value is None for value in (self.x1, self.x2, self.y1, self.y2)):
            raise ValueError("When cropping is enabled, x1, x2, y1, and y2 must all be set")
        validate_crop_bounds(x1=self.x1, x2=self.x2, y1=self.y1, y2=self.y2)
        return self

validate_project_path

validate_project_path(*, yaml_path: str | Path | None = None, write: bool = False) -> Self

Repair project_path from yaml location; optionally persist.

Source code in deeplabcut/core/config/project_config.py
def validate_project_path(self, *, yaml_path: str | Path | None = None, write: bool = False) -> Self:
    """Repair project_path from yaml location; optionally persist."""
    path = Path(yaml_path) if yaml_path is not None else self.config_yaml_path
    if not path.is_file():
        raise FileNotFoundError(f"config.yaml not found: {path}")
    self._post_yaml_load_updates(yaml_path=path)
    if write and "project_path" in self.dirty_fields:
        self.to_yaml(path, log_changes=True, mark_clean=True)
    return self