Skip to content

Commit dc09c57

Browse files
committed
Rename _to_poseinterface. Add video conversion bits
1 parent 4517f91 commit dc09c57

File tree

4 files changed

+124
-9
lines changed

4 files changed

+124
-9
lines changed

examples/SWC-plusmaze_to_benchmark.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import shutil
1414
from pathlib import Path
1515

16-
from poseinterface.io import annotations_to_coco
16+
from poseinterface.io import annotations_to_poseinterface
1717

1818
# %%
1919
# Background
@@ -102,7 +102,7 @@
102102
# Here we use the :func:`annotations_to_coco` function from `poseinterface.io`
103103
# which wraps around `sleap_io` functionality to perform the conversion.
104104

105-
annotations_to_coco(
105+
annotations_to_poseinterface(
106106
input_path=source_annotations_path,
107107
output_json_path=target_annotations_path,
108108
sub_id=subject_id,

poseinterface/io.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import copy
22
import json
3+
import logging
34
import re
5+
import shutil
46
from pathlib import Path
57
from typing import Literal
68

79
import sleap_io as sio
810
from sleap_io.io import coco
11+
from sleap_io.io.cli import _get_video_encoding_info, _is_ffmpeg_available
912
from sleap_io.io.dlc import is_dlc_file
1013

1114
_EMPTY_LABELS_ERROR_MSG = {
@@ -24,8 +27,21 @@
2427

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

30+
# We support sleap's MediaVideo files
31+
EXPECTED_SUFFIX = ".mp4"
32+
EXPECTED_ENCODING = {
33+
"pixelformat": "yuv420p",
34+
"codec": "h264", # codec name
35+
}
36+
REENCODING_PARAMS = {
37+
**EXPECTED_ENCODING,
38+
"codec": "libx264", # overwrite with encoder to use
39+
"crf": 25,
40+
"preset": "superfast",
41+
}
2742

28-
def annotations_to_coco(
43+
44+
def annotations_to_poseinterface(
2945
input_path: Path,
3046
output_json_path: Path,
3147
*,
@@ -246,3 +262,102 @@ def _pad_integers_to_same_width(input: list[int]) -> list[str]:
246262
width = len(str(max(input)))
247263
padded_numbers = [str(number).zfill(width) for number in input]
248264
return padded_numbers
265+
266+
267+
def video_to_poseinterface(
268+
input_video: Path | str,
269+
output_video_dir: Path | str,
270+
*,
271+
sub_id: str,
272+
ses_id: str,
273+
cam_id: str,
274+
) -> Path:
275+
"""Reencode and rename video."""
276+
# Check if ffmpeg is available
277+
_check_ffmpeg()
278+
279+
# Compute output_video_path
280+
output_video = (
281+
Path(output_video_dir) / f"sub-{sub_id}_ses-{ses_id}_cam-{cam_id}.mp4"
282+
)
283+
# Ensure parent directories exist
284+
Path(output_video_dir).mkdir(parents=True, exist_ok=True)
285+
286+
# Check if reencoding is required
287+
if not _needs_reencoding(input_video):
288+
# If not, copy file and rename
289+
shutil.copy(input_video, output_video)
290+
else:
291+
# Else, reencode video and rename
292+
_reencode_video(input_video, output_video)
293+
294+
return output_video
295+
296+
297+
def _check_ffmpeg():
298+
"Check FFMPEG availability"
299+
sio.set_default_video_plugin("ffmpeg")
300+
if not _is_ffmpeg_available():
301+
raise RuntimeError("ffmpeg is required but not found")
302+
303+
304+
def _needs_reencoding(input_video_path: str | Path) -> bool:
305+
"""Check if reencoding is required."""
306+
input_video_path = Path(input_video_path)
307+
logging.info(f"Input video: {input_video_path}")
308+
309+
# Check if suffix is mp4
310+
if input_video_path.suffix.lower() != EXPECTED_SUFFIX:
311+
return True
312+
313+
# Check codec and pixelformat
314+
encoding = _get_codec_pixelformat(input_video_path)
315+
if encoding != EXPECTED_ENCODING:
316+
logging.warning(
317+
f"Video encoding {encoding} does not match "
318+
f"expected {EXPECTED_ENCODING}. Please reencode "
319+
"using the `reencode_video()` function."
320+
)
321+
return True
322+
return False
323+
324+
325+
def _get_codec_pixelformat(input_video_path: str | Path) -> dict:
326+
"""Get video encoding parameters as dictionary.
327+
328+
It wraps sleap-io's _get_video_encoding_info, which
329+
uses `ffmpeg -i` to extract metadata without requiring ffprobe in PATH.
330+
331+
`_get_video_encoding_info` returns a VideoEncodingInfo object
332+
with attributes:
333+
codec: Video codec name (e.g., "h264", "hevc").
334+
codec_profile: Codec profile (e.g., "Main", "High").
335+
pixel_format: Pixel format (e.g., "yuv420p").
336+
bitrate_kbps: Bitrate in kilobits per second.
337+
fps: Frames per second.
338+
gop_size: Group of pictures size (keyframe interval).
339+
container: Container format (e.g., "mov", "avi").
340+
341+
"""
342+
info = _get_video_encoding_info(input_video_path)
343+
return {
344+
"codec": info.codec,
345+
"pixelformat": info.pixel_format,
346+
}
347+
348+
349+
def _reencode_video(
350+
input_video_path: str | Path,
351+
output_video_path: str | Path,
352+
) -> Path:
353+
"""Reencode video to default format."""
354+
# Read and save reencoded video
355+
video = sio.load_video(Path(input_video_path))
356+
reencoded_video_path = sio.save_video(
357+
video,
358+
filename=output_video_path,
359+
fps=video.fps,
360+
**REENCODING_PARAMS,
361+
)
362+
logging.info(f"Re-encoded video saved to {reencoded_video_path}")
363+
return reencoded_video_path

tests/test_integration/test_io.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from poseinterface.io import annotations_to_coco
3+
from poseinterface.io import annotations_to_poseinterface
44

55

66
@pytest.mark.parametrize(
@@ -18,6 +18,6 @@ def test_annotations_to_coco(input_path, tmp_path, test_ids, request):
1818
input_path = request.getfixturevalue(input_path)
1919
output_json_path = tmp_path / "output.json"
2020

21-
annotations_to_coco(input_path, output_json_path, **test_ids)
21+
annotations_to_poseinterface(input_path, output_json_path, **test_ids)
2222

2323
assert output_json_path.exists()

tests/test_unit/test_io.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
_generate_poseinterface_filenames,
1313
_pad_integers_to_same_width,
1414
_update_image_ids,
15-
annotations_to_coco,
15+
annotations_to_poseinterface,
1616
)
1717

1818

@@ -35,7 +35,7 @@ def test_annotations_to_coco(
3535
# Run function to test
3636
input_csv = tmp_path / "input.csv"
3737
output_path = tmp_path / "output.json"
38-
result = annotations_to_coco(
38+
result = annotations_to_poseinterface(
3939
input_csv,
4040
output_path,
4141
**test_ids,
@@ -83,7 +83,7 @@ def test_annotations_to_coco_invalid(
8383
with pytest.raises(
8484
ValueError, match=_EMPTY_LABELS_ERROR_MSG[error_message]
8585
):
86-
annotations_to_coco(
86+
annotations_to_poseinterface(
8787
input_file,
8888
tmp_path / "output.json",
8989
**test_ids,
@@ -111,7 +111,7 @@ def test_annotations_to_coco_not_single_video(
111111
ValueError,
112112
match=(r"The annotations refer to multiple videos.*Please check .*"),
113113
):
114-
annotations_to_coco(
114+
annotations_to_poseinterface(
115115
tmp_path / "input.csv",
116116
tmp_path / "output.json",
117117
**test_ids,

0 commit comments

Comments
 (0)