ray360 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ray360-0.1.0/.github/workflows/end-to-end.yml +30 -0
- ray360-0.1.0/.github/workflows/publish.yml +68 -0
- ray360-0.1.0/.github/workflows/tests.yml +44 -0
- ray360-0.1.0/.gitignore +15 -0
- ray360-0.1.0/LICENSE +21 -0
- ray360-0.1.0/PKG-INFO +186 -0
- ray360-0.1.0/README.md +167 -0
- ray360-0.1.0/benchmarks/benchmark.py +123 -0
- ray360-0.1.0/docs/api.md +144 -0
- ray360-0.1.0/docs/benchmark-cpu.json +32 -0
- ray360-0.1.0/docs/benchmarks.md +41 -0
- ray360-0.1.0/docs/releasing.md +47 -0
- ray360-0.1.0/docs/scope.md +50 -0
- ray360-0.1.0/docs/validation-e2e.json +2010 -0
- ray360-0.1.0/docs/validation.md +132 -0
- ray360-0.1.0/examples/basic.py +42 -0
- ray360-0.1.0/examples/detect.py +115 -0
- ray360-0.1.0/examples/video_frames.py +60 -0
- ray360-0.1.0/pyproject.toml +54 -0
- ray360-0.1.0/scripts/check_wheel.py +92 -0
- ray360-0.1.0/scripts/fetch_test_data.py +147 -0
- ray360-0.1.0/src/ray360/__init__.py +27 -0
- ray360-0.1.0/src/ray360/annotations.py +304 -0
- ray360-0.1.0/src/ray360/cubemap.py +149 -0
- ray360-0.1.0/src/ray360/experimental.py +55 -0
- ray360-0.1.0/src/ray360/geometry.py +78 -0
- ray360-0.1.0/src/ray360/image.py +81 -0
- ray360-0.1.0/src/ray360/panorama.py +163 -0
- ray360-0.1.0/src/ray360/projections.py +176 -0
- ray360-0.1.0/src/ray360/py.typed +0 -0
- ray360-0.1.0/src/ray360/remap.py +172 -0
- ray360-0.1.0/src/ray360/transforms.py +181 -0
- ray360-0.1.0/tests/conftest.py +63 -0
- ray360-0.1.0/tests/data-sources.json +146 -0
- ray360-0.1.0/tests/test_cubemap.py +110 -0
- ray360-0.1.0/tests/test_end_to_end.py +272 -0
- ray360-0.1.0/tests/test_examples.py +28 -0
- ray360-0.1.0/tests/test_experimental.py +37 -0
- ray360-0.1.0/tests/test_fixture_tools.py +46 -0
- ray360-0.1.0/tests/test_image.py +63 -0
- ray360-0.1.0/tests/test_package.py +78 -0
- ray360-0.1.0/tests/test_projections.py +85 -0
- ray360-0.1.0/tests/test_regions.py +134 -0
- ray360-0.1.0/tests/test_sampling.py +101 -0
- ray360-0.1.0/tests/test_views.py +144 -0
- ray360-0.1.0/uv.lock +657 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: real media and model validation
|
|
2
|
+
on: workflow_dispatch
|
|
3
|
+
|
|
4
|
+
permissions:
|
|
5
|
+
contents: read
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
cpu:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
timeout-minutes: 20
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9
|
|
14
|
+
with:
|
|
15
|
+
version: '0.10.9'
|
|
16
|
+
python-version: '3.12'
|
|
17
|
+
- run: uv sync --locked --group dev --extra image
|
|
18
|
+
- run: uv pip install torch==2.15.0 torchvision==0.29.2 --index https://download.pytorch.org/whl/cpu
|
|
19
|
+
- run: uv pip install av==19.0.2 ultralytics==8.4.213
|
|
20
|
+
- run: uv run --no-sync python scripts/fetch_test_data.py --include-e2e
|
|
21
|
+
- run: uv run --no-sync python -m pytest tests/test_end_to_end.py --e2e --e2e-output .test-results/e2e -v -s
|
|
22
|
+
env:
|
|
23
|
+
YOLO_CONFIG_DIR: .test-results/yolo
|
|
24
|
+
YOLO_OFFLINE: 'true'
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
if: always()
|
|
27
|
+
with:
|
|
28
|
+
name: cpu-validation-report
|
|
29
|
+
path: .test-results/e2e/report.json
|
|
30
|
+
include-hidden-files: true
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pypi-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: false
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
22
|
+
with:
|
|
23
|
+
version: '0.10.9'
|
|
24
|
+
python-version: '3.12'
|
|
25
|
+
- run: uv sync --locked --group dev --extra image
|
|
26
|
+
- name: Check release tag and package version
|
|
27
|
+
run: |
|
|
28
|
+
uv run --no-sync python - <<'PY'
|
|
29
|
+
import os
|
|
30
|
+
import tomllib
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
import ray360
|
|
33
|
+
project = tomllib.loads(Path('pyproject.toml').read_text())['project']
|
|
34
|
+
version = project['version']
|
|
35
|
+
if project['name'] != 'ray360' or ray360.__version__ != version:
|
|
36
|
+
raise SystemExit('Package name or version does not match the source')
|
|
37
|
+
if os.environ['GITHUB_REF_NAME'] != f'v{version}':
|
|
38
|
+
raise SystemExit('Release tag must match the package version')
|
|
39
|
+
PY
|
|
40
|
+
- run: uv run --no-sync python -m pytest
|
|
41
|
+
- run: uv build --no-sources
|
|
42
|
+
- run: uv tool run --from twine==7.0.0 twine check --strict dist/*
|
|
43
|
+
- run: uv run --no-sync python scripts/check_wheel.py
|
|
44
|
+
- run: uv run --no-sync python scripts/check_wheel.py --image
|
|
45
|
+
- uses: actions/upload-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: distributions
|
|
48
|
+
path: dist/
|
|
49
|
+
if-no-files-found: error
|
|
50
|
+
|
|
51
|
+
publish:
|
|
52
|
+
needs: build
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
environment:
|
|
55
|
+
name: pypi
|
|
56
|
+
url: https://pypi.org/project/ray360/
|
|
57
|
+
permissions:
|
|
58
|
+
id-token: write
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/download-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: distributions
|
|
63
|
+
path: dist/
|
|
64
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9
|
|
65
|
+
with:
|
|
66
|
+
version: '0.10.9'
|
|
67
|
+
enable-cache: false
|
|
68
|
+
- run: uv publish --trusted-publishing always dist/*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
permissions:
|
|
5
|
+
contents: read
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
include:
|
|
12
|
+
- {os: ubuntu-latest, python: '3.11'}
|
|
13
|
+
- {os: ubuntu-latest, python: '3.13'}
|
|
14
|
+
- {os: ubuntu-latest, python: '3.14'}
|
|
15
|
+
- {os: windows-latest, python: '3.12'}
|
|
16
|
+
- {os: macos-latest, python: '3.12'}
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
21
|
+
with:
|
|
22
|
+
version: '0.10.9'
|
|
23
|
+
python-version: ${{ matrix.python }}
|
|
24
|
+
- run: uv sync --locked --group dev --extra image
|
|
25
|
+
- run: uv run --no-sync ruff check src tests scripts examples benchmarks
|
|
26
|
+
- run: uv run --no-sync ruff format --check src tests scripts examples benchmarks
|
|
27
|
+
- run: uv run --no-sync mypy
|
|
28
|
+
- run: uv run --no-sync python -m pytest
|
|
29
|
+
- run: uv run --no-sync python examples/basic.py
|
|
30
|
+
- run: uv build --no-sources
|
|
31
|
+
- run: uv run --no-sync python scripts/check_wheel.py
|
|
32
|
+
- run: uv run --no-sync python scripts/check_wheel.py --image
|
|
33
|
+
|
|
34
|
+
minimum-dependencies:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9
|
|
39
|
+
with:
|
|
40
|
+
version: '0.10.9'
|
|
41
|
+
python-version: '3.11'
|
|
42
|
+
- run: uv sync --locked --group dev --extra image
|
|
43
|
+
- run: uv pip install 'numpy==1.24.0' 'Pillow==10.0.0'
|
|
44
|
+
- run: uv run --no-sync python -m pytest
|
ray360-0.1.0/.gitignore
ADDED
ray360-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ray360 contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ray360-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ray360
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Perspective views, cubemap conversion and prediction mapping for stitched panoramas
|
|
5
|
+
Project-URL: Repository, https://github.com/aasmanbashyal/ray360
|
|
6
|
+
Project-URL: Documentation, https://github.com/aasmanbashyal/ray360/blob/main/docs/api.md
|
|
7
|
+
Project-URL: Issues, https://github.com/aasmanbashyal/ray360/issues
|
|
8
|
+
Author: Aasman Bashyal
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: numpy>=1.24
|
|
16
|
+
Provides-Extra: image
|
|
17
|
+
Requires-Dist: pillow>=10; extra == 'image'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Ray360
|
|
21
|
+
|
|
22
|
+
Perspective views and reversible prediction mapping for **already-stitched 360 images**.
|
|
23
|
+
|
|
24
|
+
Ray360 extracts model inputs from equirectangular NumPy arrays and retains the
|
|
25
|
+
geometry through crop, resize, and letterbox operations. Map points and predicted
|
|
26
|
+
boxes back to the panorama, including objects crossing its seam or covering a pole.
|
|
27
|
+
|
|
28
|
+
Python 3.11+. NumPy is the only required dependency. Version 0.1.0 is the focused
|
|
29
|
+
first-release API; the earlier development API has been removed.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add ray360 # NumPy core
|
|
35
|
+
uv add "ray360[image]" # Optional Pillow image helpers
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With pip, use `pip install ray360` or `pip install "ray360[image]"`.
|
|
39
|
+
|
|
40
|
+
For development, clone [the repository](https://github.com/aasmanbashyal/ray360)
|
|
41
|
+
and install from the checkout:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv sync --locked # NumPy core
|
|
45
|
+
uv sync --locked --extra image # Optional Pillow image helpers
|
|
46
|
+
uv sync --locked --group dev --extra image
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To test a local build in another environment, run `uv build --no-sources`,
|
|
50
|
+
then use `uv pip install /path/to/ray360-0.1.0-py3-none-any.whl`.
|
|
51
|
+
|
|
52
|
+
## Extract a view and map a prediction
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import numpy as np
|
|
56
|
+
from ray360 import Box2D, Panorama, Perspective
|
|
57
|
+
|
|
58
|
+
# Supplying Panorama explicitly declares that these pixels are already stitched.
|
|
59
|
+
panorama = Panorama(np.zeros((1024, 2048, 3), dtype=np.uint8), source_id="capture")
|
|
60
|
+
view = panorama.view(Perspective(960, 540, yaw=180, pitch=10, hfov=100))
|
|
61
|
+
model_view = view.crop(40, 20, 880, 500).letterbox(640, 640)
|
|
62
|
+
|
|
63
|
+
# Run any model in the application. Its predictions must use model_view pixels.
|
|
64
|
+
boxes = [Box2D(250, 240, 390, 400, label="person", score=0.93, track_id=7)]
|
|
65
|
+
regions = model_view.map_boxes(boxes)
|
|
66
|
+
for region in regions:
|
|
67
|
+
polygons = region.polygons(panorama.projection)
|
|
68
|
+
rectangles = region.boxes(panorama.projection)
|
|
69
|
+
membership = region.rasterize(panorama.projection)
|
|
70
|
+
|
|
71
|
+
points, valid = model_view.transform.to_panorama([[320, 320], [10, 0]])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
One `SphericalRegion` remains one object even when rendering produces two seam
|
|
75
|
+
pieces. `region.box` retains labels, scores, tracking IDs, and attributes. Exported
|
|
76
|
+
rectangles enclose sampled boundaries and are approximate. Use `rasterize()` for
|
|
77
|
+
membership at pixel centers.
|
|
78
|
+
|
|
79
|
+
Predictions entirely in preprocessing padding are discarded; partial boxes are
|
|
80
|
+
clipped. Point mapping returns a validity mask. Ignore coordinates marked false.
|
|
81
|
+
`model_view.valid` identifies retained pixels, including after a crop followed by
|
|
82
|
+
new padding. These masks describe geometry, not detector confidence or occlusion.
|
|
83
|
+
|
|
84
|
+
## Arrays, sampling, and repeated frames
|
|
85
|
+
|
|
86
|
+
HW and HWC arrays retain their dtype. Bilinear interpolation is the default for
|
|
87
|
+
images. Use `interpolation="nearest"` for class labels; the choice carries through
|
|
88
|
+
all subsequent view preprocessing unless explicitly overridden. Boolean and
|
|
89
|
+
64-bit integer arrays require nearest sampling.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from ray360 import MapCache, coverage_views
|
|
93
|
+
|
|
94
|
+
cache = MapCache(max_bytes=64 * 1024 * 1024)
|
|
95
|
+
targets = coverage_views(width=320, height=320)
|
|
96
|
+
for view in panorama.views(targets, cache=cache):
|
|
97
|
+
pass # Process and release each view instead of retaining all images.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Reuse the cache and targets for subsequent panoramas with the same dimensions and
|
|
101
|
+
orientation. The default shared cache holds at most 128 MiB of coordinate maps.
|
|
102
|
+
Pass `cache=None` to disable caching. The budget covers cached maps, not caller
|
|
103
|
+
images, returned views, or temporary allocations. Arrays remain caller-owned.
|
|
104
|
+
|
|
105
|
+
`coverage_views()` defaults to 24 views covering the sphere. This does not
|
|
106
|
+
guarantee detection recall. Choose view size, field of view, and overlap using the
|
|
107
|
+
application's accuracy and latency measurements.
|
|
108
|
+
|
|
109
|
+
## Persistence and optional image helpers
|
|
110
|
+
|
|
111
|
+
`ViewTransform.to_dict()` and `.from_dict()` preserve the mapping without storing
|
|
112
|
+
pixels. `view.provenance()` adds source ID, optional timestamp, and interpolation.
|
|
113
|
+
`SphericalRegion.to_dict()` and `.from_dict()` preserve both a region's boundary
|
|
114
|
+
definition and its intended interior. Both schemas carry a version. Keep custom
|
|
115
|
+
annotation attributes JSON-compatible when writing them as JSON.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from ray360.image import overlay, read_panorama, write_image
|
|
119
|
+
|
|
120
|
+
panorama = read_panorama("stitched.jpg") # Explicit declaration of stitched geometry
|
|
121
|
+
view = panorama.view(Perspective(640, 360, yaw=180))
|
|
122
|
+
regions = view.map_boxes([Box2D(240, 100, 400, 260)])
|
|
123
|
+
write_image("mapped.png", overlay(panorama.image, regions, projection=panorama.projection))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Image loading returns uint8 RGB and preserves stored pixel orientation. Saving
|
|
127
|
+
refuses existing files unless `overwrite=True` is supplied.
|
|
128
|
+
|
|
129
|
+
## Cubemap conversion
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from ray360 import Equirectangular, cube_projections, from_cubemap, to_cubemap
|
|
133
|
+
|
|
134
|
+
faces = to_cubemap(panorama, size=512) # dict of six HW or HWC arrays
|
|
135
|
+
restored = from_cubemap(faces, Equirectangular(2048, 1024))
|
|
136
|
+
|
|
137
|
+
# For predictions on cube faces, keep the ordinary View and its transform.
|
|
138
|
+
front = panorama.view(cube_projections(512)["front"])
|
|
139
|
+
regions = front.map_boxes([Box2D(200, 180, 300, 320)])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Faces are named `front`, `right`, `back`, `left`, `up`, and `down`. Each is square
|
|
143
|
+
with a 90-degree field of view. [The orientation table](https://github.com/aasmanbashyal/ray360/blob/main/docs/api.md#cubemap-conversion)
|
|
144
|
+
defines the pole faces. Bilinear conversion back to a panorama samples adjacent
|
|
145
|
+
faces at cube boundaries. Use `interpolation="nearest"` in both directions for
|
|
146
|
+
label arrays. Conversion preserves dtype and can lose detail through resampling.
|
|
147
|
+
Atlas layouts and raw-camera stitching remain application concerns.
|
|
148
|
+
|
|
149
|
+
## Scope and experimental operations
|
|
150
|
+
|
|
151
|
+
The package owns projection geometry, cubemap conversion, preprocessing transforms,
|
|
152
|
+
box/point mapping, and map caching. Applications own inference, video decoding, timestamps, dataset
|
|
153
|
+
formats, and encoding. Raw-camera stitching, fisheye calibration, tensor backends,
|
|
154
|
+
network streaming, and segmentation/pose annotation codecs are outside this release.
|
|
155
|
+
|
|
156
|
+
`ray360.experimental` contains sampled spherical IoU and class-aware NMS. They
|
|
157
|
+
are outside the stable root API. Small objects can miss every sample and receive
|
|
158
|
+
zero overlap, even against themselves. NMS suppresses predictions; it does not
|
|
159
|
+
fuse partial detections or establish detector accuracy. See [scope](https://github.com/aasmanbashyal/ray360/blob/main/docs/scope.md).
|
|
160
|
+
|
|
161
|
+
## Examples and development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
uv run --no-sync python examples/basic.py
|
|
165
|
+
uv run --no-sync python -m pytest
|
|
166
|
+
uv run --no-sync ruff check src tests scripts examples benchmarks
|
|
167
|
+
uv run --no-sync mypy
|
|
168
|
+
uv build --no-sources
|
|
169
|
+
uv run --no-sync python scripts/check_wheel.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
[The basic example](https://github.com/aasmanbashyal/ray360/blob/main/examples/basic.py) needs only NumPy. It can write a synthetic
|
|
173
|
+
overlay with `--output .test-results/demo.png` when Pillow is installed.
|
|
174
|
+
[The model example](https://github.com/aasmanbashyal/ray360/blob/main/examples/detect.py) and [decoder example](https://github.com/aasmanbashyal/ray360/blob/main/examples/video_frames.py)
|
|
175
|
+
show application-owned integrations with separately installed dependencies.
|
|
176
|
+
The opt-in [end-to-end suite](https://github.com/aasmanbashyal/ray360/blob/main/docs/validation.md#real-media-and-model-validation)
|
|
177
|
+
executes these examples and runs cube conversion and CPU detection on every frame
|
|
178
|
+
of two real panorama video encodings. It also checks two stitched photos and
|
|
179
|
+
photographic cube faces. Media and model weights are checksum-verified local fixtures.
|
|
180
|
+
|
|
181
|
+
See [API conventions](https://github.com/aasmanbashyal/ray360/blob/main/docs/api.md), [validation](https://github.com/aasmanbashyal/ray360/blob/main/docs/validation.md), and
|
|
182
|
+
[whole-panorama benchmarks](https://github.com/aasmanbashyal/ray360/blob/main/docs/benchmarks.md). The ordinary test suite never
|
|
183
|
+
downloads media or model weights.
|
|
184
|
+
|
|
185
|
+
Maintainers can follow the [release guide](https://github.com/aasmanbashyal/ray360/blob/main/docs/releasing.md)
|
|
186
|
+
to publish a version from a Git tag.
|
ray360-0.1.0/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Ray360
|
|
2
|
+
|
|
3
|
+
Perspective views and reversible prediction mapping for **already-stitched 360 images**.
|
|
4
|
+
|
|
5
|
+
Ray360 extracts model inputs from equirectangular NumPy arrays and retains the
|
|
6
|
+
geometry through crop, resize, and letterbox operations. Map points and predicted
|
|
7
|
+
boxes back to the panorama, including objects crossing its seam or covering a pole.
|
|
8
|
+
|
|
9
|
+
Python 3.11+. NumPy is the only required dependency. Version 0.1.0 is the focused
|
|
10
|
+
first-release API; the earlier development API has been removed.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv add ray360 # NumPy core
|
|
16
|
+
uv add "ray360[image]" # Optional Pillow image helpers
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
With pip, use `pip install ray360` or `pip install "ray360[image]"`.
|
|
20
|
+
|
|
21
|
+
For development, clone [the repository](https://github.com/aasmanbashyal/ray360)
|
|
22
|
+
and install from the checkout:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv sync --locked # NumPy core
|
|
26
|
+
uv sync --locked --extra image # Optional Pillow image helpers
|
|
27
|
+
uv sync --locked --group dev --extra image
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To test a local build in another environment, run `uv build --no-sources`,
|
|
31
|
+
then use `uv pip install /path/to/ray360-0.1.0-py3-none-any.whl`.
|
|
32
|
+
|
|
33
|
+
## Extract a view and map a prediction
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import numpy as np
|
|
37
|
+
from ray360 import Box2D, Panorama, Perspective
|
|
38
|
+
|
|
39
|
+
# Supplying Panorama explicitly declares that these pixels are already stitched.
|
|
40
|
+
panorama = Panorama(np.zeros((1024, 2048, 3), dtype=np.uint8), source_id="capture")
|
|
41
|
+
view = panorama.view(Perspective(960, 540, yaw=180, pitch=10, hfov=100))
|
|
42
|
+
model_view = view.crop(40, 20, 880, 500).letterbox(640, 640)
|
|
43
|
+
|
|
44
|
+
# Run any model in the application. Its predictions must use model_view pixels.
|
|
45
|
+
boxes = [Box2D(250, 240, 390, 400, label="person", score=0.93, track_id=7)]
|
|
46
|
+
regions = model_view.map_boxes(boxes)
|
|
47
|
+
for region in regions:
|
|
48
|
+
polygons = region.polygons(panorama.projection)
|
|
49
|
+
rectangles = region.boxes(panorama.projection)
|
|
50
|
+
membership = region.rasterize(panorama.projection)
|
|
51
|
+
|
|
52
|
+
points, valid = model_view.transform.to_panorama([[320, 320], [10, 0]])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
One `SphericalRegion` remains one object even when rendering produces two seam
|
|
56
|
+
pieces. `region.box` retains labels, scores, tracking IDs, and attributes. Exported
|
|
57
|
+
rectangles enclose sampled boundaries and are approximate. Use `rasterize()` for
|
|
58
|
+
membership at pixel centers.
|
|
59
|
+
|
|
60
|
+
Predictions entirely in preprocessing padding are discarded; partial boxes are
|
|
61
|
+
clipped. Point mapping returns a validity mask. Ignore coordinates marked false.
|
|
62
|
+
`model_view.valid` identifies retained pixels, including after a crop followed by
|
|
63
|
+
new padding. These masks describe geometry, not detector confidence or occlusion.
|
|
64
|
+
|
|
65
|
+
## Arrays, sampling, and repeated frames
|
|
66
|
+
|
|
67
|
+
HW and HWC arrays retain their dtype. Bilinear interpolation is the default for
|
|
68
|
+
images. Use `interpolation="nearest"` for class labels; the choice carries through
|
|
69
|
+
all subsequent view preprocessing unless explicitly overridden. Boolean and
|
|
70
|
+
64-bit integer arrays require nearest sampling.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from ray360 import MapCache, coverage_views
|
|
74
|
+
|
|
75
|
+
cache = MapCache(max_bytes=64 * 1024 * 1024)
|
|
76
|
+
targets = coverage_views(width=320, height=320)
|
|
77
|
+
for view in panorama.views(targets, cache=cache):
|
|
78
|
+
pass # Process and release each view instead of retaining all images.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Reuse the cache and targets for subsequent panoramas with the same dimensions and
|
|
82
|
+
orientation. The default shared cache holds at most 128 MiB of coordinate maps.
|
|
83
|
+
Pass `cache=None` to disable caching. The budget covers cached maps, not caller
|
|
84
|
+
images, returned views, or temporary allocations. Arrays remain caller-owned.
|
|
85
|
+
|
|
86
|
+
`coverage_views()` defaults to 24 views covering the sphere. This does not
|
|
87
|
+
guarantee detection recall. Choose view size, field of view, and overlap using the
|
|
88
|
+
application's accuracy and latency measurements.
|
|
89
|
+
|
|
90
|
+
## Persistence and optional image helpers
|
|
91
|
+
|
|
92
|
+
`ViewTransform.to_dict()` and `.from_dict()` preserve the mapping without storing
|
|
93
|
+
pixels. `view.provenance()` adds source ID, optional timestamp, and interpolation.
|
|
94
|
+
`SphericalRegion.to_dict()` and `.from_dict()` preserve both a region's boundary
|
|
95
|
+
definition and its intended interior. Both schemas carry a version. Keep custom
|
|
96
|
+
annotation attributes JSON-compatible when writing them as JSON.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from ray360.image import overlay, read_panorama, write_image
|
|
100
|
+
|
|
101
|
+
panorama = read_panorama("stitched.jpg") # Explicit declaration of stitched geometry
|
|
102
|
+
view = panorama.view(Perspective(640, 360, yaw=180))
|
|
103
|
+
regions = view.map_boxes([Box2D(240, 100, 400, 260)])
|
|
104
|
+
write_image("mapped.png", overlay(panorama.image, regions, projection=panorama.projection))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Image loading returns uint8 RGB and preserves stored pixel orientation. Saving
|
|
108
|
+
refuses existing files unless `overwrite=True` is supplied.
|
|
109
|
+
|
|
110
|
+
## Cubemap conversion
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from ray360 import Equirectangular, cube_projections, from_cubemap, to_cubemap
|
|
114
|
+
|
|
115
|
+
faces = to_cubemap(panorama, size=512) # dict of six HW or HWC arrays
|
|
116
|
+
restored = from_cubemap(faces, Equirectangular(2048, 1024))
|
|
117
|
+
|
|
118
|
+
# For predictions on cube faces, keep the ordinary View and its transform.
|
|
119
|
+
front = panorama.view(cube_projections(512)["front"])
|
|
120
|
+
regions = front.map_boxes([Box2D(200, 180, 300, 320)])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Faces are named `front`, `right`, `back`, `left`, `up`, and `down`. Each is square
|
|
124
|
+
with a 90-degree field of view. [The orientation table](https://github.com/aasmanbashyal/ray360/blob/main/docs/api.md#cubemap-conversion)
|
|
125
|
+
defines the pole faces. Bilinear conversion back to a panorama samples adjacent
|
|
126
|
+
faces at cube boundaries. Use `interpolation="nearest"` in both directions for
|
|
127
|
+
label arrays. Conversion preserves dtype and can lose detail through resampling.
|
|
128
|
+
Atlas layouts and raw-camera stitching remain application concerns.
|
|
129
|
+
|
|
130
|
+
## Scope and experimental operations
|
|
131
|
+
|
|
132
|
+
The package owns projection geometry, cubemap conversion, preprocessing transforms,
|
|
133
|
+
box/point mapping, and map caching. Applications own inference, video decoding, timestamps, dataset
|
|
134
|
+
formats, and encoding. Raw-camera stitching, fisheye calibration, tensor backends,
|
|
135
|
+
network streaming, and segmentation/pose annotation codecs are outside this release.
|
|
136
|
+
|
|
137
|
+
`ray360.experimental` contains sampled spherical IoU and class-aware NMS. They
|
|
138
|
+
are outside the stable root API. Small objects can miss every sample and receive
|
|
139
|
+
zero overlap, even against themselves. NMS suppresses predictions; it does not
|
|
140
|
+
fuse partial detections or establish detector accuracy. See [scope](https://github.com/aasmanbashyal/ray360/blob/main/docs/scope.md).
|
|
141
|
+
|
|
142
|
+
## Examples and development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv run --no-sync python examples/basic.py
|
|
146
|
+
uv run --no-sync python -m pytest
|
|
147
|
+
uv run --no-sync ruff check src tests scripts examples benchmarks
|
|
148
|
+
uv run --no-sync mypy
|
|
149
|
+
uv build --no-sources
|
|
150
|
+
uv run --no-sync python scripts/check_wheel.py
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
[The basic example](https://github.com/aasmanbashyal/ray360/blob/main/examples/basic.py) needs only NumPy. It can write a synthetic
|
|
154
|
+
overlay with `--output .test-results/demo.png` when Pillow is installed.
|
|
155
|
+
[The model example](https://github.com/aasmanbashyal/ray360/blob/main/examples/detect.py) and [decoder example](https://github.com/aasmanbashyal/ray360/blob/main/examples/video_frames.py)
|
|
156
|
+
show application-owned integrations with separately installed dependencies.
|
|
157
|
+
The opt-in [end-to-end suite](https://github.com/aasmanbashyal/ray360/blob/main/docs/validation.md#real-media-and-model-validation)
|
|
158
|
+
executes these examples and runs cube conversion and CPU detection on every frame
|
|
159
|
+
of two real panorama video encodings. It also checks two stitched photos and
|
|
160
|
+
photographic cube faces. Media and model weights are checksum-verified local fixtures.
|
|
161
|
+
|
|
162
|
+
See [API conventions](https://github.com/aasmanbashyal/ray360/blob/main/docs/api.md), [validation](https://github.com/aasmanbashyal/ray360/blob/main/docs/validation.md), and
|
|
163
|
+
[whole-panorama benchmarks](https://github.com/aasmanbashyal/ray360/blob/main/docs/benchmarks.md). The ordinary test suite never
|
|
164
|
+
downloads media or model weights.
|
|
165
|
+
|
|
166
|
+
Maintainers can follow the [release guide](https://github.com/aasmanbashyal/ray360/blob/main/docs/releasing.md)
|
|
167
|
+
to publish a version from a Git tag.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Measure one view and a complete geometry workflow; inference and I/O are excluded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from ray360 import Box2D, MapCache, Panorama, Perspective, ProjectionMap, coverage_views
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def measure(operation, iterations):
|
|
18
|
+
times = []
|
|
19
|
+
for _ in range(iterations):
|
|
20
|
+
start = time.perf_counter()
|
|
21
|
+
operation()
|
|
22
|
+
times.append((time.perf_counter() - start) * 1000)
|
|
23
|
+
return {"p50_ms": float(np.percentile(times, 50)), "p95_ms": float(np.percentile(times, 95))}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def peak_rss_bytes():
|
|
27
|
+
if os.name == "nt":
|
|
28
|
+
import ctypes
|
|
29
|
+
from ctypes import wintypes
|
|
30
|
+
|
|
31
|
+
class Counters(ctypes.Structure):
|
|
32
|
+
_fields_ = [("cb", wintypes.DWORD), ("PageFaultCount", wintypes.DWORD)] + [
|
|
33
|
+
(name, ctypes.c_size_t)
|
|
34
|
+
for name in (
|
|
35
|
+
"PeakWorkingSetSize",
|
|
36
|
+
"WorkingSetSize",
|
|
37
|
+
"QuotaPeakPagedPoolUsage",
|
|
38
|
+
"QuotaPagedPoolUsage",
|
|
39
|
+
"QuotaPeakNonPagedPoolUsage",
|
|
40
|
+
"QuotaNonPagedPoolUsage",
|
|
41
|
+
"PagefileUsage",
|
|
42
|
+
"PeakPagefileUsage",
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
47
|
+
psapi = ctypes.WinDLL("psapi", use_last_error=True)
|
|
48
|
+
kernel.GetCurrentProcess.restype = wintypes.HANDLE
|
|
49
|
+
psapi.GetProcessMemoryInfo.argtypes = [
|
|
50
|
+
wintypes.HANDLE,
|
|
51
|
+
ctypes.POINTER(Counters),
|
|
52
|
+
wintypes.DWORD,
|
|
53
|
+
]
|
|
54
|
+
psapi.GetProcessMemoryInfo.restype = wintypes.BOOL
|
|
55
|
+
counters = Counters()
|
|
56
|
+
counters.cb = ctypes.sizeof(counters)
|
|
57
|
+
if not psapi.GetProcessMemoryInfo(
|
|
58
|
+
kernel.GetCurrentProcess(), ctypes.byref(counters), counters.cb
|
|
59
|
+
):
|
|
60
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
61
|
+
return counters.PeakWorkingSetSize
|
|
62
|
+
import resource
|
|
63
|
+
|
|
64
|
+
rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
65
|
+
return rss if platform.system() == "Darwin" else rss * 1024
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
70
|
+
parser.add_argument("--width", type=int, default=4096)
|
|
71
|
+
parser.add_argument("--height", type=int, default=2048)
|
|
72
|
+
parser.add_argument("--view-size", type=int, default=320)
|
|
73
|
+
parser.add_argument("--iterations", type=int, default=5)
|
|
74
|
+
parser.add_argument("--output", type=Path)
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
if min(args.width, args.height, args.view_size, args.iterations) < 1:
|
|
77
|
+
parser.error("dimensions and iterations must be positive")
|
|
78
|
+
image = np.random.default_rng(42).integers(0, 256, (args.height, args.width, 3), np.uint8)
|
|
79
|
+
panorama = Panorama(image)
|
|
80
|
+
target = Perspective(args.view_size, args.view_size, yaw=179, pitch=20)
|
|
81
|
+
cache = MapCache()
|
|
82
|
+
targets = coverage_views(args.view_size, args.view_size)
|
|
83
|
+
padded_size = args.view_size + max(1, args.view_size // 4)
|
|
84
|
+
box = Box2D(
|
|
85
|
+
args.view_size * 0.25, padded_size * 0.35, args.view_size * 0.75, padded_size * 0.65
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def full_workflow():
|
|
89
|
+
for view in panorama.views(targets, cache=cache):
|
|
90
|
+
view = view.letterbox(args.view_size, padded_size)
|
|
91
|
+
view.transform.to_panorama([[args.view_size / 2, padded_size / 2]])
|
|
92
|
+
for region in view.map_boxes([box]):
|
|
93
|
+
region.polygons(panorama.projection)
|
|
94
|
+
|
|
95
|
+
panorama.view(target, cache=cache)
|
|
96
|
+
full_workflow()
|
|
97
|
+
result = {
|
|
98
|
+
"python": platform.python_version(),
|
|
99
|
+
"numpy": np.__version__,
|
|
100
|
+
"platform": platform.platform(),
|
|
101
|
+
"cpu_count": os.cpu_count(),
|
|
102
|
+
"source_size": list(panorama.projection.size),
|
|
103
|
+
"view_size": list(target.size),
|
|
104
|
+
"coverage_view_count": len(targets),
|
|
105
|
+
"iterations": args.iterations,
|
|
106
|
+
"map_build": measure(
|
|
107
|
+
lambda: ProjectionMap.build(panorama.projection, target), args.iterations
|
|
108
|
+
),
|
|
109
|
+
"cached_single_view": measure(lambda: panorama.view(target, cache=cache), args.iterations),
|
|
110
|
+
"full_panorama_geometry": measure(full_workflow, args.iterations),
|
|
111
|
+
"cache_bytes": cache.nbytes,
|
|
112
|
+
"cache_budget_bytes": cache.max_bytes,
|
|
113
|
+
"process_peak_rss_bytes": peak_rss_bytes(),
|
|
114
|
+
"scope": "Already-decoded pixels; all coverage views, letterboxing, one point and box mapping per view, polygon rendering. Excludes decoding, inference, NMS and file output.",
|
|
115
|
+
}
|
|
116
|
+
encoded = json.dumps(result, indent=2) + "\n"
|
|
117
|
+
if args.output:
|
|
118
|
+
args.output.write_text(encoded, encoding="utf-8")
|
|
119
|
+
print(encoded, end="")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|