Skip to content

dlc

sleap_io.io.dlc

This module handles direct I/O operations for working with DeepLabCut (DLC) files.

Functions:

Name Description
is_dlc_file

Check if file is a DLC CSV file.

load_dlc

Load DeepLabCut annotations from CSV file.

is_dlc_file(filename)

Check if file is a DLC CSV file.

Parameters:

Name Type Description Default
filename Union[str, Path]

Path to file to check.

required

Returns:

Type Description
bool

True if file appears to be a DLC CSV file.

Source code in sleap_io/io/dlc.py
def is_dlc_file(filename: Union[str, Path]) -> bool:
    """Check if file is a DLC CSV file.

    Args:
        filename: Path to file to check.

    Returns:
        True if file appears to be a DLC CSV file.
    """
    try:
        # Read first few lines as raw text to check for DLC structure
        with open(filename, "r") as f:
            lines = [f.readline().strip() for _ in range(4)]

        # Join all lines to search for DLC patterns
        content = "\n".join(lines).lower()

        # Check for DLC's characteristic patterns
        has_scorer = "scorer" in content
        has_coords = "coords" in content
        has_xy = "x" in content and "y" in content
        has_bodyparts = "bodyparts" in content or any(
            part in content for part in ["animal", "individual"]
        )

        return has_scorer and has_coords and has_xy and has_bodyparts

    except Exception:
        return False

load_dlc(filename, video_search_paths=None, **kwargs)

Load DeepLabCut annotations from CSV file.

Parameters:

Name Type Description Default
filename Union[str, Path]

Path to DLC CSV file.

required
video_search_paths Optional[list[Union[str, Path]]]

List of paths to search for video files.

None
**kwargs

Additional arguments (unused).

{}

Returns:

Type Description
Labels

Labels object with loaded data.

Source code in sleap_io/io/dlc.py
def load_dlc(
    filename: Union[str, Path],
    video_search_paths: Optional[list[Union[str, Path]]] = None,
    **kwargs,
) -> Labels:
    """Load DeepLabCut annotations from CSV file.

    Args:
        filename: Path to DLC CSV file.
        video_search_paths: List of paths to search for video files.
        **kwargs: Additional arguments (unused).

    Returns:
        Labels object with loaded data.
    """
    filename = Path(filename)

    # Try reading first few rows to determine format
    try:
        # Try multi-animal format first (header rows 1-3, skipping scorer row)
        df = pd.read_csv(filename, header=[1, 2, 3], nrows=2)
        is_multianimal = df.columns[0][0] == "individuals"
    except Exception:
        # Fall back to single-animal format
        is_multianimal = False

    # Read full file with appropriate header levels
    if is_multianimal:
        # Multi-animal format: skip scorer row, use individuals/bodyparts/coords
        df = pd.read_csv(filename, header=[1, 2, 3], index_col=0)
    else:
        # Single-animal format: use scorer/bodyparts/coords
        df = pd.read_csv(filename, header=[0, 1, 2], index_col=0)

    # Parse structure based on format
    if is_multianimal:
        skeleton, tracks = _parse_multi_animal_structure(df)
    else:
        skeleton = _parse_single_animal_structure(df)
        tracks = []

    # First, group all image paths by their video directory
    video_image_paths = {}
    frame_map = {}  # Maps image path to frame index

    for idx in df.index:
        img_path = str(idx)
        frame_idx = _extract_frame_index(img_path)
        frame_map[img_path] = frame_idx

        # Extract video name from path
        # e.g., "labeled-data/video/img000.png" -> "video"
        path_parts = Path(img_path).parts
        if len(path_parts) >= 2 and path_parts[0] == "labeled-data":
            video_name = path_parts[1]
        else:
            video_name = Path(img_path).parent.name or "default"

        if video_name not in video_image_paths:
            video_image_paths[video_name] = []
        video_image_paths[video_name].append(img_path)

    # Create one Video object per video directory
    videos = {}
    for video_name, image_paths in video_image_paths.items():
        # Sort image paths to ensure consistent ordering
        sorted_paths = sorted(image_paths, key=lambda p: frame_map[p])

        # Find the actual image files
        actual_image_files = []
        for img_path in sorted_paths:
            # First try the full path from CSV
            full_path = filename.parent / img_path
            if full_path.exists():
                actual_image_files.append(str(full_path))
            else:
                # Try just the filename in the same directory as the CSV
                img_name = Path(img_path).name
                simple_path = filename.parent / img_name
                if simple_path.exists():
                    actual_image_files.append(str(simple_path))
                else:
                    # Try going up one directory from CSV location
                    # (CSV in subdir references parent/subdir/img.png)
                    parent_path = filename.parent.parent / img_path
                    if parent_path.exists():
                        actual_image_files.append(str(parent_path))

        # Only create video if we found actual images
        if actual_image_files:
            videos[video_name] = Video.from_filename(actual_image_files)

    # Parse the actual data rows and create labeled frames
    labeled_frames = []
    for idx, row in df.iterrows():
        # Get image path from index
        img_path = str(idx)
        frame_idx = _extract_frame_index(img_path)

        # Determine which video this frame belongs to
        path_parts = Path(img_path).parts
        if len(path_parts) >= 2 and path_parts[0] == "labeled-data":
            video_name = path_parts[1]
        else:
            video_name = Path(img_path).parent.name or "default"

        # Skip if we don't have a video for this frame
        if video_name not in videos:
            continue

        # Parse instances for this frame
        if is_multianimal:
            instances = _parse_multi_animal_row(row, skeleton, tracks)
        else:
            instances = _parse_single_animal_row(row, skeleton)

        if instances:
            # Get the index of this image within its video
            sorted_video_paths = sorted(
                video_image_paths[video_name], key=lambda p: frame_map[p]
            )
            video_frame_idx = sorted_video_paths.index(img_path)
            labeled_frames.append(
                LabeledFrame(
                    video=videos[video_name],
                    frame_idx=video_frame_idx,
                    instances=instances,
                )
            )

    unique_videos = list(videos.values())

    return Labels(
        labeled_frames=labeled_frames,
        videos=unique_videos,
        tracks=tracks,
        skeletons=[skeleton] if skeleton.nodes else [],
    )