Skip to content
Merged

Dev #13

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
39 changes: 39 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Docs on Release

on:
release:
types: [published]

permissions:
contents: write

jobs:
deploy-docs:
# Only deploy docs for non-prerelease releases
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install docs dependencies
run: |
python -m pip install -U pip
# Install only the docs extras — no need for the full runtime on the CI runner
pip install 'mkdocs-material >= 9.5' 'mkdocstrings[python] >= 0.25' 'mkdocs-autorefs >= 1.0'
# Install the package itself (needed so mkdocstrings can import and introspect it),
# but skip heavy runtime deps that require native libs or Windows-only packages.
pip install -e . --no-deps

- name: Build and deploy docs
run: |
git config user.name "github-actions"
git config user.email "github-actions@users.noreply.github.com"
mkdocs gh-deploy --force
39 changes: 39 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Unit Tests

on:
push:
pull_request:

jobs:
test:
name: Python ${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

# opencv-python needs libGL on headless Ubuntu runners.
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update -q && sudo apt-get install -y libgl1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install -r requirements-dev.txt

- name: Run unit tests
run: python -m pytest tests/ -q --tb=short

3 changes: 3 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
title: Py3R Pose Tracking
description: Pose estimation and analysis library for the ETH Zurich 3R Hub
theme: minima
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Home
nav_order: 1
---

# Py3R Pose Tracking

Pose estimation library for the ETH Zurich 3R Hub.

## Pages


9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ build-backend = "setuptools.build_meta"

[project]
name = "py3r_pose"
version = "0.7.0"
version = "1.0.0"
authors = [
{ name="Marcel Schmutz", email="mschmut@ethz.ch" },
]
description = "Library for pose estimation and pose analysis in the ETH Zurich 3R Hub"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICEN[CS]E*"]
dependencies = [
"py3r_media @ git+https://github.com/ETHZ-INS/Py3R-Media.git@v0.5.0",
"py3r_media @ git+https://github.com/ETHZ-INS/Py3R-Media.git@v0.7.0",
"numpy >= 2",
"opencv-python >= 4",
"reactivex >= 4.0.0",
"pyyaml >= 6.0",
]

[project.optional-dependencies]
dev = [
"pytest >= 8.0",
]
yolo = [
"ultralytics >= 8.0.0",
"torch>=2",
Expand Down
6 changes: 6 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
py3r_media @ git+https://github.com/ETHZ-INS/Py3R-Media.git@master
numpy>=2
opencv-python>=4
reactivex>=4.0.0
pyyaml>=6.0.0
pytest>=8.0
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ py3r_media @ git+https://github.com/ETHZ-INS/Py3R-Media.git@master
numpy>=2
opencv-python>=4
reactivex>=4.0.0
pyyaml>=6.0.0
pyyaml>=6.0.0
32 changes: 25 additions & 7 deletions src/py3r/pose/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,15 @@ def run_track(args):
job.set_pose_renderer(pose_renderer)
job.set_tracker(tracker)

job.set_live_preview(args.live_preview)
if args.live_preview:
from py3r.pose.cli.image_display import OpenCVImageDisplay
job.set_live_preview_display(OpenCVImageDisplay("Live Preview"))
job.set_quiet(args.quiet)
else:
from py3r.pose.yolo.model.staged_yolo_pose_model import StagedYoloPoseModel
from py3r.pose.core.serialization.dynamic_csv_writer import DynamicPoseCSVWriter
from py3r.media.video.ffmpeg_video_file_writer import FFmpegVideoFileWriter

models = [build_model(model_identifier) for model_identifier in args.model]

if len(models) == 1:
Expand All @@ -217,7 +223,8 @@ def run_track(args):

pose_renderer = build_pose_renderer(args, model.instance_types)

job = PredictionJob(model)
staged_model = StagedYoloPoseModel(model, max_batch=args.batch_size, input_channels=1 if args.grayscale else 3)
job = PredictionJob(staged_model)
job.set_source(source)

if args.original_speed:
Expand All @@ -226,16 +233,27 @@ def run_track(args):
job.set_start_frame(start_frame)
job.set_end_frame(end_frame)

job.set_output_file(output_file)
job.set_visualization_file(vis_file)
if output_file is not None:
output_file.parent.mkdir(parents=True, exist_ok=True)
job.set_pose_writer(DynamicPoseCSVWriter(output_file))

if vis_file is not None:
vis_file.parent.mkdir(parents=True, exist_ok=True)
job.set_video_writer(FFmpegVideoFileWriter(vis_file, size=source.get_size(), fps=source.get_fps(), quality="medium", grayscale=False))

job.set_pose_renderer(pose_renderer)
job.set_batch_size(args.batch_size)

#job.set_label_filter(label_filter)
job.set_tracker(tracker)

job.set_live_preview(args.live_preview)
if args.filter:
from py3r.pose.cli.filter_chain_builder import build_sequential_filter
job.set_pose_filter(build_sequential_filter(args.filter))

if args.live_preview:
from py3r.pose.cli.image_display import OpenCVImageDisplay
job.set_live_preview_display(OpenCVImageDisplay("Live Preview"))
job.set_no_progress(args.quiet)

if len(input_files) > 1 and not args.quiet:
Expand Down Expand Up @@ -346,6 +364,8 @@ def _track_arguments(parser):

parser.add_argument("--render", action="store_true", help="Use existing pose data to create visualization / live preview")

parser.add_argument("--filter", type=str, default=None, help="Filter chain string (e.g. 'smooth | confidence_threshold[0.5]')")

return parser


Expand Down Expand Up @@ -433,8 +453,6 @@ def main():
run_concat([Path(input_file) for input_file in args.input_files], args.output_file, args.reset_frame_index)
elif args.command == "merge":
run_merge([Path(input_file) for input_file in args.input_files], args.output_file)
elif args.command == "stereo_track":
run_stereo_track



Expand Down
74 changes: 74 additions & 0 deletions src/py3r/pose/cli/filter_chain_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import difflib
from dataclasses import dataclass
from typing import List, Optional

from py3r.pose.core.filtering.abc.pose_filter import IPoseFilter
from py3r.pose.core.filtering.instance_scoped_filter import InstanceScopedFilter
from py3r.pose.core.filtering.sequential_filter import SequentialPoseFilter
from py3r.pose.cli.filter_chain_parser import parse_filter_chain
from py3r.pose.cli.filter_registry import FILTER_REGISTRY


@dataclass
class FilterStep:
"""
A fully-built filter step ready for use in a single-stream pipeline.

filter — an InstanceScopedFilter wrapping the inner filter class.

Note: multi-stream routing (input source streams via <s:...>, output
stream naming via @name) is not yet implemented. When added, FilterStep
will gain input_streams and output_stream fields alongside a stream
graph evaluator to replace the current linear SequentialPoseFilter usage.
"""
filter: InstanceScopedFilter


def build_filter_chain(chain_str: str) -> List[FilterStep]:
"""
Parse and build a filter chain from a filter chain string.

Raises FilterSyntaxError on parse errors, ValueError on unknown filter
names or invalid params.
"""
specs = parse_filter_chain(chain_str)
steps: List[FilterStep] = []

for spec in specs:
if spec.name not in FILTER_REGISTRY:
close = difflib.get_close_matches(spec.name, FILTER_REGISTRY.keys(), n=1)
hint = f", did you mean '{close[0]}'?" if close else ""
raise ValueError(f"Unknown filter '{spec.name}'{hint}")

descriptor = FILTER_REGISTRY[spec.name]
resolved = descriptor.resolve_params(spec.params)
inner = descriptor.build(resolved)

subject_types: Optional[List[str]] = None
if spec.subject_selector is not None:
names = spec.subject_selector.instance_type_names
subject_types = names if names else None

context_types: Optional[List[str]] = (
spec.context_selector.instance_type_names
if spec.context_selector is not None
else None
)

steps.append(FilterStep(InstanceScopedFilter(inner, subject_types, context_types)))

return steps


def filters_from_chain(steps: List[FilterStep]) -> List[InstanceScopedFilter]:
"""Return the ordered list of filters from a built chain."""
return [step.filter for step in steps]


def build_sequential_filter(chain_str: str) -> IPoseFilter:
"""
Build a single composed pose filter object for the current single-stream implementation.

The returned object can be passed directly to PredictionJob / RenderJob.
"""
return SequentialPoseFilter(filters_from_chain(build_filter_chain(chain_str)))
Loading
Loading