pypixelpack 0.2.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.
- pypixelpack-0.2.0/.flywheel.yml +7 -0
- pypixelpack-0.2.0/.github/workflows/ci.yml +48 -0
- pypixelpack-0.2.0/.github/workflows/flywheel-pr.yml +26 -0
- pypixelpack-0.2.0/.github/workflows/flywheel-push.yml +22 -0
- pypixelpack-0.2.0/.github/workflows/governance-lint.yml +11 -0
- pypixelpack-0.2.0/.github/workflows/quality.yml +55 -0
- pypixelpack-0.2.0/.gitignore +10 -0
- pypixelpack-0.2.0/.markdownlint-cli2.jsonc +21 -0
- pypixelpack-0.2.0/.markdownlint.json +1 -0
- pypixelpack-0.2.0/.python-version +1 -0
- pypixelpack-0.2.0/.vale.ini +8 -0
- pypixelpack-0.2.0/CHANGELOG.md +6 -0
- pypixelpack-0.2.0/LICENSE +28 -0
- pypixelpack-0.2.0/PKG-INFO +138 -0
- pypixelpack-0.2.0/README.md +111 -0
- pypixelpack-0.2.0/REQUIREMENTS.md +96 -0
- pypixelpack-0.2.0/ROADMAP.md +7 -0
- pypixelpack-0.2.0/SPEC.md +178 -0
- pypixelpack-0.2.0/pypixelpack/__init__.py +29 -0
- pypixelpack-0.2.0/pypixelpack/_backend.py +72 -0
- pypixelpack-0.2.0/pypixelpack/encoding.py +287 -0
- pypixelpack-0.2.0/pypixelpack/layouts.py +333 -0
- pypixelpack-0.2.0/pypixelpack/py.typed +0 -0
- pypixelpack-0.2.0/pyproject.toml +101 -0
- pypixelpack-0.2.0/styles/Requirements/FillerPhrases.yml +12 -0
- pypixelpack-0.2.0/styles/Requirements/MustDeprecated.yml +7 -0
- pypixelpack-0.2.0/styles/Requirements/OrdinalHeadings.yml +7 -0
- pypixelpack-0.2.0/styles/Requirements/WillDeprecated.yml +7 -0
- pypixelpack-0.2.0/tests/conftest.py +35 -0
- pypixelpack-0.2.0/tests/test_backend.py +43 -0
- pypixelpack-0.2.0/tests/test_backend_parity.py +148 -0
- pypixelpack-0.2.0/tests/test_encoding.py +287 -0
- pypixelpack-0.2.0/tests/test_layouts.py +319 -0
- pypixelpack-0.2.0/tests/test_package.py +30 -0
- pypixelpack-0.2.0/tools/check_core_install.py +66 -0
- pypixelpack-0.2.0/uv.lock +420 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
# ruff and pyright are pinned to py312 in pyproject.toml, so one leg
|
|
10
|
+
# carries the full lint signal; only pytest is version-sensitive.
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: uv sync
|
|
19
|
+
- run: uv run ruff format --check .
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run pyright
|
|
22
|
+
|
|
23
|
+
test:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
strategy:
|
|
26
|
+
matrix:
|
|
27
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v5
|
|
30
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ matrix.python-version }}
|
|
33
|
+
- run: uv sync
|
|
34
|
+
- run: uv run pytest
|
|
35
|
+
|
|
36
|
+
# Prove the numpy-only core: install the built wheel into a clean
|
|
37
|
+
# environment and run the core probe (§spec:package-shape).
|
|
38
|
+
core-install:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v5
|
|
42
|
+
with:
|
|
43
|
+
fetch-depth: 0 # hatch-vcs derives the version from tag history
|
|
44
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.12"
|
|
47
|
+
- run: uv build
|
|
48
|
+
- run: uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Flywheel — PR
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
types: [opened, synchronize, reopened, ready_for_review, edited]
|
|
5
|
+
concurrency:
|
|
6
|
+
group: flywheel-pr-${{ github.event.pull_request.number }}
|
|
7
|
+
cancel-in-progress: true
|
|
8
|
+
jobs:
|
|
9
|
+
conduct:
|
|
10
|
+
# Skip drafts; ignore `edited` events from bots (only a human editing the
|
|
11
|
+
# title/body should retrigger the conventional-commit rewrite). Skips
|
|
12
|
+
# cleanly until the flywheel GitHub App is installed on the org and its
|
|
13
|
+
# credentials are configured.
|
|
14
|
+
if: |
|
|
15
|
+
vars.FLYWHEEL_GH_APP_ID != '' &&
|
|
16
|
+
github.event.pull_request.draft == false &&
|
|
17
|
+
(github.event.action != 'edited' || github.event.sender.type == 'User')
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
# SHA-pinned (no floating tags for third-party actions); the trailing
|
|
21
|
+
# # vX.Y.Z comment is for human review. Mirrors pydecklink's setup.
|
|
22
|
+
- uses: point-source/flywheel@59758d1940ebe054ce9fdd4b7a7dc79a0cf235e0 # v2.1.0
|
|
23
|
+
with:
|
|
24
|
+
event: pull_request
|
|
25
|
+
app-id: ${{ vars.FLYWHEEL_GH_APP_ID }}
|
|
26
|
+
app-private-key: ${{ secrets.FLYWHEEL_GH_APP_PRIVATE_KEY }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Flywheel — Push
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: ["**"]
|
|
5
|
+
concurrency:
|
|
6
|
+
group: flywheel-push-${{ github.ref_name }}
|
|
7
|
+
cancel-in-progress: false
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
# Skips cleanly until the flywheel GitHub App is installed on the org
|
|
11
|
+
# and its credentials are configured (vars.FLYWHEEL_GH_APP_ID,
|
|
12
|
+
# secrets.FLYWHEEL_GH_APP_PRIVATE_KEY).
|
|
13
|
+
if: vars.FLYWHEEL_GH_APP_ID != ''
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
# SHA-pinned (no floating tags for third-party actions); the trailing
|
|
17
|
+
# # vX.Y.Z comment is for human review. Mirrors pydecklink's setup.
|
|
18
|
+
- uses: point-source/flywheel@59758d1940ebe054ce9fdd4b7a7dc79a0cf235e0 # v2.1.0
|
|
19
|
+
with:
|
|
20
|
+
event: push
|
|
21
|
+
app-id: ${{ vars.FLYWHEEL_GH_APP_ID }}
|
|
22
|
+
app-private-key: ${{ secrets.FLYWHEEL_GH_APP_PRIVATE_KEY }}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: quality
|
|
2
|
+
|
|
3
|
+
# The two checks the `Flywheel - review` ruleset requires by name —
|
|
4
|
+
# `markdownlint` and `shellcheck` — and which nothing in this repository
|
|
5
|
+
# emitted, so every pull request into the default branch sat at "Expected —
|
|
6
|
+
# Waiting for status to be reported" and could not merge. The job keys below
|
|
7
|
+
# ARE the check names the ruleset matches; renaming one silently re-blocks
|
|
8
|
+
# the branch.
|
|
9
|
+
#
|
|
10
|
+
# Neither ran here under those names: markdownlint only as a step inside the
|
|
11
|
+
# reusable governance-lint workflow, and shellcheck not at all — this
|
|
12
|
+
# repository ships no shell scripts, so that job passes on an empty set and
|
|
13
|
+
# exists to satisfy the context. A step's name is not a check's name, which is
|
|
14
|
+
# why the ruleset never saw them.
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
merge_group:
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
# Skip on Flywheel's own release and back-merge commits, which carry
|
|
24
|
+
# generated content rather than reviewable changes. The guard is per job so
|
|
25
|
+
# each required check still resolves rather than hanging — a skipped job
|
|
26
|
+
# satisfies a required context, an absent one does not.
|
|
27
|
+
markdownlint:
|
|
28
|
+
if: >-
|
|
29
|
+
!contains(github.event.pull_request.title || '', ': promote ') &&
|
|
30
|
+
!startsWith(github.event.merge_group.head_commit.message || '', 'chore(release):') &&
|
|
31
|
+
!startsWith(github.event.merge_group.head_commit.message || '', 'chore: back-merge')
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
35
|
+
# Root governance and landing documents, matching the sibling
|
|
36
|
+
# repositories. Config is .markdownlint.json plus the ignore list in
|
|
37
|
+
# .markdownlint-cli2.jsonc.
|
|
38
|
+
- uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24
|
|
39
|
+
with:
|
|
40
|
+
globs: "*.md"
|
|
41
|
+
|
|
42
|
+
shellcheck:
|
|
43
|
+
if: >-
|
|
44
|
+
!contains(github.event.pull_request.title || '', ': promote ') &&
|
|
45
|
+
!startsWith(github.event.merge_group.head_commit.message || '', 'chore(release):') &&
|
|
46
|
+
!startsWith(github.event.merge_group.head_commit.message || '', 'chore: back-merge')
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
50
|
+
# `severity: error` is the enforced baseline, as in the sibling
|
|
51
|
+
# repositories: the floor keeps an info-level finding — SC1091 from a
|
|
52
|
+
# source this action does not follow, say — out of a required gate.
|
|
53
|
+
- uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0
|
|
54
|
+
with:
|
|
55
|
+
severity: error
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
// CHANGELOG.md is generated by Flywheel/semantic-release in
|
|
3
|
+
// conventional-changelog format, which opens each release with an h1 and
|
|
4
|
+
// then jumps to h3 for the section headings — MD001, and not ours to fix:
|
|
5
|
+
// the file is rewritten from the commit history on every release, so a
|
|
6
|
+
// correction would be overwritten by the next one and a release would be
|
|
7
|
+
// able to fail the branch's own required checks.
|
|
8
|
+
//
|
|
9
|
+
// The rest are directories a local `markdownlint-cli2` run would otherwise
|
|
10
|
+
// walk: .venv/ is the environment uv creates and .claude/ the worktrees this
|
|
11
|
+
// repository's own workflow makes. CI lints the root *.md alone
|
|
12
|
+
// (.github/workflows/quality.yml), so these matter only to a developer
|
|
13
|
+
// running the tool by hand — which is exactly when an ignore list earns its
|
|
14
|
+
// place.
|
|
15
|
+
"ignores": [
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"node_modules/**",
|
|
18
|
+
".venv/**",
|
|
19
|
+
".claude/**"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"MD013": false, "MD024": false, "MD036": false}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# [0.2.0](https://github.com/Fuse-Technical-Group/pypixelpack/compare/v0.1.0...v0.2.0) (2026-08-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **encoding:** expose the achromatic chroma code ([b39317c](https://github.com/Fuse-Technical-Group/pypixelpack/commit/b39317cad4034e78ae7bda17905e10ed27f6b15f))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fuse Technical Group
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pypixelpack
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Pixel layouts and wire encodings for video I/O, device-free
|
|
5
|
+
Project-URL: Homepage, https://github.com/Fuse-Technical-Group/pypixelpack
|
|
6
|
+
Project-URL: Repository, https://github.com/Fuse-Technical-Group/pypixelpack.git
|
|
7
|
+
Project-URL: Issues, https://github.com/Fuse-Technical-Group/pypixelpack/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Fuse-Technical-Group/pypixelpack/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Ritchie Argue <4462072+repentsinner@users.noreply.github.com>
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: decklink,pixel-format,r210,sdi,st2110,v210,ycbcr
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Multimedia :: Video
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Requires-Dist: numpy>=2.3.1
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# pypixelpack
|
|
29
|
+
|
|
30
|
+
**Pixel layouts and wire encodings for video I/O, device-free.**
|
|
31
|
+
|
|
32
|
+
The bytes a video frame becomes on a wire — v210, r210, the ST 2110-20
|
|
33
|
+
pgroup — and the encoding between a frame's RGB and those bytes: colour
|
|
34
|
+
matrix, range, chroma subsampling. Written once against a caller-supplied
|
|
35
|
+
array namespace, so the same source packs on numpy for a host frame and
|
|
36
|
+
on torch for a frame that never leaves a GPU.
|
|
37
|
+
|
|
38
|
+
Extracted from [pydecklink](https://github.com/Fuse-Technical-Group/pydecklink)'s
|
|
39
|
+
packing module and a GPU render pipeline's colorspace node, which held
|
|
40
|
+
the same layout twice. This repository's [SPEC.md](SPEC.md) and
|
|
41
|
+
[ROADMAP.md](ROADMAP.md) govern the package.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv add pypixelpack
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
numpy is the only dependency. torch is a namespace the caller supplies,
|
|
50
|
+
never a dependency of this package (`§spec:backend`).
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import numpy as np
|
|
56
|
+
from pypixelpack import decode, encode, pack, unpack
|
|
57
|
+
|
|
58
|
+
rgb = np.zeros((1080, 1920, 3), dtype=np.float32) # [R, G, B] in [0, 1]
|
|
59
|
+
codes = encode(rgb, subsampling="422") # (H, W, 3) uint16 [Y, Cb, Cr]
|
|
60
|
+
data = pack(codes, "v210", row_bytes=5120) # 1-D uint8, DMA-ready
|
|
61
|
+
back = unpack(data, "v210", width=1920, height=1080, row_bytes=5120)
|
|
62
|
+
rgb_again = decode(back, subsampling="422") # float32 [R, G, B] in [0, 1]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On a GPU host, pass the namespace and the arrays stay resident:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import torch
|
|
69
|
+
|
|
70
|
+
codes = encode(rgb_on_cuda, subsampling="422", xp=torch)
|
|
71
|
+
data = pack(codes, "v210", row_bytes=5120, xp=torch)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
- `pack(pixels, layout, row_bytes, *, xp=numpy)` — `(H, W, 3)` integer
|
|
77
|
+
samples to a 1-D `uint8` buffer of `H × row_bytes` in `layout`, on the
|
|
78
|
+
input's device (`§spec:layouts`). RGB layouts take `[R, G, B]`; `v210`
|
|
79
|
+
and `2vuy` take `[Y, Cb, Cr]` with chroma read from even columns.
|
|
80
|
+
- `unpack(data, layout, width, height, row_bytes, *, xp=numpy)` — the
|
|
81
|
+
inverse; `unpack(pack(x)) == x` for every layout. Returns `uint8` for
|
|
82
|
+
8-bit layouts and `uint16` otherwise.
|
|
83
|
+
- `row_bytes(layout, width)` — the smallest `row_bytes` that holds a line.
|
|
84
|
+
- `LAYOUTS` — the layout table, `name → (pixels per group, bytes per
|
|
85
|
+
group, bit depth)`: `argb`, `bgra`, `r210`, `r10b`, `r10l`, `v210`,
|
|
86
|
+
`2vuy`, `r12b`, `r12l`.
|
|
87
|
+
|
|
88
|
+
`pack` raises `ValueError` for an unknown layout, a `row_bytes` shorter
|
|
89
|
+
than the packed line, or a sample above the layout's bit depth; the
|
|
90
|
+
last check is skipped under `torch.compile`, where a compiled caller
|
|
91
|
+
trusts its own inputs (`§spec:backend`).
|
|
92
|
+
|
|
93
|
+
### Encoding
|
|
94
|
+
|
|
95
|
+
- `encode(rgb, *, matrix="bt709", levels="narrow", layout=None,
|
|
96
|
+
bits=None, subsampling=None, xp=numpy)` — `(H, W, 3)` float RGB in
|
|
97
|
+
`[0, 1]` to `(H, W, 3)` `uint16` `[Y, Cb, Cr]` (`§spec:encoding`).
|
|
98
|
+
`matrix` is `bt709` or `bt2020`; `levels` is `narrow` (luma 16–235,
|
|
99
|
+
chroma 16–240 at 8 bits, shifted up by `bits - 8`) or `full`.
|
|
100
|
+
`layout="v210"` selects the depth and subsampling that wire format
|
|
101
|
+
expects — 10-bit 4:2:2; `2vuy` is 8-bit 4:2:2 — and a `bits` or
|
|
102
|
+
`subsampling` that contradicts it is refused; without a layout they
|
|
103
|
+
default to 10 and `444`. `422` averages chroma over each horizontal
|
|
104
|
+
pair and writes it to both pixels, the shape `pack` reads. Rounding is
|
|
105
|
+
half to even, arithmetic is float32 on every backend, and codes clamp
|
|
106
|
+
to the level range's span.
|
|
107
|
+
- `decode(ycbcr, *, matrix, levels, layout=None, bits=None, xp=numpy)`
|
|
108
|
+
— the inverse, per pixel, to within half a code per component;
|
|
109
|
+
returns `float32` RGB clamped to `[0, 1]`.
|
|
110
|
+
- `encoding_for(layout)` — the `(bits, subsampling)` a layout carries;
|
|
111
|
+
`ENCODINGS` is the table behind it.
|
|
112
|
+
- `legal_codes(*, levels="narrow", bits=10)` — the `(luma, chroma)` code
|
|
113
|
+
spans as `range` objects; `len(legal_codes().luma)` is 877, the levels
|
|
114
|
+
10-bit narrow range can represent.
|
|
115
|
+
- `MATRICES` — `name → (KR, KB)`: `bt709`, `bt2020`.
|
|
116
|
+
|
|
117
|
+
`encode` and `decode` raise `ValueError` for an unknown matrix, levels or
|
|
118
|
+
subsampling, a `bits` outside 8–16, or a layout with no component
|
|
119
|
+
encoding.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
uv sync
|
|
125
|
+
uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`uv sync` installs torch (CPU) into the dev group so the parity suite
|
|
129
|
+
runs; without it those tests skip. To prove the wheel on numpy alone:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
uv build
|
|
133
|
+
uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
BSD-3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# pypixelpack
|
|
2
|
+
|
|
3
|
+
**Pixel layouts and wire encodings for video I/O, device-free.**
|
|
4
|
+
|
|
5
|
+
The bytes a video frame becomes on a wire — v210, r210, the ST 2110-20
|
|
6
|
+
pgroup — and the encoding between a frame's RGB and those bytes: colour
|
|
7
|
+
matrix, range, chroma subsampling. Written once against a caller-supplied
|
|
8
|
+
array namespace, so the same source packs on numpy for a host frame and
|
|
9
|
+
on torch for a frame that never leaves a GPU.
|
|
10
|
+
|
|
11
|
+
Extracted from [pydecklink](https://github.com/Fuse-Technical-Group/pydecklink)'s
|
|
12
|
+
packing module and a GPU render pipeline's colorspace node, which held
|
|
13
|
+
the same layout twice. This repository's [SPEC.md](SPEC.md) and
|
|
14
|
+
[ROADMAP.md](ROADMAP.md) govern the package.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uv add pypixelpack
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
numpy is the only dependency. torch is a namespace the caller supplies,
|
|
23
|
+
never a dependency of this package (`§spec:backend`).
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import numpy as np
|
|
29
|
+
from pypixelpack import decode, encode, pack, unpack
|
|
30
|
+
|
|
31
|
+
rgb = np.zeros((1080, 1920, 3), dtype=np.float32) # [R, G, B] in [0, 1]
|
|
32
|
+
codes = encode(rgb, subsampling="422") # (H, W, 3) uint16 [Y, Cb, Cr]
|
|
33
|
+
data = pack(codes, "v210", row_bytes=5120) # 1-D uint8, DMA-ready
|
|
34
|
+
back = unpack(data, "v210", width=1920, height=1080, row_bytes=5120)
|
|
35
|
+
rgb_again = decode(back, subsampling="422") # float32 [R, G, B] in [0, 1]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
On a GPU host, pass the namespace and the arrays stay resident:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import torch
|
|
42
|
+
|
|
43
|
+
codes = encode(rgb_on_cuda, subsampling="422", xp=torch)
|
|
44
|
+
data = pack(codes, "v210", row_bytes=5120, xp=torch)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
- `pack(pixels, layout, row_bytes, *, xp=numpy)` — `(H, W, 3)` integer
|
|
50
|
+
samples to a 1-D `uint8` buffer of `H × row_bytes` in `layout`, on the
|
|
51
|
+
input's device (`§spec:layouts`). RGB layouts take `[R, G, B]`; `v210`
|
|
52
|
+
and `2vuy` take `[Y, Cb, Cr]` with chroma read from even columns.
|
|
53
|
+
- `unpack(data, layout, width, height, row_bytes, *, xp=numpy)` — the
|
|
54
|
+
inverse; `unpack(pack(x)) == x` for every layout. Returns `uint8` for
|
|
55
|
+
8-bit layouts and `uint16` otherwise.
|
|
56
|
+
- `row_bytes(layout, width)` — the smallest `row_bytes` that holds a line.
|
|
57
|
+
- `LAYOUTS` — the layout table, `name → (pixels per group, bytes per
|
|
58
|
+
group, bit depth)`: `argb`, `bgra`, `r210`, `r10b`, `r10l`, `v210`,
|
|
59
|
+
`2vuy`, `r12b`, `r12l`.
|
|
60
|
+
|
|
61
|
+
`pack` raises `ValueError` for an unknown layout, a `row_bytes` shorter
|
|
62
|
+
than the packed line, or a sample above the layout's bit depth; the
|
|
63
|
+
last check is skipped under `torch.compile`, where a compiled caller
|
|
64
|
+
trusts its own inputs (`§spec:backend`).
|
|
65
|
+
|
|
66
|
+
### Encoding
|
|
67
|
+
|
|
68
|
+
- `encode(rgb, *, matrix="bt709", levels="narrow", layout=None,
|
|
69
|
+
bits=None, subsampling=None, xp=numpy)` — `(H, W, 3)` float RGB in
|
|
70
|
+
`[0, 1]` to `(H, W, 3)` `uint16` `[Y, Cb, Cr]` (`§spec:encoding`).
|
|
71
|
+
`matrix` is `bt709` or `bt2020`; `levels` is `narrow` (luma 16–235,
|
|
72
|
+
chroma 16–240 at 8 bits, shifted up by `bits - 8`) or `full`.
|
|
73
|
+
`layout="v210"` selects the depth and subsampling that wire format
|
|
74
|
+
expects — 10-bit 4:2:2; `2vuy` is 8-bit 4:2:2 — and a `bits` or
|
|
75
|
+
`subsampling` that contradicts it is refused; without a layout they
|
|
76
|
+
default to 10 and `444`. `422` averages chroma over each horizontal
|
|
77
|
+
pair and writes it to both pixels, the shape `pack` reads. Rounding is
|
|
78
|
+
half to even, arithmetic is float32 on every backend, and codes clamp
|
|
79
|
+
to the level range's span.
|
|
80
|
+
- `decode(ycbcr, *, matrix, levels, layout=None, bits=None, xp=numpy)`
|
|
81
|
+
— the inverse, per pixel, to within half a code per component;
|
|
82
|
+
returns `float32` RGB clamped to `[0, 1]`.
|
|
83
|
+
- `encoding_for(layout)` — the `(bits, subsampling)` a layout carries;
|
|
84
|
+
`ENCODINGS` is the table behind it.
|
|
85
|
+
- `legal_codes(*, levels="narrow", bits=10)` — the `(luma, chroma)` code
|
|
86
|
+
spans as `range` objects; `len(legal_codes().luma)` is 877, the levels
|
|
87
|
+
10-bit narrow range can represent.
|
|
88
|
+
- `MATRICES` — `name → (KR, KB)`: `bt709`, `bt2020`.
|
|
89
|
+
|
|
90
|
+
`encode` and `decode` raise `ValueError` for an unknown matrix, levels or
|
|
91
|
+
subsampling, a `bits` outside 8–16, or a layout with no component
|
|
92
|
+
encoding.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
uv sync
|
|
98
|
+
uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`uv sync` installs torch (CPU) into the dev group so the parity suite
|
|
102
|
+
runs; without it those tests skip. To prove the wheel on numpy alone:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
uv build
|
|
106
|
+
uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
BSD-3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Requirements
|
|
2
|
+
|
|
3
|
+
Problem-space document for pypixelpack: pixel layouts and wire encodings
|
|
4
|
+
as a device-free library. Extracted from
|
|
5
|
+
[pydecklink](https://github.com/Fuse-Technical-Group/pydecklink)
|
|
6
|
+
(`§spec:pixel-packing` there) and a GPU render pipeline that outputs to
|
|
7
|
+
SDI and ST 2110.
|
|
8
|
+
|
|
9
|
+
## Problem statement §req:problem-statement
|
|
10
|
+
|
|
11
|
+
Target users are the maintainers of video I/O tools in this organization
|
|
12
|
+
and the display-measurement tools that drive them: a DeckLink binding, a
|
|
13
|
+
GPU render pipeline that outputs to SDI and ST 2110, a signal generator,
|
|
14
|
+
and a measurement session that puts exact code values on a wire.
|
|
15
|
+
|
|
16
|
+
The same v210 layout exists twice. pydecklink packs it in numpy on the
|
|
17
|
+
host for a DMA buffer; a GPU render pipeline packs it in torch on the
|
|
18
|
+
device and DMAs the result, because a round trip through host memory costs a full
|
|
19
|
+
uncompressed frame across PCIe every frame. Each is correct for its
|
|
20
|
+
caller and neither serves the other, so the next wire format — r210 for
|
|
21
|
+
RGB 4:4:4 over SDI — recurs the same way.
|
|
22
|
+
|
|
23
|
+
The colour encoding between RGB and those samples lives in one of the two
|
|
24
|
+
and is hard-bound to one matrix. A measurement session on SDI has to
|
|
25
|
+
declare that encoding and cannot implement it without a second copy,
|
|
26
|
+
and a wrong matrix or range offsets every code in a way nothing
|
|
27
|
+
downstream can detect.
|
|
28
|
+
|
|
29
|
+
## Success criteria §req:success-criteria
|
|
30
|
+
|
|
31
|
+
- pydecklink consumes this library for its layouts, keeps only the map
|
|
32
|
+
from its SDK enum to a layout name, and `pack`/`unpack` output is
|
|
33
|
+
byte-identical before and after.
|
|
34
|
+
- The GPU render pipeline consumes this library on torch, on the
|
|
35
|
+
device, with no host round trip, and a test proves the compiled path is taken.
|
|
36
|
+
- The same source packs a frame on numpy and on torch to identical
|
|
37
|
+
bytes.
|
|
38
|
+
- Every layout round-trips: `unpack(pack(x)) == x`.
|
|
39
|
+
- display-measure declares an SDI encoding by name and matrix and never
|
|
40
|
+
carries a conversion of its own.
|
|
41
|
+
|
|
42
|
+
## User stories §req:user-stories
|
|
43
|
+
|
|
44
|
+
- As the pydecklink maintainer, I delete the layout implementations and
|
|
45
|
+
keep the enum map, so the transport binding holds nothing but the
|
|
46
|
+
vendor's surface.
|
|
47
|
+
- As a render-pipeline developer, I pack v210 and r210 on the GPU and
|
|
48
|
+
DMA the bytes, so the frame never crosses the bus uncompressed.
|
|
49
|
+
- As a measurement-session author, I declare "10-bit BT.709 narrow-range
|
|
50
|
+
4:2:2 in v210" and drive RGB patches through it, so the encoding on
|
|
51
|
+
the wire is the one the artifact records.
|
|
52
|
+
- As a UHD integrator, I select BT.2020, so the encoding follows the
|
|
53
|
+
signal rather than a constant.
|
|
54
|
+
|
|
55
|
+
## Quality attributes §req:quality-attributes
|
|
56
|
+
|
|
57
|
+
- **Exactness.** Layouts are bit-exact against the format's own
|
|
58
|
+
reference tables; encodings state their rounding and honor it.
|
|
59
|
+
- **Determinism.** Same inputs, same bytes, on every backend.
|
|
60
|
+
- **Footprint.** numpy is the only dependency; torch is supplied by the
|
|
61
|
+
caller.
|
|
62
|
+
- **Device residency.** No function forces a transfer to host memory.
|
|
63
|
+
- **Fusion.** The torch path stays traceable by `torch.compile`; a
|
|
64
|
+
change that silently drops it to eager fails a test.
|
|
65
|
+
|
|
66
|
+
## Constraints §req:constraints
|
|
67
|
+
|
|
68
|
+
- Public repository under Fuse-Technical-Group; BSD-3-Clause;
|
|
69
|
+
published to PyPI as `pypixelpack`.
|
|
70
|
+
- Seeded by extraction: pydecklink's layouts and the GPU pipeline's
|
|
71
|
+
encoding move verbatim before any reshaping; adoption is behind one
|
|
72
|
+
release each.
|
|
73
|
+
- No transport and no device: what a byte buffer is *for* — a DeckLink
|
|
74
|
+
frame, an RTP payload — belongs to the consumer.
|
|
75
|
+
- No resizing, no colour management beyond the stated matrix and range:
|
|
76
|
+
the library encodes what it is given.
|
|
77
|
+
- Renders into a caller-supplied array namespace on the same contract
|
|
78
|
+
display-patterns established.
|
|
79
|
+
|
|
80
|
+
## Priorities §req:priorities
|
|
81
|
+
|
|
82
|
+
Essential, in adoption order:
|
|
83
|
+
|
|
84
|
+
1. The eight layouts pydecklink holds, namespace-generic, with
|
|
85
|
+
reference vectors and numpy/torch byte-identity.
|
|
86
|
+
2. pydecklink adoption; first release.
|
|
87
|
+
3. BT.709 and BT.2020 encoding at narrow and full range, chroma
|
|
88
|
+
subsampling by pair average.
|
|
89
|
+
4. GPU render pipeline adoption on the device, with fusion asserted.
|
|
90
|
+
|
|
91
|
+
Nice-to-have, after adoption:
|
|
92
|
+
|
|
93
|
+
- `2vuy`, which no current implementation packs.
|
|
94
|
+
- Provenance for `r10b`, `r10l`, `r12b`, `r12l` against FFmpeg's
|
|
95
|
+
format list.
|
|
96
|
+
- The ST 2110-20 pgroup, if a second consumer appears.
|