chore(metadata): update beads and sisyphus planning artifacts

This commit is contained in:
2026-02-07 04:16:41 +00:00
parent ddb7054f96
commit cdc4f9eec4
28 changed files with 633 additions and 220 deletions
+45
View File
@@ -0,0 +1,45 @@
# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
sync-state.json
last-touched
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
# Legacy database files
db.sqlite
bd.db
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
sync_base.jsonl
export-state/
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
# They would override fork protection in .git/info/exclude, allowing
# contributors to accidentally commit upstream issue databases.
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
# are tracked by git by default since no pattern above ignores them.
+81
View File
@@ -0,0 +1,81 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --status in_progress
bd update <issue-id> --status done
# Sync with git remote
bd sync
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Always in sync**: Auto-syncs with your commits
## Why Beads?
**AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Automatic sync with git commits
- Branch-aware issue tracking
- Intelligent JSONL merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought*
+67
View File
@@ -0,0 +1,67 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: load from JSONL, no SQLite, write back after each command
# When true, bd will use .beads/issues.jsonl as the source of truth
# instead of SQLite database
# no-db: false
# Disable daemon for RPC communication (forces direct database access)
# no-daemon: false
# Disable auto-flush of database to JSONL after mutations
# no-auto-flush: false
# Disable auto-import from JSONL when it's newer than database
# no-auto-import: false
# Enable JSON output by default
# json: false
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
# actor: ""
# Path to database (overridden by BEADS_DB or --db)
# db: ""
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
# auto-start-daemon: true
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
# flush-debounce: "5s"
# Export events (audit trail) to .beads/events.jsonl on each flush/sync
# When enabled, new events are appended incrementally using a high-water mark.
# Use 'bd export --events' to trigger manually regardless of this setting.
# events-export: false
# Git branch for beads commits (bd sync will commit to this branch)
# IMPORTANT: Set this for team projects so all clones use the same sync branch.
# This setting persists across clones (unlike database config which is gitignored).
# Can also use BEADS_SYNC_BRANCH env var for local override.
# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.
# sync-branch: "beads-sync"
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# Integration settings (access with 'bd config get/set')
# These are stored in the database, not in this file:
# - jira.url
# - jira.project
# - linear.url
# - linear.api-key
# - github.org
# - github.repo
+7
View File
@@ -0,0 +1,7 @@
{"id":"py_workspace-6sg","title":"Document marker parquet structure","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T02:48:08.95742431Z","created_by":"crosstyan","updated_at":"2026-02-07T02:49:35.897152691Z","closed_at":"2026-02-07T02:49:35.897152691Z","close_reason":"Documented parquet structure in aruco/markers/PARQUET_FORMAT.md"}
{"id":"py_workspace-a85","title":"Add CLI option for ArUco dictionary in calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:13:41.896728814Z","created_by":"crosstyan","updated_at":"2026-02-06T10:14:44.083065399Z","closed_at":"2026-02-06T10:14:44.083065399Z","close_reason":"Added CLI option for selectable ArUco dictionary including AprilTag aliases"}
{"id":"py_workspace-cg9","title":"Implement core alignment utilities (Task 1)","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:40:36.296030875Z","created_by":"crosstyan","updated_at":"2026-02-06T10:40:46.196825039Z","closed_at":"2026-02-06T10:40:46.196825039Z","close_reason":"Implemented compute_face_normal, rotation_align_vectors, and apply_alignment_to_pose in aruco/alignment.py"}
{"id":"py_workspace-kuy","title":"Move parquet documentation to docs/","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T02:52:12.609090777Z","created_by":"crosstyan","updated_at":"2026-02-07T02:52:43.088520272Z","closed_at":"2026-02-07T02:52:43.088520272Z","close_reason":"Moved parquet documentation to docs/marker-parquet-format.md"}
{"id":"py_workspace-q4w","title":"Add type hints and folder-aware --svo input in calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:01:13.943518267Z","created_by":"crosstyan","updated_at":"2026-02-06T10:03:09.855307397Z","closed_at":"2026-02-06T10:03:09.855307397Z","close_reason":"Implemented type hints and directory expansion for --svo"}
{"id":"py_workspace-t4e","title":"Add --min-markers CLI and rejection debug logs in calibrate_extrinsics","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:21:51.846079425Z","created_by":"crosstyan","updated_at":"2026-02-06T10:22:39.870440044Z","closed_at":"2026-02-06T10:22:39.870440044Z","close_reason":"Added --min-markers (default 1), rejection debug logs, and clarified accepted-pose summary label"}
{"id":"py_workspace-z3r","title":"Add debug logs for successful ArUco detection","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:17:30.195422209Z","created_by":"crosstyan","updated_at":"2026-02-06T10:18:35.263206185Z","closed_at":"2026-02-06T10:18:35.263206185Z","close_reason":"Added loguru debug logs for successful ArUco detections in calibrate_extrinsics loop"}
+4
View File
@@ -0,0 +1,4 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl"
}
+8
View File
@@ -0,0 +1,8 @@
{
"active_plan": "/workspaces/zed-playground/py_workspace/.sisyphus/plans/ground-plane-alignment.md",
"started_at": "2026-02-06T10:34:57.130Z",
"session_ids": [
"ses_3cd9cdde1ffeQFgrhQqYAExSTn"
],
"plan_name": "ground-plane-alignment"
}
@@ -0,0 +1,2 @@
- Alignment is applied via pre-multiplication to the 4x4 pose matrix, consistent with global frame rotation.
- Chose to raise ValueError for degenerate cases (collinear corners) in compute_face_normal.
@@ -32,7 +32,14 @@
## Debugging Heuristics ## Debugging Heuristics
## Documentation Gaps ## Documentation Gaps
- Users were unclear on how `--auto-align` made decisions (heuristic vs explicit) and what `--refine-depth` actually did. The new documentation addresses this by explaining the decision flow and the optimization objective function.
## Jaxtyping Runtime Dependencies
- `jaxtyping` imports failed at runtime because it expects a backend (jax, torch, or tensorflow) to be installed.
## Depth Refinement Failure
- Depth refinement was failing (0 iterations, no improvement) because the depth map was in millimeters (~2500) while the computed depth from extrinsics was in meters (~2.5). This resulted in huge residuals (~2497.5) that the optimizer couldn't handle effectively. Fixed by normalizing the depth map to meters immediately upon retrieval.
@@ -64,7 +64,17 @@
## Debug Visibility ## Debug Visibility
## Documentation ## Documentation
- Created `docs/calibrate-extrinsics-workflow.md` to document the runtime behavior of the calibration tool, specifically detailing the precedence logic for ground plane alignment and the mathematical basis for depth verification/refinement.
## Type Annotation Hardening
- Integrated `jaxtyping` for shape-aware array annotations (e.g., `Float[np.ndarray, "4 4"]`).
- Used `TYPE_CHECKING` blocks to define these aliases, ensuring they are available for static analysis (like `basedpyright`) while falling back to standard `np.ndarray` at runtime if `jaxtyping` backends are missing.
## Depth Units
- ZED SDK `retrieve_measure(sl.MEASURE.DEPTH)` returns values in the unit defined in `InitParameters.coordinate_units`.
- The default unit is `MILLIMETER`.
- Since our extrinsics and marker geometry are in meters, we must explicitly convert the retrieved depth map to meters (divide by 1000.0) to avoid massive scale mismatches during verification and refinement.
+20 -6
View File
@@ -1,8 +1,22 @@
import numpy as np import numpy as np
from loguru import logger from loguru import logger
from jaxtyping import Float
from typing import TYPE_CHECKING
# Type aliases for shape-aware annotations
if TYPE_CHECKING:
Vec3 = Float[np.ndarray, "3"]
Mat33 = Float[np.ndarray, "3 3"]
Mat44 = Float[np.ndarray, "4 4"]
CornersNC = Float[np.ndarray, "N 3"]
else:
Vec3 = np.ndarray
Mat33 = np.ndarray
Mat44 = np.ndarray
CornersNC = np.ndarray
def compute_face_normal(corners: np.ndarray) -> np.ndarray: def compute_face_normal(corners: CornersNC) -> Vec3:
""" """
Compute the normal vector of a face defined by its corners. Compute the normal vector of a face defined by its corners.
Assumes corners are in order (e.g., clockwise or counter-clockwise). Assumes corners are in order (e.g., clockwise or counter-clockwise).
@@ -37,7 +51,7 @@ def compute_face_normal(corners: np.ndarray) -> np.ndarray:
return (normal / norm).astype(np.float64) return (normal / norm).astype(np.float64)
def rotation_align_vectors(from_vec: np.ndarray, to_vec: np.ndarray) -> np.ndarray: def rotation_align_vectors(from_vec: Vec3, to_vec: Vec3) -> Mat33:
""" """
Compute the 3x3 rotation matrix that aligns from_vec to to_vec. Compute the 3x3 rotation matrix that aligns from_vec to to_vec.
@@ -100,7 +114,7 @@ def rotation_align_vectors(from_vec: np.ndarray, to_vec: np.ndarray) -> np.ndarr
return R.astype(np.float64) return R.astype(np.float64)
def apply_alignment_to_pose(T: np.ndarray, R_align: np.ndarray) -> np.ndarray: def apply_alignment_to_pose(T: Mat44, R_align: Mat33) -> Mat44:
""" """
Apply an alignment rotation to a 4x4 pose matrix. Apply an alignment rotation to a 4x4 pose matrix.
The alignment is applied in the global frame (pre-multiplication of rotation). The alignment is applied in the global frame (pre-multiplication of rotation).
@@ -127,7 +141,7 @@ def get_face_normal_from_geometry(
face_name: str, face_name: str,
marker_geometry: dict[int, np.ndarray], marker_geometry: dict[int, np.ndarray],
face_marker_map: dict[str, list[int]] | None = None, face_marker_map: dict[str, list[int]] | None = None,
) -> np.ndarray | None: ) -> Vec3 | None:
""" """
Compute the average normal vector for a face based on available marker geometry. Compute the average normal vector for a face based on available marker geometry.
@@ -171,9 +185,9 @@ def get_face_normal_from_geometry(
def detect_ground_face( def detect_ground_face(
visible_marker_ids: set[int], visible_marker_ids: set[int],
marker_geometry: dict[int, np.ndarray], marker_geometry: dict[int, np.ndarray],
camera_up_vector: np.ndarray = np.array([0, -1, 0]), camera_up_vector: Vec3 = np.array([0, -1, 0]),
face_marker_map: dict[str, list[int]] | None = None, face_marker_map: dict[str, list[int]] | None = None,
) -> tuple[str, np.ndarray] | None: ) -> tuple[str, Vec3] | None:
""" """
Detect which face of the object is most likely the ground face. Detect which face of the object is most likely the ground face.
The ground face is the one whose normal is most aligned with the camera's up vector. The ground face is the one whose normal is most aligned with the camera's up vector.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+3 -3
View File
@@ -234,9 +234,9 @@ class PoseAccumulator:
stats = { stats = {
"n_total": n_total, "n_total": n_total,
"n_inliers": n_inliers, "n_inliers": n_inliers,
"median_reproj_error": float(np.median(inlier_errors)) "median_reproj_error": (
if inlier_errors float(np.median(inlier_errors)) if inlier_errors else 0.0
else 0.0, ),
} }
return T_mean, stats return T_mean, stats
+5 -1
View File
@@ -182,7 +182,11 @@ class SVOReader:
return None return None
depth_mat = sl.Mat() depth_mat = sl.Mat()
cam.retrieve_measure(depth_mat, sl.MEASURE.DEPTH) cam.retrieve_measure(depth_mat, sl.MEASURE.DEPTH)
return depth_mat.get_data().copy() depth_data = depth_mat.get_data().copy()
# ZED SDK defaults to MILLIMETER units if not specified in InitParameters.
# We convert to meters to match the extrinsics coordinate system.
return depth_data / 1000.0
def _retrieve_confidence(self, cam: sl.Camera) -> np.ndarray | None: def _retrieve_confidence(self, cam: sl.Camera) -> np.ndarray | None:
if not self.enable_depth: if not self.enable_depth:
+15 -2
View File
@@ -29,8 +29,21 @@ from aruco.alignment import (
detect_ground_face, detect_ground_face,
rotation_align_vectors, rotation_align_vectors,
apply_alignment_to_pose, apply_alignment_to_pose,
Vec3,
Mat44,
) )
from loguru import logger from loguru import logger
from jaxtyping import Float
from typing import TYPE_CHECKING
# Type aliases
if TYPE_CHECKING:
Mat33 = Float[np.ndarray, "3 3"]
CornersNC = Float[np.ndarray, "N 3"]
else:
Mat33 = np.ndarray
CornersNC = np.ndarray
ARUCO_DICT_MAP = { ARUCO_DICT_MAP = {
"DICT_4X4_50": cv2.aruco.DICT_4X4_50, "DICT_4X4_50": cv2.aruco.DICT_4X4_50,
@@ -590,11 +603,11 @@ def main(
) )
if ground_normal is not None: if ground_normal is not None:
R_align = rotation_align_vectors(ground_normal, np.array([0, 1, 0])) R_align: Mat33 = rotation_align_vectors(ground_normal, np.array([0, 1, 0]))
logger.info(f"Computed alignment rotation for face '{target_face}'") logger.info(f"Computed alignment rotation for face '{target_face}'")
for serial, data in results.items(): for serial, data in results.items():
T_mean = np.fromstring(data["pose"], sep=" ").reshape(4, 4) T_mean: Mat44 = np.fromstring(data["pose"], sep=" ").reshape(4, 4)
T_aligned = apply_alignment_to_pose(T_mean, R_align) T_aligned = apply_alignment_to_pose(T_mean, R_align)
data["pose"] = " ".join(f"{x:.6f}" for x in T_aligned.flatten()) data["pose"] = " ".join(f"{x:.6f}" for x in T_aligned.flatten())
logger.debug(f"Applied alignment to camera {serial}") logger.debug(f"Applied alignment to camera {serial}")
+55 -41
View File
@@ -1,23 +1,3 @@
########################################################################
#
# Copyright (c) 2022, STEREOLABS.
#
# All rights reserved.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
########################################################################
""" """
This sample demonstrates how to capture a live 3D point cloud This sample demonstrates how to capture a live 3D point cloud
with the ZED SDK and display the result in an OpenGL window. with the ZED SDK and display the result in an OpenGL window.
@@ -28,36 +8,44 @@ import ogl_viewer.viewer as gl
import pyzed.sl as sl import pyzed.sl as sl
import argparse import argparse
def parse_args(init, opt): def parse_args(init, opt):
if len(opt.input_svo_file) > 0 and opt.input_svo_file.endswith((".svo", ".svo2")): if len(opt.input_svo_file) > 0 and opt.input_svo_file.endswith((".svo", ".svo2")):
init.set_from_svo_file(opt.input_svo_file) init.set_from_svo_file(opt.input_svo_file)
print("[Sample] Using SVO File input: {0}".format(opt.input_svo_file)) print("[Sample] Using SVO File input: {0}".format(opt.input_svo_file))
elif len(opt.ip_address) > 0: elif len(opt.ip_address) > 0:
ip_str = opt.ip_address ip_str = opt.ip_address
if ip_str.replace(':','').replace('.','').isdigit() and len(ip_str.split('.'))==4 and len(ip_str.split(':'))==2: if (
init.set_from_stream(ip_str.split(':')[0],int(ip_str.split(':')[1])) ip_str.replace(":", "").replace(".", "").isdigit()
and len(ip_str.split(".")) == 4
and len(ip_str.split(":")) == 2
):
init.set_from_stream(ip_str.split(":")[0], int(ip_str.split(":")[1]))
print("[Sample] Using Stream input, IP : ", ip_str) print("[Sample] Using Stream input, IP : ", ip_str)
elif ip_str.replace(':','').replace('.','').isdigit() and len(ip_str.split('.'))==4: elif (
ip_str.replace(":", "").replace(".", "").isdigit()
and len(ip_str.split(".")) == 4
):
init.set_from_stream(ip_str) init.set_from_stream(ip_str)
print("[Sample] Using Stream input, IP : ", ip_str) print("[Sample] Using Stream input, IP : ", ip_str)
else: else:
print("Unvalid IP format. Using live stream") print("Unvalid IP format. Using live stream")
if ("HD2K" in opt.resolution): if "HD2K" in opt.resolution:
init.camera_resolution = sl.RESOLUTION.HD2K init.camera_resolution = sl.RESOLUTION.HD2K
print("[Sample] Using Camera in resolution HD2K") print("[Sample] Using Camera in resolution HD2K")
elif ("HD1200" in opt.resolution): elif "HD1200" in opt.resolution:
init.camera_resolution = sl.RESOLUTION.HD1200 init.camera_resolution = sl.RESOLUTION.HD1200
print("[Sample] Using Camera in resolution HD1200") print("[Sample] Using Camera in resolution HD1200")
elif ("HD1080" in opt.resolution): elif "HD1080" in opt.resolution:
init.camera_resolution = sl.RESOLUTION.HD1080 init.camera_resolution = sl.RESOLUTION.HD1080
print("[Sample] Using Camera in resolution HD1080") print("[Sample] Using Camera in resolution HD1080")
elif ("HD720" in opt.resolution): elif "HD720" in opt.resolution:
init.camera_resolution = sl.RESOLUTION.HD720 init.camera_resolution = sl.RESOLUTION.HD720
print("[Sample] Using Camera in resolution HD720") print("[Sample] Using Camera in resolution HD720")
elif ("SVGA" in opt.resolution): elif "SVGA" in opt.resolution:
init.camera_resolution = sl.RESOLUTION.SVGA init.camera_resolution = sl.RESOLUTION.SVGA
print("[Sample] Using Camera in resolution SVGA") print("[Sample] Using Camera in resolution SVGA")
elif ("VGA" in opt.resolution): elif "VGA" in opt.resolution:
init.camera_resolution = sl.RESOLUTION.VGA init.camera_resolution = sl.RESOLUTION.VGA
print("[Sample] Using Camera in resolution VGA") print("[Sample] Using Camera in resolution VGA")
elif len(opt.resolution) > 0: elif len(opt.resolution) > 0:
@@ -66,9 +54,10 @@ def parse_args(init, opt):
print("[Sample] Using default resolution") print("[Sample] Using default resolution")
def main(opt): def main(opt):
print("Running Depth Sensing sample ... Press 'Esc' to quit\nPress 's' to save the point cloud") print(
"Running Depth Sensing sample ... Press 'Esc' to quit\nPress 's' to save the point cloud"
)
# Determine memory type based on CuPy availability and user preference # Determine memory type based on CuPy availability and user preference
use_gpu = gl.GPU_ACCELERATION_AVAILABLE and not opt.disable_gpu_data_transfer use_gpu = gl.GPU_ACCELERATION_AVAILABLE and not opt.disable_gpu_data_transfer
@@ -76,9 +65,11 @@ def main(opt):
if use_gpu: if use_gpu:
print("🚀 Using GPU data transfer with CuPy") print("🚀 Using GPU data transfer with CuPy")
init = sl.InitParameters(depth_mode=sl.DEPTH_MODE.NEURAL, init = sl.InitParameters(
depth_mode=sl.DEPTH_MODE.NEURAL,
coordinate_units=sl.UNIT.METER, coordinate_units=sl.UNIT.METER,
coordinate_system=sl.COORDINATE_SYSTEM.RIGHT_HANDED_Y_UP) coordinate_system=sl.COORDINATE_SYSTEM.RIGHT_HANDED_Y_UP,
)
parse_args(init, opt) parse_args(init, opt)
zed = sl.Camera() zed = sl.Camera()
status = zed.open(init) status = zed.open(init)
@@ -107,9 +98,11 @@ def main(opt):
if viewer.save_data: if viewer.save_data:
# For saving, we take CPU memory regardless of processing type # For saving, we take CPU memory regardless of processing type
point_cloud_to_save = sl.Mat() point_cloud_to_save = sl.Mat()
zed.retrieve_measure(point_cloud_to_save, sl.MEASURE.XYZRGBA, sl.MEM.CPU) zed.retrieve_measure(
err = point_cloud_to_save.write('Pointcloud.ply') point_cloud_to_save, sl.MEASURE.XYZRGBA, sl.MEM.CPU
if(err == sl.ERROR_CODE.SUCCESS): )
err = point_cloud_to_save.write("Pointcloud.ply")
if err == sl.ERROR_CODE.SUCCESS:
print("Current .ply file saving succeed") print("Current .ply file saving succeed")
else: else:
print("Current .ply file failed") print("Current .ply file failed")
@@ -120,12 +113,33 @@ def main(opt):
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--input_svo_file', type=str, help='Path to an .svo file, if you want to replay it',default = '') parser.add_argument(
parser.add_argument('--ip_address', type=str, help='IP Adress, in format a.b.c.d:port or a.b.c.d, if you have a streaming setup', default = '') "--input_svo_file",
parser.add_argument('--resolution', type=str, help='Resolution, can be either HD2K, HD1200, HD1080, HD720, SVGA or VGA', default = '') type=str,
parser.add_argument('--disable-gpu-data-transfer', action='store_true', help='Disable GPU data transfer acceleration with CuPy even if CuPy is available') help="Path to an .svo file, if you want to replay it",
default="",
)
parser.add_argument(
"--ip_address",
type=str,
help="IP Adress, in format a.b.c.d:port or a.b.c.d, if you have a streaming setup",
default="",
)
parser.add_argument(
"--resolution",
type=str,
help="Resolution, can be either HD2K, HD1200, HD1080, HD720, SVGA or VGA",
default="",
)
parser.add_argument(
"--disable-gpu-data-transfer",
action="store_true",
help="Disable GPU data transfer acceleration with CuPy even if CuPy is available",
)
opt = parser.parse_args() opt = parser.parse_args()
if len(opt.input_svo_file) > 0 and len(opt.ip_address) > 0: if len(opt.input_svo_file) > 0 and len(opt.ip_address) > 0:
print("Specify only input_svo_file or ip_address, or none to use wired camera, not both. Exit program") print(
"Specify only input_svo_file or ip_address, or none to use wired camera, not both. Exit program"
)
exit() exit()
main(opt) main(opt)
@@ -131,7 +131,7 @@ If you suspect a unit mismatch, check the `depth_verify` RMSE in the output JSON
*Note: Confidence filtering (`--depth-confidence-threshold`) is orthogonal to this issue. A unit mismatch affects all valid pixels regardless of confidence.* *Note: Confidence filtering (`--depth-confidence-threshold`) is orthogonal to this issue. A unit mismatch affects all valid pixels regardless of confidence.*
## Findings Summary (2026-02-07 exhaustive search) ## Findings Summary (2026-02-07)
This section summarizes the latest deep investigation across local code, outputs, and external docs. This section summarizes the latest deep investigation across local code, outputs, and external docs.
+142 -71
View File
@@ -184,7 +184,9 @@ try:
try: try:
if cudart is not None: # Check if cudart is still available if cudart is not None: # Check if cudart is still available
check_cudart_err( check_cudart_err(
cudart.cudaGraphicsUnmapResources(1, self._graphics_ressource, stream) cudart.cudaGraphicsUnmapResources(
1, self._graphics_ressource, stream
)
) )
self._cuda_buffer = None self._cuda_buffer = None
except Exception: except Exception:
@@ -193,7 +195,7 @@ try:
return self return self
class CudaOpenGLMappedArray(CudaOpenGLMappedBuffer): class CudaOpenGLMappedArray(CudaOpenGLMappedBuffer):
def __init__(self, dtype, shape, gl_buffer, flags=0, strides=None, order='C'): def __init__(self, dtype, shape, gl_buffer, flags=0, strides=None, order="C"):
super().__init__(gl_buffer, flags) super().__init__(gl_buffer, flags)
self._dtype = dtype self._dtype = dtype
self._shape = shape self._shape = shape
@@ -233,13 +235,21 @@ class Shader:
if glGetProgramiv(self.program_id, GL_LINK_STATUS) != GL_TRUE: if glGetProgramiv(self.program_id, GL_LINK_STATUS) != GL_TRUE:
info = glGetProgramInfoLog(self.program_id) info = glGetProgramInfoLog(self.program_id)
if (self.program_id is not None) and (self.program_id > 0) and glIsProgram(self.program_id): if (
(self.program_id is not None)
and (self.program_id > 0)
and glIsProgram(self.program_id)
):
glDeleteProgram(self.program_id) glDeleteProgram(self.program_id)
if (vertex_id is not None) and (vertex_id > 0) and glIsShader(vertex_id): if (vertex_id is not None) and (vertex_id > 0) and glIsShader(vertex_id):
glDeleteShader(vertex_id) glDeleteShader(vertex_id)
if (fragment_id is not None) and (fragment_id > 0) and glIsShader(fragment_id): if (
(fragment_id is not None)
and (fragment_id > 0)
and glIsShader(fragment_id)
):
glDeleteShader(fragment_id) glDeleteShader(fragment_id)
raise RuntimeError('Error linking program: %s' % (info)) raise RuntimeError("Error linking program: %s" % (info))
if (vertex_id is not None) and (vertex_id > 0) and glIsShader(vertex_id): if (vertex_id is not None) and (vertex_id > 0) and glIsShader(vertex_id):
glDeleteShader(vertex_id) glDeleteShader(vertex_id)
if (fragment_id is not None) and (fragment_id > 0) and glIsShader(fragment_id): if (fragment_id is not None) and (fragment_id > 0) and glIsShader(fragment_id):
@@ -257,9 +267,13 @@ class Shader:
glCompileShader(shader_id) glCompileShader(shader_id)
if glGetShaderiv(shader_id, GL_COMPILE_STATUS) != GL_TRUE: if glGetShaderiv(shader_id, GL_COMPILE_STATUS) != GL_TRUE:
info = glGetShaderInfoLog(shader_id) info = glGetShaderInfoLog(shader_id)
if (shader_id is not None) and (shader_id > 0) and glIsShader(shader_id): if (
(shader_id is not None)
and (shader_id > 0)
and glIsShader(shader_id)
):
glDeleteShader(shader_id) glDeleteShader(shader_id)
raise RuntimeError('Shader compilation failed: %s' % (info)) raise RuntimeError("Shader compilation failed: %s" % (info))
return shader_id return shader_id
except: except:
if (shader_id is not None) and (shader_id > 0) and glIsShader(shader_id): if (shader_id is not None) and (shader_id > 0) and glIsShader(shader_id):
@@ -269,6 +283,7 @@ class Shader:
def get_program_id(self): def get_program_id(self):
return self.program_id return self.program_id
class Simple3DObject: class Simple3DObject:
def __init__(self, _is_static, pts_size=3, clr_size=3): def __init__(self, _is_static, pts_size=3, clr_size=3):
self.is_init = False self.is_init = False
@@ -315,15 +330,30 @@ class Simple3DObject:
if len(self.vertices): if len(self.vertices):
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0])
glBufferData(GL_ARRAY_BUFFER, len(self.vertices) * self.vertices.itemsize, (GLfloat * len(self.vertices))(*self.vertices), type_draw) glBufferData(
GL_ARRAY_BUFFER,
len(self.vertices) * self.vertices.itemsize,
(GLfloat * len(self.vertices))(*self.vertices),
type_draw,
)
if len(self.colors): if len(self.colors):
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[1]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[1])
glBufferData(GL_ARRAY_BUFFER, len(self.colors) * self.colors.itemsize, (GLfloat * len(self.colors))(*self.colors), type_draw) glBufferData(
GL_ARRAY_BUFFER,
len(self.colors) * self.colors.itemsize,
(GLfloat * len(self.colors))(*self.colors),
type_draw,
)
if len(self.indices): if len(self.indices):
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vboID[2]) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vboID[2])
glBufferData(GL_ELEMENT_ARRAY_BUFFER,len(self.indices) * self.indices.itemsize,(GLuint * len(self.indices))(*self.indices), type_draw) glBufferData(
GL_ELEMENT_ARRAY_BUFFER,
len(self.indices) * self.indices.itemsize,
(GLuint * len(self.indices))(*self.indices),
type_draw,
)
self.elementbufferSize = len(self.indices) self.elementbufferSize = len(self.indices)
@@ -341,33 +371,52 @@ class Simple3DObject:
# Initialize vertex buffer (for XYZRGBA data) # Initialize vertex buffer (for XYZRGBA data)
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0])
glBufferData(GL_ARRAY_BUFFER, self.elementbufferSize * self.pt_type * self.vertices.itemsize, None, type_draw) glBufferData(
GL_ARRAY_BUFFER,
self.elementbufferSize * self.pt_type * self.vertices.itemsize,
None,
type_draw,
)
# Try to set up GPU acceleration if available # Try to set up GPU acceleration if available
if self.use_gpu: if self.use_gpu:
try: try:
flags = cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard flags = (
cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard
)
self.cuda_mapped_buffer = CudaOpenGLMappedArray( self.cuda_mapped_buffer = CudaOpenGLMappedArray(
dtype=np.float32, dtype=np.float32,
shape=(self.elementbufferSize, self.pt_type), shape=(self.elementbufferSize, self.pt_type),
gl_buffer=self.vboID[0], gl_buffer=self.vboID[0],
flags=flags flags=flags,
) )
except Exception as e: except Exception as e:
print(f"Failed to initialize GPU acceleration, falling back to CPU: {e}") print(
f"Failed to initialize GPU acceleration, falling back to CPU: {e}"
)
self.use_gpu = False self.use_gpu = False
self.cuda_mapped_buffer = None self.cuda_mapped_buffer = None
# Initialize color buffer (not used for point clouds with XYZRGBA) # Initialize color buffer (not used for point clouds with XYZRGBA)
if self.clr_type: if self.clr_type:
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[1]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[1])
glBufferData(GL_ARRAY_BUFFER, self.elementbufferSize * self.clr_type * self.colors.itemsize, None, type_draw) glBufferData(
GL_ARRAY_BUFFER,
self.elementbufferSize * self.clr_type * self.colors.itemsize,
None,
type_draw,
)
for i in range(0, self.elementbufferSize): for i in range(0, self.elementbufferSize):
self.indices.append(i) self.indices.append(i)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vboID[2]) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vboID[2])
glBufferData(GL_ELEMENT_ARRAY_BUFFER,len(self.indices) * self.indices.itemsize,(GLuint * len(self.indices))(*self.indices), type_draw) glBufferData(
GL_ELEMENT_ARRAY_BUFFER,
len(self.indices) * self.indices.itemsize,
(GLuint * len(self.indices))(*self.indices),
type_draw,
)
def setPoints(self, pc): def setPoints(self, pc):
"""Update point cloud data from sl.Mat""" """Update point cloud data from sl.Mat"""
@@ -375,7 +424,11 @@ class Simple3DObject:
return return
try: try:
if self.use_gpu and self.cuda_mapped_buffer and pc.get_memory_type() in (sl.MEM.GPU, sl.MEM.BOTH): if (
self.use_gpu
and self.cuda_mapped_buffer
and pc.get_memory_type() in (sl.MEM.GPU, sl.MEM.BOTH)
):
self.setPointsGPU(pc) self.setPointsGPU(pc)
else: else:
self.setPointsCPU(pc) self.setPointsCPU(pc)
@@ -423,7 +476,9 @@ class Simple3DObject:
# Get CPU pointer and upload to GPU buffer # Get CPU pointer and upload to GPU buffer
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0])
data_ptr = pc.get_pointer(sl.MEM.CPU) data_ptr = pc.get_pointer(sl.MEM.CPU)
buffer_size = self.elementbufferSize * self.pt_type * 4 # 4 bytes per float32 buffer_size = (
self.elementbufferSize * self.pt_type * 4
) # 4 bytes per float32
glBufferSubData(GL_ARRAY_BUFFER, 0, buffer_size, ctypes.c_void_p(data_ptr)) glBufferSubData(GL_ARRAY_BUFFER, 0, buffer_size, ctypes.c_void_p(data_ptr))
glBindBuffer(GL_ARRAY_BUFFER, 0) glBindBuffer(GL_ARRAY_BUFFER, 0)
@@ -432,9 +487,9 @@ class Simple3DObject:
raise raise
def clear(self): def clear(self):
self.vertices = array.array('f') self.vertices = array.array("f")
self.colors = array.array('f') self.colors = array.array("f")
self.indices = array.array('I') self.indices = array.array("I")
self.elementbufferSize = 0 self.elementbufferSize = 0
def set_drawing_type(self, _type): def set_drawing_type(self, _type):
@@ -446,13 +501,15 @@ class Simple3DObject:
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[0])
glVertexAttribPointer(0, self.pt_type, GL_FLOAT, GL_FALSE, 0, None) glVertexAttribPointer(0, self.pt_type, GL_FLOAT, GL_FALSE, 0, None)
if(self.clr_type): if self.clr_type:
glEnableVertexAttribArray(1) glEnableVertexAttribArray(1)
glBindBuffer(GL_ARRAY_BUFFER, self.vboID[1]) glBindBuffer(GL_ARRAY_BUFFER, self.vboID[1])
glVertexAttribPointer(1, self.clr_type, GL_FLOAT, GL_FALSE, 0, None) glVertexAttribPointer(1, self.clr_type, GL_FLOAT, GL_FALSE, 0, None)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vboID[2]) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vboID[2])
glDrawElements(self.drawing_type, self.elementbufferSize, GL_UNSIGNED_INT, None) glDrawElements(
self.drawing_type, self.elementbufferSize, GL_UNSIGNED_INT, None
)
glDisableVertexAttribArray(0) glDisableVertexAttribArray(0)
if self.clr_type: if self.clr_type:
@@ -460,22 +517,23 @@ class Simple3DObject:
def __del__(self): def __del__(self):
"""Cleanup GPU resources""" """Cleanup GPU resources"""
if hasattr(self, 'cuda_mapped_buffer') and self.cuda_mapped_buffer: if hasattr(self, "cuda_mapped_buffer") and self.cuda_mapped_buffer:
try: try:
self.cuda_mapped_buffer.unregister() self.cuda_mapped_buffer.unregister()
except: except:
pass pass
class GLViewer: class GLViewer:
def __init__(self): def __init__(self):
self.available = False self.available = False
self.mutex = Lock() self.mutex = Lock()
self.camera = CameraGL() self.camera = CameraGL()
self.wheelPosition = 0. self.wheelPosition = 0.0
self.mouse_button = [False, False] self.mouse_button = [False, False]
self.mouseCurrentPosition = [0., 0.] self.mouseCurrentPosition = [0.0, 0.0]
self.previousMouseMotion = [0., 0.] self.previousMouseMotion = [0.0, 0.0]
self.mouseMotion = [0., 0.] self.mouseMotion = [0.0, 0.0]
self.zedModel = Simple3DObject(True) self.zedModel = Simple3DObject(True)
self.point_cloud = Simple3DObject(False, 4) self.point_cloud = Simple3DObject(False, 4)
self.save_data = False self.save_data = False
@@ -491,8 +549,7 @@ class GLViewer:
glutCreateWindow(b"ZED Depth Sensing") glutCreateWindow(b"ZED Depth Sensing")
glViewport(0, 0, wnd_w, wnd_h) glViewport(0, 0, wnd_w, wnd_h)
glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_CONTINUE_EXECUTION)
GLUT_ACTION_CONTINUE_EXECUTION)
glEnable(GL_DEPTH_TEST) glEnable(GL_DEPTH_TEST)
@@ -504,17 +561,21 @@ class GLViewer:
# Compile and create the shader for 3D objects # Compile and create the shader for 3D objects
self.shader_image = Shader(VERTEX_SHADER, FRAGMENT_SHADER) self.shader_image = Shader(VERTEX_SHADER, FRAGMENT_SHADER)
self.shader_image_MVP = glGetUniformLocation(self.shader_image.get_program_id(), "u_mvpMatrix") self.shader_image_MVP = glGetUniformLocation(
self.shader_image.get_program_id(), "u_mvpMatrix"
)
self.shader_pc = Shader(POINTCLOUD_VERTEX_SHADER, POINTCLOUD_FRAGMENT_SHADER) self.shader_pc = Shader(POINTCLOUD_VERTEX_SHADER, POINTCLOUD_FRAGMENT_SHADER)
self.shader_pc_MVP = glGetUniformLocation(self.shader_pc.get_program_id(), "u_mvpMatrix") self.shader_pc_MVP = glGetUniformLocation(
self.shader_pc.get_program_id(), "u_mvpMatrix"
)
self.bckgrnd_clr = np.array([223/255., 230/255., 233/255.]) self.bckgrnd_clr = np.array([223 / 255.0, 230 / 255.0, 233 / 255.0])
# Create the camera model # Create the camera model
Z_ = -0.15 Z_ = -0.15
Y_ = Z_ * math.tan(95. * M_PI / 180. / 2.) Y_ = Z_ * math.tan(95.0 * M_PI / 180.0 / 2.0)
X_ = Y_ * 16./9. X_ = Y_ * 16.0 / 9.0
A = np.array([0, 0, 0]) A = np.array([0, 0, 0])
B = np.array([X_, Y_, Z_]) B = np.array([X_, Y_, Z_])
@@ -578,26 +639,25 @@ class GLViewer:
def keyPressedCallback(self, key, x, y): def keyPressedCallback(self, key, x, y):
if ord(key) == 27: if ord(key) == 27:
self.close_func() self.close_func()
if (ord(key) == 83 or ord(key) == 115): if ord(key) == 83 or ord(key) == 115:
self.save_data = True self.save_data = True
def on_mouse(self, *args, **kwargs): def on_mouse(self, *args, **kwargs):
(key,Up,x,y) = args key, Up, x, y = args
if key == 0: if key == 0:
self.mouse_button[0] = (Up == 0) self.mouse_button[0] = Up == 0
elif key == 2: elif key == 2:
self.mouse_button[1] = (Up == 0) self.mouse_button[1] = Up == 0
elif(key == 3): elif key == 3:
self.wheelPosition = self.wheelPosition + 1 self.wheelPosition = self.wheelPosition + 1
elif(key == 4): elif key == 4:
self.wheelPosition = self.wheelPosition - 1 self.wheelPosition = self.wheelPosition - 1
self.mouseCurrentPosition = [x, y] self.mouseCurrentPosition = [x, y]
self.previousMouseMotion = [x, y] self.previousMouseMotion = [x, y]
def on_mousemove(self, *args, **kwargs): def on_mousemove(self, *args, **kwargs):
(x,y) = args x, y = args
self.mouseMotion[0] = x - self.previousMouseMotion[0] self.mouseMotion[0] = x - self.previousMouseMotion[0]
self.mouseMotion[1] = y - self.previousMouseMotion[1] self.mouseMotion[1] = y - self.previousMouseMotion[1]
self.previousMouseMotion = [x, y] self.previousMouseMotion = [x, y]
@@ -610,7 +670,9 @@ class GLViewer:
def draw_callback(self): def draw_callback(self):
if self.available: if self.available:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glClearColor(self.bckgrnd_clr[0], self.bckgrnd_clr[1], self.bckgrnd_clr[2], 1.) glClearColor(
self.bckgrnd_clr[0], self.bckgrnd_clr[1], self.bckgrnd_clr[2], 1.0
)
self.mutex.acquire() self.mutex.acquire()
self.update() self.update()
@@ -621,18 +683,18 @@ class GLViewer:
glutPostRedisplay() glutPostRedisplay()
def update(self): def update(self):
if(self.mouse_button[0]): if self.mouse_button[0]:
r = sl.Rotation() r = sl.Rotation()
vert = self.camera.vertical_ vert = self.camera.vertical_
tmp = vert.get() tmp = vert.get()
vert.init_vector(tmp[0] * 1.,tmp[1] * 1., tmp[2] * 1.) vert.init_vector(tmp[0] * 1.0, tmp[1] * 1.0, tmp[2] * 1.0)
r.init_angle_translation(self.mouseMotion[0] * 0.02, vert) r.init_angle_translation(self.mouseMotion[0] * 0.02, vert)
self.camera.rotate(r) self.camera.rotate(r)
r.init_angle_translation(self.mouseMotion[1] * 0.02, self.camera.right_) r.init_angle_translation(self.mouseMotion[1] * 0.02, self.camera.right_)
self.camera.rotate(r) self.camera.rotate(r)
if(self.mouse_button[1]): if self.mouse_button[1]:
t = sl.Translation() t = sl.Translation()
tmp = self.camera.right_.get() tmp = self.camera.right_.get()
scale = self.mouseMotion[0] * -0.05 scale = self.mouseMotion[0] * -0.05
@@ -644,7 +706,7 @@ class GLViewer:
t.init_vector(tmp[0] * scale, tmp[1] * scale, tmp[2] * scale) t.init_vector(tmp[0] * scale, tmp[1] * scale, tmp[2] * scale)
self.camera.translate(t) self.camera.translate(t)
if (self.wheelPosition != 0): if self.wheelPosition != 0:
t = sl.Translation() t = sl.Translation()
tmp = self.camera.forward_.get() tmp = self.camera.forward_.get()
scale = self.wheelPosition * -0.065 scale = self.wheelPosition * -0.065
@@ -653,23 +715,28 @@ class GLViewer:
self.camera.update() self.camera.update()
self.mouseMotion = [0., 0.] self.mouseMotion = [0.0, 0.0]
self.wheelPosition = 0 self.wheelPosition = 0
def draw(self): def draw(self):
vpMatrix = self.camera.getViewProjectionMatrix() vpMatrix = self.camera.getViewProjectionMatrix()
glUseProgram(self.shader_image.get_program_id()) glUseProgram(self.shader_image.get_program_id())
glUniformMatrix4fv(self.shader_image_MVP, 1, GL_TRUE, (GLfloat * len(vpMatrix))(*vpMatrix)) glUniformMatrix4fv(
self.shader_image_MVP, 1, GL_TRUE, (GLfloat * len(vpMatrix))(*vpMatrix)
)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
self.zedModel.draw() self.zedModel.draw()
glUseProgram(0) glUseProgram(0)
glUseProgram(self.shader_pc.get_program_id()) glUseProgram(self.shader_pc.get_program_id())
glUniformMatrix4fv(self.shader_pc_MVP, 1, GL_TRUE, (GLfloat * len(vpMatrix))(*vpMatrix)) glUniformMatrix4fv(
glPointSize(1.) self.shader_pc_MVP, 1, GL_TRUE, (GLfloat * len(vpMatrix))(*vpMatrix)
)
glPointSize(1.0)
self.point_cloud.draw() self.point_cloud.draw()
glUseProgram(0) glUseProgram(0)
class CameraGL: class CameraGL:
def __init__(self): def __init__(self):
self.ORIGINAL_FORWARD = sl.Translation() self.ORIGINAL_FORWARD = sl.Translation()
@@ -679,8 +746,8 @@ class CameraGL:
self.ORIGINAL_RIGHT = sl.Translation() self.ORIGINAL_RIGHT = sl.Translation()
self.ORIGINAL_RIGHT.init_vector(1, 0, 0) self.ORIGINAL_RIGHT.init_vector(1, 0, 0)
self.znear = 0.5 self.znear = 0.5
self.zfar = 100. self.zfar = 100.0
self.horizontalFOV = 70. self.horizontalFOV = 70.0
self.orientation_ = sl.Orientation() self.orientation_ = sl.Orientation()
self.position_ = sl.Translation() self.position_ = sl.Translation()
self.forward_ = sl.Translation() self.forward_ = sl.Translation()
@@ -694,42 +761,46 @@ class CameraGL:
self.projection_.set_identity() self.projection_.set_identity()
self.setProjection(1.78) self.setProjection(1.78)
self.position_.init_vector(0., 0., 0.) self.position_.init_vector(0.0, 0.0, 0.0)
tmp = sl.Translation() tmp = sl.Translation()
tmp.init_vector(0, 0, -.1) tmp.init_vector(0, 0, -0.1)
tmp2 = sl.Translation() tmp2 = sl.Translation()
tmp2.init_vector(0, 1, 0) tmp2.init_vector(0, 1, 0)
self.setDirection(tmp, tmp2) self.setDirection(tmp, tmp2)
def update(self): def update(self):
dot_ = sl.Translation.dot_translation(self.vertical_, self.up_) dot_ = sl.Translation.dot_translation(self.vertical_, self.up_)
if(dot_ < 0.): if dot_ < 0.0:
tmp = self.vertical_.get() tmp = self.vertical_.get()
self.vertical_.init_vector(tmp[0] * -1.,tmp[1] * -1., tmp[2] * -1.) self.vertical_.init_vector(tmp[0] * -1.0, tmp[1] * -1.0, tmp[2] * -1.0)
transformation = sl.Transform() transformation = sl.Transform()
tmp_position = self.position_.get() tmp_position = self.position_.get()
tmp = (self.offset_ * self.orientation_).get() tmp = (self.offset_ * self.orientation_).get()
new_position = sl.Translation() new_position = sl.Translation()
new_position.init_vector(tmp_position[0] + tmp[0], tmp_position[1] + tmp[1], tmp_position[2] + tmp[2]) new_position.init_vector(
tmp_position[0] + tmp[0], tmp_position[1] + tmp[1], tmp_position[2] + tmp[2]
)
transformation.init_orientation_translation(self.orientation_, new_position) transformation.init_orientation_translation(self.orientation_, new_position)
transformation.inverse() transformation.inverse()
self.vpMatrix_ = self.projection_ * transformation self.vpMatrix_ = self.projection_ * transformation
def setProjection(self, im_ratio): def setProjection(self, im_ratio):
fov_x = self.horizontalFOV * 3.1416 / 180. fov_x = self.horizontalFOV * 3.1416 / 180.0
fov_y = self.horizontalFOV * im_ratio * 3.1416 / 180. fov_y = self.horizontalFOV * im_ratio * 3.1416 / 180.0
self.projection_[(0,0)] = 1. / math.tan(fov_x * .5) self.projection_[(0, 0)] = 1.0 / math.tan(fov_x * 0.5)
self.projection_[(1,1)] = 1. / math.tan(fov_y * .5) self.projection_[(1, 1)] = 1.0 / math.tan(fov_y * 0.5)
self.projection_[(2, 2)] = -(self.zfar + self.znear) / (self.zfar - self.znear) self.projection_[(2, 2)] = -(self.zfar + self.znear) / (self.zfar - self.znear)
self.projection_[(3,2)] = -1. self.projection_[(3, 2)] = -1.0
self.projection_[(2,3)] = -(2. * self.zfar * self.znear) / (self.zfar - self.znear) self.projection_[(2, 3)] = -(2.0 * self.zfar * self.znear) / (
self.projection_[(3,3)] = 0. self.zfar - self.znear
)
self.projection_[(3, 3)] = 0.0
def getViewProjectionMatrix(self): def getViewProjectionMatrix(self):
tmp = self.vpMatrix_.m tmp = self.vpMatrix_.m
vpMat = array.array('f') vpMat = array.array("f")
for row in tmp: for row in tmp:
for v in row: for v in row:
vpMat.append(v) vpMat.append(v)
@@ -740,7 +811,7 @@ class CameraGL:
tmp.transpose() tmp.transpose()
tr.transpose() tr.transpose()
tmp = (tr * tmp).m tmp = (tr * tmp).m
vpMat = array.array('f') vpMat = array.array("f")
for row in tmp: for row in tmp:
for v in row: for v in row:
vpMat.append(v) vpMat.append(v)
@@ -749,11 +820,11 @@ class CameraGL:
def setDirection(self, dir, vert): def setDirection(self, dir, vert):
dir.normalize() dir.normalize()
tmp = dir.get() tmp = dir.get()
dir.init_vector(tmp[0] * -1.,tmp[1] * -1., tmp[2] * -1.) dir.init_vector(tmp[0] * -1.0, tmp[1] * -1.0, tmp[2] * -1.0)
self.orientation_.init_translation(self.ORIGINAL_FORWARD, dir) self.orientation_.init_translation(self.ORIGINAL_FORWARD, dir)
self.updateVectors() self.updateVectors()
self.vertical_ = vert self.vertical_ = vert
if(sl.Translation.dot_translation(self.vertical_, self.up_) < 0.): if sl.Translation.dot_translation(self.vertical_, self.up_) < 0.0:
tmp = sl.Rotation() tmp = sl.Rotation()
tmp.init_angle_translation(3.14, self.ORIGINAL_FORWARD) tmp.init_angle_translation(3.14, self.ORIGINAL_FORWARD)
self.rotate(tmp) self.rotate(tmp)
@@ -781,5 +852,5 @@ class CameraGL:
self.up_ = self.ORIGINAL_UP * self.orientation_ self.up_ = self.ORIGINAL_UP * self.orientation_
right = self.ORIGINAL_RIGHT right = self.ORIGINAL_RIGHT
tmp = right.get() tmp = right.get()
right.init_vector(tmp[0] * -1.,tmp[1] * -1., tmp[2] * -1.) right.init_vector(tmp[0] * -1.0, tmp[1] * -1.0, tmp[2] * -1.0)
self.right_ = right * self.orientation_ self.right_ = right * self.orientation_
+72 -14
View File
@@ -8,7 +8,11 @@ import threading
import signal import signal
import time import time
import sys import sys
import click
import zed_network_utils import zed_network_utils
import cv2
import queue
import os
# Global variable to handle exit # Global variable to handle exit
exit_app = False exit_app = False
@@ -21,14 +25,26 @@ def signal_handler(signal, frame):
print("\nCtrl+C pressed. Exiting...") print("\nCtrl+C pressed. Exiting...")
def acquisition(zed): def acquisition(zed, frame_queue=None):
"""Acquisition thread function to continuously grab frames""" """Acquisition thread function to continuously grab frames"""
infos = zed.get_camera_information() infos = zed.get_camera_information()
mat = sl.Mat()
while not exit_app: while not exit_app:
if zed.grab() == sl.ERROR_CODE.SUCCESS: if zed.grab() == sl.ERROR_CODE.SUCCESS:
# If needed, add more processing here if frame_queue is not None:
# But be aware that any processing involving the GiL will slow down the multi threading performance # Retrieve left image
zed.retrieve_image(mat, sl.VIEW.LEFT)
# Convert to numpy and copy to ensure thread safety when passing to main
try:
# Keep latest frame only
if frame_queue.full():
try:
frame_queue.get_nowait()
except queue.Empty:
pass
frame_queue.put_nowait(mat.get_data().copy())
except queue.Full:
pass pass
print(f"{infos.camera_model}[{infos.serial_number}] QUIT") print(f"{infos.camera_model}[{infos.serial_number}] QUIT")
@@ -39,8 +55,8 @@ def acquisition(zed):
zed.close() zed.close()
def open_camera(zed, config): def open_camera(zed, config, save_dir):
"""Open a camera with given configuration and enable streaming""" """Open a camera with given configuration and enable recording"""
ip, port = zed_network_utils.extract_ip_port(config) ip, port = zed_network_utils.extract_ip_port(config)
if not ip or not port: if not ip or not port:
@@ -59,7 +75,7 @@ def open_camera(zed, config):
print(f"ZED SN{serial} Opened from {ip}:{port}") print(f"ZED SN{serial} Opened from {ip}:{port}")
# Enable Recording # Enable Recording
output_svo_file = f"ZED_SN{serial}.svo2" output_svo_file = os.path.join(save_dir, f"ZED_SN{serial}.svo2")
recording_param = sl.RecordingParameters( recording_param = sl.RecordingParameters(
output_svo_file.replace(" ", ""), sl.SVO_COMPRESSION_MODE.H265 output_svo_file.replace(" ", ""), sl.SVO_COMPRESSION_MODE.H265
) )
@@ -76,41 +92,63 @@ def open_camera(zed, config):
return True return True
def main(): @click.command()
@click.option(
"--monitor", is_flag=True, help="Enable local monitoring of the camera streams."
)
@click.option(
"--config",
default=zed_network_utils.DEFAULT_CONFIG_PATH,
help="Path to the network configuration JSON file.",
type=click.Path(exists=True),
)
@click.option(
"--save-dir",
default=os.getcwd(),
help="Directory where SVO files will be saved.",
type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True),
)
def main(monitor, config, save_dir):
global exit_app global exit_app
# Read network configuration using utility # Read network configuration using utility
network_config = zed_network_utils.parse_network_config() network_config = zed_network_utils.parse_network_config(config)
if not network_config: if not network_config:
return 1 return
print(f"Found {len(network_config)} cameras in configuration") print(f"Found {len(network_config)} cameras in configuration")
if len(network_config) == 0: if len(network_config) == 0:
print("No ZED configured, exit program") print("No ZED configured, exit program")
return 1 return
zed_open = False zed_open = False
# Open all cameras # Open all cameras
zeds = [] zeds = []
threads = [] threads = []
queues = {} # serial -> queue
for serial, config in network_config.items(): for serial, config in network_config.items():
zed = sl.Camera() zed = sl.Camera()
if open_camera(zed, config): if open_camera(zed, config, save_dir):
zeds.append(zed) zeds.append(zed)
zed_open = True zed_open = True
fq = None
if monitor:
fq = queue.Queue(maxsize=1)
queues[serial] = fq
# Start acquisition thread immediately # Start acquisition thread immediately
thread = threading.Thread(target=acquisition, args=(zed,)) thread = threading.Thread(target=acquisition, args=(zed, fq))
thread.start() thread.start()
threads.append(thread) threads.append(thread)
if not zed_open: if not zed_open:
print("No ZED opened, exit program") print("No ZED opened, exit program")
return 1 return
# Set up signal handler for Ctrl+C # Set up signal handler for Ctrl+C
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
@@ -118,10 +156,30 @@ def main():
# Main loop # Main loop
while not exit_app: while not exit_app:
if monitor:
for serial, q in queues.items():
try:
frame = q.get_nowait()
# Display the frame
# Use serial number as window name to distinguish cameras
# Resize is handled by window automatically usually, or we can resize
cv2.imshow(f"ZED {serial}", frame)
except queue.Empty:
pass
# Check for quit key
key = cv2.waitKey(10)
if key == 113 or key == ord("q") or key == 27: # q or Esc
exit_app = True
else:
time.sleep(0.02) time.sleep(0.02)
# Wait for all threads to finish # Wait for all threads to finish
print("Exit signal, closing ZEDs") print("Exit signal, closing ZEDs")
if monitor:
cv2.destroyAllWindows()
time.sleep(0.1) time.sleep(0.1)
for thread in threads: for thread in threads:
@@ -132,4 +190,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) main()
-20
View File
@@ -1,23 +1,3 @@
########################################################################
#
# Copyright (c) 2022, STEREOLABS.
#
# All rights reserved.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
########################################################################
""" """
Read a stream and display the left images using OpenCV Read a stream and display the left images using OpenCV
""" """
+33 -3
View File
@@ -1,10 +1,10 @@
# ... existing code ...
import sys import sys
import pyzed.sl as sl import pyzed.sl as sl
import cv2 import cv2
import argparse import argparse
import os import os
import math import math
from pathlib import Path
def progress_bar(percent_done, bar_length=50): def progress_bar(percent_done, bar_length=50):
@@ -16,8 +16,38 @@ def progress_bar(percent_done, bar_length=50):
def main(opt): def main(opt):
svo_files = opt.input_svo_files input_paths = [Path(p) for p in opt.input_svo_files]
svo_files = []
for path in input_paths:
if path.is_dir():
print(f"Searching for SVO files in {path}...")
found = sorted(
[
str(f)
for f in path.iterdir()
if f.is_file() and f.suffix.lower() in (".svo", ".svo2")
]
)
if found:
print(f"Found {len(found)} files in {path}")
svo_files.extend(found)
else:
print(f"No .svo or .svo2 files found in {path}")
elif path.is_file():
svo_files.append(str(path))
else:
print(f"Path not found: {path}")
if not svo_files:
print("No valid SVO files provided. Exiting.")
return
# Sort files to ensure deterministic order
svo_files.sort()
cameras = [] cameras = []
cam_data = [] # List of dicts to store camera info cam_data = [] # List of dicts to store camera info
print(f"Opening {len(svo_files)} SVO files...") print(f"Opening {len(svo_files)} SVO files...")
@@ -177,7 +207,7 @@ if __name__ == "__main__":
"--input_svo_files", "--input_svo_files",
nargs="+", nargs="+",
type=str, type=str,
help="Path to .svo/.svo2 files", help="Path to .svo/.svo2 files or directories",
required=True, required=True,
) )
opt = parser.parse_args() opt = parser.parse_args()