chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:05:14 +08:00
commit 2a547be7fe
7904 changed files with 1000926 additions and 0 deletions
@@ -0,0 +1,98 @@
<!--[metadata]
title = "Open photogrammetry format"
tags = ["2D", "3D", "Camera", "Photogrammetry"]
thumbnail = "https://static.rerun.io/open-photogrammetry-format/c9bec43a3a3abd725a55ee8eb527a4c0cb01979b/480w.png"
thumbnail_dimensions = [480, 480]
channel = "release"
include_in_manifest = true
build_args = ["--jpeg-quality=50"]
-->
Uses [`pyopf`](https://github.com/Pix4D/pyopf) to load and display a photogrammetrically reconstructed 3D point cloud in the [Open Photogrammetry Format (OPF)](https://www.pix4d.com/open-photogrammetry-format/).
<picture data-inline-viewer="examples/open_photogrammetry_format">
<source media="(max-width: 480px)" srcset="https://static.rerun.io/open_photogrammetry_format/603d5605f9670889bc8bce3365f16b831fce1eb1/480w.png">
<source media="(max-width: 768px)" srcset="https://static.rerun.io/open_photogrammetry_format/603d5605f9670889bc8bce3365f16b831fce1eb1/768w.png">
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/open_photogrammetry_format/603d5605f9670889bc8bce3365f16b831fce1eb1/1024w.png">
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/open_photogrammetry_format/603d5605f9670889bc8bce3365f16b831fce1eb1/1200w.png">
<img src="https://static.rerun.io/open_photogrammetry_format/603d5605f9670889bc8bce3365f16b831fce1eb1/full.png" alt="">
</picture>
## Used Rerun types
[`Image`](https://www.rerun.io/docs/reference/types/archetypes/image), [`Points3D`](https://www.rerun.io/docs/reference/types/archetypes/points3d), [`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d), [`Pinhole`](https://www.rerun.io/docs/reference/types/archetypes/pinhole)
## Background
This example loads an Open Photogrammetry Format (OPF) project and displays the cameras and point cloud data.
OPF, which stands for 'open photogrammetry format,' is a file format used for photogrammetry data.
It contains all the necessary information related to a reconstructed 3D model made with photogrammetry, including calibration, point clouds and dense reconstruction.
## Logging and visualizing with Rerun
The visualizations in this example were created with the following Rerun code:
### Timelines
For each processed frame, all data sent to Rerun is associated with specific time using [`timelines`](https://www.rerun.io/docs/concepts/logging-and-ingestion/timelines).
```python
rr.set_time("image", sequence=i)
```
### Video
Pinhole camera is utilized for achieving a 3D view and camera perspective through the use of the [`Pinhole`](https://www.rerun.io/docs/reference/types/archetypes/pinhole) and [`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d) archetypes.
```python
rr.log("world/cameras", rr.Transform3D(translation=calib_camera.position, mat3x3=rot))
```
```python
rr.log(
"world/cameras/image",
rr.Pinhole(
resolution=sensor.image_size_px,
focal_length=calib_sensor.internals.focal_length_px,
principal_point=calib_sensor.internals.principal_point_px,
camera_xyz=rr.ViewCoordinates.RUB,
),
)
```
The input video is logged as a sequence of [`Image`](https://www.rerun.io/docs/reference/types/archetypes/image) objects to the `world/cameras/image/rgb` entity.
```python
rr.log("world/cameras/image/rgb", rr.Image(np.array(img)).compress(jpeg_quality=jpeg_quality))
```
### Point clouds
Point clouds from the project are logged as [`Points3D`](https://www.rerun.io/docs/reference/types/archetypes/points3d) archetype to the `world/points` entity.
```python
rr.log("world/points", rr.Points3D(points.position, colors=points.color), static=True)
```
## Run the code
> This example requires Python 3.10 or higher because of [`pyopf`](https://pypi.org/project/pyopf/).
To run this example, make sure you have the Rerun repository checked out and the latest SDK installed:
```bash
pip install --upgrade rerun-sdk # install the latest Rerun SDK
git clone git@github.com:rerun-io/rerun.git # Clone the repository
cd rerun
git checkout latest # Check out the commit matching the latest SDK release
```
Install the necessary libraries specified in the requirements file:
```bash
pip install -e examples/python/open_photogrammetry_format
```
To experiment with the provided example, simply execute the main Python script:
```bash
python -m open_photogrammetry_format # run the example
```
If you wish to customize it or explore additional features, use the CLI with the `--help` option for guidance:
```bash
python -m open_photogrammetry_format --help
```
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Load an Open Photogrammetry Format (OPF) project and display the cameras and point cloud.
Requires Python 3.10 or higher because of [pyopf](https://pypi.org/project/pyopf/).
"""
from __future__ import annotations
import argparse
import logging
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Final
import numpy as np
import requests
import tqdm
from PIL import Image
from pyopf.io import load
from pyopf.resolve import resolve
import rerun as rr
DESCRIPTION = """
# Open Photogrammetry Format
Visualizes an Open Photogrammetry Format (OPF) project, displaying the cameras and point cloud.
The full source code for this example is available
[on GitHub](https://github.com/rerun-io/rerun/blob/latest/examples/python/open_photogrammetry_format).
### Links
* [OPF specification](https://pix4d.github.io/opf-spec/index.html)
* [Dataset source](https://support.pix4d.com/hc/en-us/articles/360000235126#OPF)
* [pyopf](https://github.com/Pix4D/pyopf)
"""
@dataclass
class DatasetSpec:
dir_name: str
url: str
DATASETS = {
"olympic": DatasetSpec("olympic_flame", "https://data.pix4d.com/misc/example_datasets/olympic_flame.zip"),
"rainwater": DatasetSpec(
"catch_rainwater_demo",
"https://data.pix4d.com/misc/example_datasets/catch_rainwater_demo.zip",
),
"rivaz": DatasetSpec("rivaz_demo", "https://data.pix4d.com/misc/example_datasets/rivaz_demo.zip"),
}
DATASET_DIR: Final = Path(__file__).parent / "dataset"
def download_file(url: str, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
logging.info("Downloading %s to %s", url, path)
response = requests.get(url, stream=True)
with tqdm.tqdm.wrapattr(
open(path, "wb"),
"write",
miniters=1,
total=int(response.headers.get("content-length", 0)),
desc=f"Downloading {path.name}",
) as f:
for chunk in response.iter_content(chunk_size=4096):
f.write(chunk)
def unzip_dir(archive: Path, destination: Path) -> None:
"""Unzip the archive to the destination, using tqdm to display progress."""
logging.info("Extracting %s to %s", archive, destination)
with zipfile.ZipFile(archive, "r") as zip_ref:
zip_ref.extractall(destination)
class OPFProject:
def __init__(self, path: Path, log_as_frames: bool = True) -> None:
"""
Create a new OPFProject from the given path.
Parameters
----------
path : Path
Path to the project file.
log_as_frames : bool, optional
Whether to log the cameras as individual frames, by default True
"""
self.path = path
self.project = resolve(load(str(self.path)))
self.log_as_frames = log_as_frames
@classmethod
def from_dataset(cls, dataset: str, log_as_frames: bool = True) -> OPFProject:
"""
Download the dataset if necessary and return the project file.
Parameters
----------
dataset : str
Name of the dataset to download.
log_as_frames : bool, optional
Whether to log the cameras as individual frames, by default True
"""
spec = DATASETS[dataset]
if not (DATASET_DIR / spec.dir_name).exists():
zip_file = DATASET_DIR / f"{dataset}.zip"
if not zip_file.exists():
download_file(DATASETS[dataset].url, zip_file)
unzip_dir(DATASET_DIR / f"{dataset}.zip", DATASET_DIR)
return cls(DATASET_DIR / spec.dir_name / "project.opf", log_as_frames=log_as_frames)
def log_point_cloud(self) -> None:
"""Log the project's point cloud."""
points = self.project.point_cloud_objs[0].nodes[0]
rr.log("world/points", rr.Points3D(points.position, colors=points.color), static=True)
def log_calibrated_cameras(self, jpeg_quality: int | None) -> None:
"""
Log the project's calibrated cameras as individual frames.
Logging all cameras in a single frame is also possible, but clutter the default view with too many image views.
"""
sensor_map = {sensor.id: sensor for sensor in self.project.input_cameras.sensors}
calib_sensor_map = {sensor.id: sensor for sensor in self.project.calibration.calibrated_cameras.sensors}
for i, (camera, calib_camera) in enumerate(
zip(
self.project.camera_list.cameras,
self.project.calibration.calibrated_cameras.cameras,
strict=False,
),
):
if not str(camera.uri).endswith(".jpg"):
continue
if self.log_as_frames:
rr.set_time("image", sequence=i)
entity = "world/cameras"
else:
entity = f"world/cameras/{i}"
sensor = sensor_map[calib_camera.sensor_id]
calib_sensor = calib_sensor_map[calib_camera.sensor_id]
# Specification for the omega, phi, kappa angles:
# https://pix4d.github.io/opf-spec/specification/calibrated_cameras.html#calibrated-camera
omega, phi, kappa = tuple(np.deg2rad(a) for a in calib_camera.orientation_deg)
rot = (
np.array([
[1, 0, 0],
[0, np.cos(omega), -np.sin(omega)],
[0, np.sin(omega), np.cos(omega)],
])
@ np.array([
[np.cos(phi), 0, np.sin(phi)],
[0, 1, 0],
[-np.sin(phi), 0, np.cos(phi)],
])
@ np.array([
[np.cos(kappa), -np.sin(kappa), 0],
[np.sin(kappa), np.cos(kappa), 0],
[0, 0, 1],
])
)
rr.log(entity, rr.Transform3D(translation=calib_camera.position, mat3x3=rot))
assert calib_sensor.internals.type == "perspective"
# RUB coordinate system specified in https://pix4d.github.io/opf-spec/specification/projected_input_cameras.html#coordinate-system-specification
rr.log(
entity + "/image",
rr.Pinhole(
resolution=sensor.image_size_px,
focal_length=calib_sensor.internals.focal_length_px,
principal_point=calib_sensor.internals.principal_point_px,
camera_xyz=rr.ViewCoordinates.RUB,
),
)
if jpeg_quality is not None:
with Image.open(self.path.parent / camera.uri) as img:
rr.log(entity + "/image/rgb", rr.Image(img).compress(jpeg_quality=jpeg_quality))
else:
rr.log(entity + "/image/rgb", rr.EncodedImage(path=self.path.parent / camera.uri))
def main() -> None:
logging.getLogger().addHandler(rr.LoggingHandler())
logging.getLogger().setLevel("INFO")
parser = argparse.ArgumentParser(
description="Load an Open Photogrammetry Format (OPF) project and display the cameras and point cloud.",
)
parser.add_argument(
"--dataset",
choices=DATASETS.keys(),
default="olympic",
help="Run on a demo image automatically downloaded",
)
parser.add_argument(
"--no-frames",
action="store_true",
help="Log all cameras globally instead of as individual frames in the timeline.",
)
parser.add_argument(
"--jpeg-quality",
type=int,
default=None,
help="If specified, compress the camera images with the given JPEG quality.",
)
rr.script_add_args(parser)
args, unknown = parser.parse_known_args()
for arg in unknown:
logging.warning(f"unknown arg: {arg}")
# load the data set
project = OPFProject.from_dataset(args.dataset, log_as_frames=not args.no_frames)
# display everything in Rerun
rr.script_setup(args, "rerun_example_open_photogrammetry_format")
rr.log("description", rr.TextDocument(DESCRIPTION, media_type=rr.MediaType.MARKDOWN), static=True)
rr.log("world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)
project.log_point_cloud()
project.log_calibrated_cameras(jpeg_quality=args.jpeg_quality)
rr.script_teardown(args)
if __name__ == "__main__":
main()
@@ -0,0 +1,13 @@
[project]
name = "open_photogrammetry_format"
version = "0.1.0"
requires-python = ">=3.10" # pyopf requirement
readme = "README.md"
dependencies = ["numpy", "pillow", "pyopf", "requests", "rerun-sdk", "tqdm"]
[project.scripts]
open_photogrammetry_format = "open_photogrammetry_format:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"