Source code for poseinterface.io

import copy
import json
import re
from pathlib import Path

import sleap_io as sio
from sleap_io.io import coco
from sleap_io.io.dlc import is_dlc_file

_EMPTY_LABELS_ERROR_MSG = {
    "default": (
        "No annotations could be extracted from the input file. "
        "Please check that the input file contains labeled frames. "
    ),
    "dlc": (
        "Ensure that the paths to the labelled frames are in the "
        "standard DLC project format: "
        "labeled-data / <video-name> / "
        "<filename-with-frame-number>.<extension> "
        "and that the frames files exist."
    ),
}

POSEINTERFACE_FRAME_REGEXP = r"frame-(\d+)"


[docs] def annotations_to_coco( input_path: Path, output_json_path: Path, *, coco_image_filenames: str | list[str] | None = None, coco_visibility_encoding: str = "ternary", ) -> Path: """Export annotations file from a single video to ``poseinterface`` format. Parameters ---------- input_path : pathlib.Path Path to the input annotations file. output_json_path : pathlib.Path Path to save the output ``poseinterface`` COCO JSON file. coco_image_filenames : str | list[str] | None, optional Optional image filenames to use in the ``poseinterface`` COCO JSON. If provided, must be a single string (for single-frame videos) or a list of strings matching the number of labeled frames. If None (default), generates filenames from video filenames and frame indices. coco_visibility_encoding : str, optional Encoding scheme for keypoint visibility in the ``poseinterface`` COCO JSON file. Options are "ternary" (0: not labeled, 1: labeled but not visible, 2: labeled and visible) or "binary" (0: not visible, 1: visible). Default is "ternary". Returns ------- pathlib.Path Path to the saved ``poseinterface`` COCO JSON file. Notes ----- The format of the input annotations file is automatically inferred based on its extension. See :func:`sleap_io.io.main.load_file` for supported formats. See Also -------- sleap_io.io.coco.convert_labels The underlying function used to convert SLEAP labels to COCO format. Example ------- >>> from pathlib import Path >>> from poseinterface.io import annotations_to_coco >>> coco_json_path = annotations_to_coco( ... input_path=Path("path/to/annotations.slp"), ... output_json_path=Path("path/to/annotations_coco.json"), ... ) """ labels = sio.load_file(input_path) # Check if labels object is empty if len(labels.labeled_frames) == 0: error_msg = _EMPTY_LABELS_ERROR_MSG["default"] if is_dlc_file(input_path): error_msg += _EMPTY_LABELS_ERROR_MSG["dlc"] raise ValueError(error_msg) # Check single video if len(labels.videos) > 1: raise ValueError( "The annotations refer to multiple videos " f"(n={len(labels.videos)}). " "Please check that the input file contains annotations " "for a single video only." ) # Generate COCO dict from sleap-io coco_data = coco.convert_labels( labels, image_filenames=coco_image_filenames, visibility_encoding=coco_visibility_encoding, ) # Update image ids to match frame number # uncomment after PR19 # coco_data = _update_image_ids(coco_data) # Save JSON file with open(output_json_path, "w") as f: json.dump(coco_data, f) return output_json_path
def _update_image_ids(input_data: dict) -> dict: """Assigns new image IDs based on the frame number in the filename.""" # Create new dict data = copy.deepcopy(input_data) # Build map old-to-new image IDs and update image id in images list old_to_new_id = {} for img in data["images"]: # map old image_id to new image_id old_img_id = img["id"] new_img_id = _extract_frame_number(img["file_name"]) old_to_new_id[old_img_id] = new_img_id # update image_id in images list img["id"] = new_img_id # Check new image IDs are unique if len(old_to_new_id) != len(set(old_to_new_id.values())): raise ValueError( "Extracted image IDs are not unique. Please check that the frame " "numbers as specified in the filename are unique." ) # Update image_id in annotations list for annot in data["annotations"]: annot["image_id"] = old_to_new_id[annot["image_id"]] return data def _extract_frame_number( filename: str, frame_regexp: str = POSEINTERFACE_FRAME_REGEXP ) -> int | None: """Extract the frame number in the input filename. If no frame number is found, returns None. """ match = re.search(frame_regexp, filename) if match is None: raise ValueError( "No frame number could be extracted from filename " f"{filename}. Please check that the filename contains a " "frame number matching the provided regexp pattern " rf"'{frame_regexp}'." ) return int(match.group(1))