Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: video shipped #27

Merged
merged 3 commits into from
May 24, 2024
Merged
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
10 changes: 4 additions & 6 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@ jobs:
- name: Install extra dependencies
run: mamba install pytest-cov black pytest pytest-cov codecov -cconda-forge

- name: Install pyorerun with pip
run: pip install .

- name: Run the actual tests
run: pytest -v --color=yes --cov-report term-missing --cov=pyorerun tests

- name: Upload coverage to Codecov
run: codecov
if: matrix.label == 'linux-64'

- name: Test installed version of spartacus
run: |
pip install -e . --no-deps
mamba list
if: matrix.label == 'linux-64'
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ We can rerun c3d files and display their original content.
And all also animate biorbd models from the pyomeca organization.

# Installation prerequisites
``` conda install -c conda-forge ezc3d rerun-sdk trimesh numpy biorbd pyomeca tk```
``` conda install -c conda-forge ezc3d rerun-sdk trimesh numpy biorbd pyomeca tk imageio imageio-ffmpeg```

Then, ensure it is accessible in your Python environment by installing the package:

Expand Down
4 changes: 3 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ dependencies:
- biorbd>=1.10.5
- trimesh
- pyomeca
- tk
- tk
- imageio
- imageio-ffmpeg
Binary file added examples/c3d/Running_0002.c3d
Binary file not shown.
Binary file added examples/c3d/Running_0002_Oqus_6_15004.avi
Binary file not shown.
Binary file added examples/c3d/Running_0002_Oqus_9_15003.avi
Binary file not shown.
5 changes: 1 addition & 4 deletions examples/c3d/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import os

import pyorerun as prr

print(os.getcwd())
prr.c3d("example.c3d")
prr.c3d("example.c3d", show_forces=False)
10 changes: 10 additions & 0 deletions examples/c3d/running_gait.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pyorerun as prr

prr.c3d(
"Running_0002.c3d",
show_floor=True,
show_force_plates=True,
show_forces=True,
down_sampled_forces=True,
video=("Running_0002_Oqus_6_15004.avi", "Running_0002_Oqus_9_15003.avi"),
)
9 changes: 8 additions & 1 deletion pyorerun/phase_rerun.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .biorbd_phase import BiorbdRerunPhase
from .timeless import Gravity, Floor, ForcePlate
from .timeless_components import TimelessRerunPhase
from .xp_components import MarkersXp, TimeSeriesQ, ForceVector
from .xp_components import MarkersXp, TimeSeriesQ, ForceVector, Video
from .xp_phase import XpRerunPhase


Expand Down Expand Up @@ -179,6 +179,13 @@ def add_force_data(self, num: int, force_origin: np.ndarray, force_vector: np.nd
ForceVector(name=f"{self.name}", num=num, vector_origins=force_origin, vector_magnitudes=force_vector)
)

def add_video(self, name, video_array: np.ndarray) -> None:
"""Add a video to the phase."""
if video_array.shape[0] != self.t_span.shape[0]:
raise ValueError("The video array and tspan are inconsistent. They must have the same length.")

self.xp_data.add_data(Video(name=f"{self.name}/{name}", video_array=video_array))

def rerun(self, name: str = "animation_phase", init: bool = True, clear_last_node: bool = False) -> None:
if init:
rr.init(f"{name}_{self.phase}", spawn=True)
Expand Down
63 changes: 63 additions & 0 deletions pyorerun/rrc3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any

import ezc3d
import imageio
import numpy as np
import rerun as rr
from pyomeca import Markers as PyoMarkers
Expand All @@ -16,6 +17,8 @@ def rrc3d(
show_force_plates: bool = True,
show_forces: bool = True,
down_sampled_forces: bool = False,
video: str | tuple[str, ...] = None,
video_crop_mode: str = "from_c3d",
marker_trajectories: bool = False,
) -> None:
"""
Expand All @@ -34,6 +37,11 @@ def rrc3d(
down_sampled_forces: bool
If True, down sample the force data to align with the marker data.
If False, the force data will be displayed at their original frame rate, It may get slower when loading the data.
video: str or tuple
If str, the path to the video to display.
If tuple, the first element is the path to the video and the second element is the path to the time data of the video.
video_crop_mode: str
The mode to crop the video. If 'from_c3d', the video will be cropped to the same time span as the c3d file.
marker_trajectories: bool
If True, show the marker trajectories.
"""
Expand All @@ -59,6 +67,8 @@ def rrc3d(

if show_forces:
force_data = get_force_vector(c3d_file)
if len(force_data) == 0:
raise RuntimeError("No force data found in the c3d file. Set show_forces to False.")
if down_sampled_forces:
for i, force in enumerate(force_data):
force["center_of_pressure"], force["force"] = down_sample_force(force, t_span, units)
Expand All @@ -82,6 +92,21 @@ def rrc3d(
square_width = max_xy_coordinate_span_by_markers(pyomarkers)
phase_rerun.add_floor(square_width, height_offset=lowest_corner)

if video is not None:
for i, vid in enumerate(video if isinstance(video, tuple) else [video]):
vid_name = Path(vid).name
vid, time = load_a_video(vid)
vid, time = crop_video(
vid,
time,
video_crop_mode,
first_frame_in_sec=pyomarkers.first_frame / pyomarkers.rate,
last_frame_in_sec=pyomarkers.last_frame / pyomarkers.rate,
)

phase_reruns.append(PhaseRerun(time))
phase_reruns[-1].add_video(vid_name, vid)

multi_phase_rerun = MultiFrameRatePhaseRerun(phase_reruns)
multi_phase_rerun.rerun(filename)

Expand Down Expand Up @@ -199,3 +224,41 @@ def down_sample_force(plateform, t_span, units) -> tuple[np.ndarray, np.ndarray]
)

return down_sampled_center_of_pressure, plateform["force"][:, down_sampled_slice]


def load_a_video(video_path: str) -> tuple[np.ndarray, np.ndarray]:
"""Load a video from a path."""
# Load the video
video_reader = imageio.get_reader(video_path, "ffmpeg")

# Extract frames and convert to numpy ndarray
frames = []
for frame in video_reader:
frames.append(frame)

frame_rate = video_reader.get_meta_data()["fps"]
time = np.linspace(0, len(frames) / frame_rate, len(frames))

return np.array(frames, dtype=np.uint16), time


def crop_video(
Ipuch marked this conversation as resolved.
Show resolved Hide resolved
video: np.ndarray, time, video_crop_mode: str, first_frame_in_sec: float, last_frame_in_sec: float
) -> tuple[np.ndarray, np.ndarray]:

if video_crop_mode == "from_c3d":
closest_time = lambda t: np.argmin(np.abs(t - time))
first_frame = closest_time(first_frame_in_sec)
last_frame = closest_time(last_frame_in_sec)
time = time[first_frame : last_frame + 1] - first_frame_in_sec
video = video[first_frame : last_frame + 1, :, :, :]

return video, time

elif video_crop_mode is not None:
raise NotImplementedError(
f"video_crop_mode={video_crop_mode} is not implemented yet."
"Please use video_crop_mode='from_c3d' or None if already cropped."
)

return video, time
1 change: 1 addition & 0 deletions pyorerun/xp_components/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .force_vector import ForceVector
from .markers import MarkersXp
from .timeseries_q import TimeSeriesQ
from .video import Video
32 changes: 32 additions & 0 deletions pyorerun/xp_components/video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import numpy as np
import rerun as rr

from ..abstract.abstract_class import ExperimentalData


class Video(ExperimentalData):
def __init__(self, name: str, video_array: np.ndarray):
self.name = name
self.video = video_array

@property
def nb_frames(self):
return self.video.shape[0]

def nb_vertical_pixels(self):
return self.video.shape[2]

def nb_horizontal_pixels(self):
return self.video.shape[1]

@property
def nb_components(self):
return 1

def to_rerun(self, frame: int) -> None:
rr.log(
self.name,
rr.Image(
self.video[frame, :, :, :],
),
)
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dependencies = [
"trimesh",
"pyomeca",
"tk",
"imageio",
"imageio-ffmpeg",
# "biorbd" # Not yet available on pypi, use `conda install -c conda-forge biorbd`
]
keywords = ["c3d", "motion capture", "rerun", "biorbd", "pyomeca"]
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"biorbd",
"pyomeca",
"tkinter",
"imageio",
"imageio-ffmpeg",
],
author="Pierre Puchaud",
author_email="[email protected]",
Expand Down
Loading