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.
Files changed (46) hide show
  1. ray360-0.1.0/.github/workflows/end-to-end.yml +30 -0
  2. ray360-0.1.0/.github/workflows/publish.yml +68 -0
  3. ray360-0.1.0/.github/workflows/tests.yml +44 -0
  4. ray360-0.1.0/.gitignore +15 -0
  5. ray360-0.1.0/LICENSE +21 -0
  6. ray360-0.1.0/PKG-INFO +186 -0
  7. ray360-0.1.0/README.md +167 -0
  8. ray360-0.1.0/benchmarks/benchmark.py +123 -0
  9. ray360-0.1.0/docs/api.md +144 -0
  10. ray360-0.1.0/docs/benchmark-cpu.json +32 -0
  11. ray360-0.1.0/docs/benchmarks.md +41 -0
  12. ray360-0.1.0/docs/releasing.md +47 -0
  13. ray360-0.1.0/docs/scope.md +50 -0
  14. ray360-0.1.0/docs/validation-e2e.json +2010 -0
  15. ray360-0.1.0/docs/validation.md +132 -0
  16. ray360-0.1.0/examples/basic.py +42 -0
  17. ray360-0.1.0/examples/detect.py +115 -0
  18. ray360-0.1.0/examples/video_frames.py +60 -0
  19. ray360-0.1.0/pyproject.toml +54 -0
  20. ray360-0.1.0/scripts/check_wheel.py +92 -0
  21. ray360-0.1.0/scripts/fetch_test_data.py +147 -0
  22. ray360-0.1.0/src/ray360/__init__.py +27 -0
  23. ray360-0.1.0/src/ray360/annotations.py +304 -0
  24. ray360-0.1.0/src/ray360/cubemap.py +149 -0
  25. ray360-0.1.0/src/ray360/experimental.py +55 -0
  26. ray360-0.1.0/src/ray360/geometry.py +78 -0
  27. ray360-0.1.0/src/ray360/image.py +81 -0
  28. ray360-0.1.0/src/ray360/panorama.py +163 -0
  29. ray360-0.1.0/src/ray360/projections.py +176 -0
  30. ray360-0.1.0/src/ray360/py.typed +0 -0
  31. ray360-0.1.0/src/ray360/remap.py +172 -0
  32. ray360-0.1.0/src/ray360/transforms.py +181 -0
  33. ray360-0.1.0/tests/conftest.py +63 -0
  34. ray360-0.1.0/tests/data-sources.json +146 -0
  35. ray360-0.1.0/tests/test_cubemap.py +110 -0
  36. ray360-0.1.0/tests/test_end_to_end.py +272 -0
  37. ray360-0.1.0/tests/test_examples.py +28 -0
  38. ray360-0.1.0/tests/test_experimental.py +37 -0
  39. ray360-0.1.0/tests/test_fixture_tools.py +46 -0
  40. ray360-0.1.0/tests/test_image.py +63 -0
  41. ray360-0.1.0/tests/test_package.py +78 -0
  42. ray360-0.1.0/tests/test_projections.py +85 -0
  43. ray360-0.1.0/tests/test_regions.py +134 -0
  44. ray360-0.1.0/tests/test_sampling.py +101 -0
  45. ray360-0.1.0/tests/test_views.py +144 -0
  46. 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
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ .venv-e2e/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ benchmark-results*.json
12
+ .test-data/
13
+ .test-results/
14
+ .coverage*
15
+ htmlcov/
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()