Skip to content
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
11 changes: 10 additions & 1 deletion PySpotObserver/QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
pip install -r requirements.txt
```

2. **Install package in development mode:**
2. **Optional vision pipeline support:**
```bash
pip install -e ".[vision]"
```

The dependency source of truth is `setup.py`; `requirements.txt` installs the development extra without duplicating package lists.

3. **Minimal install without dev tools (alternative to step 1):**
```bash
pip install -e .
```
Expand Down Expand Up @@ -215,4 +222,6 @@ logging.basicConfig(

**Import errors**: Make sure all dependencies are installed: `pip install -r requirements.txt`

**Vision pipeline errors**: Install `pip install -e ".[vision]"` and set `vision_model_path` in config, pass `--vision-model-path`, or set `PYSPOTOBSERVER_VISION_MODEL`

**No images received**: Check that cameras are not already in use by another client
13 changes: 12 additions & 1 deletion PySpotObserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A clean, Pythonic interface for streaming camera data from Boston Dynamics Spot
- **YAML Configuration**: Load settings from config files or pass as parameters
- **Multi-stream**: Support for multiple concurrent camera streams
- **Thread-safe**: Background streaming with thread-safe image buffering
- **Optional Vision Pipeline**: ONNX Runtime inference can be enabled explicitly without making it a base dependency

## Installation

Expand All @@ -27,6 +28,14 @@ pip install -e .
pip install -e ".[dev]"
```

### With optional vision pipeline support

```bash
pip install -e ".[vision]"
```

`requirements.txt` delegates to `setup.py` for a development install, so dependency declarations stay in one place.

## Quick Start

### Basic Synchronous Usage
Expand Down Expand Up @@ -97,6 +106,7 @@ username: ""
password: ""
image_buffer_size: 5
image_quality_percent: 100.0
request_timeout_seconds: 10.0
```

## Architecture
Expand Down Expand Up @@ -165,7 +175,7 @@ The Python implementation differs in these ways:
- **No CUDA**: Uses CPU-only NumPy arrays instead of GPU memory
- **FIFO Queue**: Simpler queue.Queue instead of custom circular buffer
- **Threading**: Python threading instead of C++ jthread
- **No ML Pipeline**: Focuses on camera streaming only (no inference)
- **Optional ML Pipeline**: Inference is available through an optional ONNX Runtime extra
- **Simplified**: Removes Unity plugin and DLL export complexity

## Requirements
Expand All @@ -175,6 +185,7 @@ The Python implementation differs in these ways:
- NumPy
- OpenCV (opencv-python)
- PyYAML
- ONNX Runtime GPU (`onnxruntime-gpu`) only when installing the `vision` extra

## Contributing

Expand Down
4 changes: 3 additions & 1 deletion PySpotObserver/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ Install the package and dependencies first:

```bash
pip install -r requirements.txt
pip install -e .
```

For `--vision-pipeline`, also install `pip install -e ".[vision]"` and provide a model path with `--vision-model-path`, config, or `PYSPOTOBSERVER_VISION_MODEL`.

## Configuration

The examples load `examples/config_example.yaml` by default. Fill in at least:
Expand All @@ -37,6 +38,7 @@ python examples/basic_streaming.py --robot-ip 192.168.80.3 --username <user> --p
- asynchronous streaming with `--async-mode`
- one or two stream configurations, mirrored across one or two robots
- optional OpenCV display
- optional ONNX vision pipeline with `--vision-pipeline`
- optional timing summaries with `--print-timing`

Show the full CLI:
Expand Down
30 changes: 26 additions & 4 deletions PySpotObserver/examples/basic_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
from contextlib import AsyncExitStack, ExitStack
from dataclasses import dataclass
import logging
from pathlib import Path
import time
from typing import Sequence


import cv2
import numpy as np

Expand Down Expand Up @@ -123,6 +125,22 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Print per-stream timing summary at the end of the run.",
)
parser.add_argument(
"--vision-pipeline",
action="store_true",
help="Run the vision pipeline on the Spot outputs"
)
parser.add_argument(
"--vision-model-path",
type=Path,
help="ONNX model path for --vision-pipeline. Overrides config and environment.",
)
parser.add_argument(
"--vision-provider",
action="append",
dest="vision_providers",
help="ONNX Runtime provider to request. Repeat to set provider preference order.",
)
return parser.parse_args()


Expand Down Expand Up @@ -259,7 +277,10 @@ def run_sync(args: argparse.Namespace, specs: list[StreamSpec]) -> int:
stream = streams[spec.label]

fetch_start = time.perf_counter()
rgb_images, depth_images = stream.get_current_images(timeout=args.timeout)
rgb_images, depth_images = stream.get_current_images(
timeout=args.timeout,
run_pipeline=args.vision_pipeline,
)
fetch_elapsed = time.perf_counter() - fetch_start

display_elapsed = 0.0
Expand All @@ -282,9 +303,10 @@ def run_sync(args: argparse.Namespace, specs: list[StreamSpec]) -> int:
return 0


async def fetch_stream_async(stream, timeout: float) -> FetchResult:
async def fetch_stream_async(stream, timeout: float, vision_pipeline: bool) -> FetchResult:
fetch_start = time.perf_counter()
rgb_images, depth_images = await stream.async_get_current_images(timeout=timeout)
rgb_images, depth_images = await stream.async_get_current_images(timeout=timeout, run_pipeline=vision_pipeline)

return FetchResult(
rgb_images=rgb_images,
depth_images=depth_images,
Expand All @@ -310,7 +332,7 @@ async def run_async(args: argparse.Namespace, specs: list[StreamSpec]) -> int:
start_time = time.perf_counter()
while time.perf_counter() - start_time < args.duration:
results = await asyncio.gather(
*(fetch_stream_async(streams[spec.label], args.timeout) for spec in specs)
*(fetch_stream_async(streams[spec.label], args.timeout, args.vision_pipeline) for spec in specs)
)

should_quit = False
Expand Down
4 changes: 4 additions & 0 deletions PySpotObserver/examples/common_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def build_config_from_args(args: argparse.Namespace) -> SpotConfig:
config.password = args.password
if hasattr(args, "image_buffer_size") and args.image_buffer_size is not None:
config.image_buffer_size = args.image_buffer_size
if hasattr(args, "vision_model_path") and args.vision_model_path is not None:
config.vision_model_path = str(args.vision_model_path)
if hasattr(args, "vision_providers") and args.vision_providers:
config.vision_providers = args.vision_providers

if not config.robot_ip:
raise ValueError("Robot IP must be set in --config or with --robot-ip.")
Expand Down
6 changes: 6 additions & 0 deletions PySpotObserver/examples/config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ image_buffer_size: 5
image_quality_percent: 100.0
request_timeout_seconds: 10.0

# Optional vision pipeline settings
# vision_model_path: "C:/path/to/model.onnx"
# vision_providers:
# - "CUDAExecutionProvider"
# - "CPUExecutionProvider"

# Advanced settings
sdk_name: "PySpotObserver"
connection_retry_attempts: 3
Expand Down
7 changes: 5 additions & 2 deletions PySpotObserver/pyspotobserver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
"""

from .config import SpotConfig, CameraType
from .connection import SpotConnection
from .camera_stream import SpotCamStream
from .connection import SpotAuthenticationError, SpotConnection, SpotConnectionError
from .camera_stream import SpotCamStream, SpotCamStreamError

__version__ = "0.1.0"
__all__ = [
"SpotConfig",
"CameraType",
"SpotConnection",
"SpotConnectionError",
"SpotAuthenticationError",
"SpotCamStream",
"SpotCamStreamError",
]
Loading