diff options
Diffstat (limited to 'pixpat-python/pixpat/__init__.py')
| -rw-r--r-- | pixpat-python/pixpat/__init__.py | 489 |
1 files changed, 0 insertions, 489 deletions
diff --git a/pixpat-python/pixpat/__init__.py b/pixpat-python/pixpat/__init__.py deleted file mode 100644 index 6531662..0000000 --- a/pixpat-python/pixpat/__init__.py +++ /dev/null @@ -1,489 +0,0 @@ -"""Python bindings for libpixpat. - -Loads the bundled shared library via ctypes — no CPython extension. Works on -any CPython >= 3.9 for the wheel's target architecture. - -The library draws test patterns and converts pixel data directly into -caller-owned pixel buffers, in a wide range of pixel formats -(planar/semi-planar/packed YUV, RGB, raw, …). The caller is responsible for -allocating each plane with the correct size and stride for the chosen format. - -Pixel data is described by :class:`Buffer`, a passive struct mirroring the -C ``pixpat_buffer``: format name, width, height, one writable buffer per -plane, and one row stride per plane. Use :func:`draw_pattern` to fill a -buffer with a test pattern, and :func:`convert` to convert pixel data -between formats. - -Format names follow the convention used by `kms++` and `pixutils` -(e.g. ``"XRGB8888"``, ``"NV12"``, ``"YUYV"``) — not the DRM/V4L2 -four-character codes (``"XR24"``, etc.). - -Example: - >>> import pixpat - >>> width, height = 1920, 1080 - >>> stride = width * 4 # XRGB8888: 4 bytes per pixel - >>> data = bytearray(stride * height) - >>> buf = pixpat.Buffer(planes=[data], fmt="XRGB8888", - ... width=width, height=height, strides=[stride]) - >>> pixpat.draw_pattern(buf, "smpte") - -Use :func:`supported_formats` to enumerate the format names accepted by -``Buffer.fmt``, and :func:`is_supported` to test a single format. - -Using numpy buffers -------------------- - -pixpat does not import numpy and does not depend on it, but any -C-contiguous ``numpy.ndarray`` works as a plane via Python's buffer -protocol. The caller is responsible for matching the array's dtype and -shape to the pixel format and for passing the correct row stride: - - >>> import numpy as np - >>> arr = np.zeros((height, width, 4), dtype=np.uint8) - >>> stride = arr.strides[0] # bytes per row - >>> buf = pixpat.Buffer(planes=[arr], fmt="XRGB8888", - ... width=width, height=height, strides=[stride]) - >>> pixpat.draw_pattern(buf, "smpte") - -For multi-plane formats like ``"NV12"`` pass one ndarray per plane (e.g. -``(h, w)`` uint8 for Y and ``(h//2, w)`` uint8 for the interleaved UV -plane). - -If you already hold all planes in a single contiguous buffer — the layout -OpenCV uses for NV12, an ``(h * 3 // 2, w)`` uint8 array with Y on top -and the interleaved UV plane below — slice it into per-plane views and -pass those:: - - >>> nv12 = np.zeros((h * 3 // 2, w), dtype=np.uint8) - >>> y, uv = nv12[:h], nv12[h:].reshape(h // 2, w) - >>> pixpat.Buffer([y, uv], "NV12", w, h, - ... [y.strides[0], uv.strides[0]]) - -The source side of :func:`convert` accepts read-only buffers (``bytes``, -``arr.view()`` with ``writeable=False``, mmap'd files, …). The -destination side must always be writable. -""" - -import ctypes -from dataclasses import dataclass, field -from enum import IntEnum -from typing import Mapping, Optional, Sequence, Union - -from ._native import ( - _Buffer, - _ConvertOpts, - _PatternOpts, - _PinnedBuffers, - _fill_buffer, - _lib, -) - -_VALID_PATTERNS = frozenset( - { - 'kmstest', - 'smpte', - 'plain', - 'checker', - 'hramp', - 'vramp', - 'hbar', - 'vbar', - 'dramp', - 'zoneplate', - } -) - - -class Rec(IntEnum): - """YCbCr color encoding standard used for YUV output formats. - - Selects the matrix used to convert from internal RGB to YUV. Has no - effect when drawing into an RGB or raw format. - - Attributes: - BT601: ITU-R BT.601 (standard definition). - BT709: ITU-R BT.709 (HD). - BT2020: ITU-R BT.2020 (UHD / HDR, non-constant luminance). - """ - - BT601 = 0 - BT709 = 1 - BT2020 = 2 - - -class Range(IntEnum): - """Quantization range used for YUV output formats. - - Attributes: - LIMITED: "TV" / studio range — Y in [16, 235], C in [16, 240] - (scaled to bit depth). What most video pipelines expect. - FULL: "PC" / full range — every component uses the full code range - (e.g. [0, 255] for 8-bit). - """ - - LIMITED = 0 - FULL = 1 - - -class PixpatError(RuntimeError): - """Raised when the underlying ``pixpat_*`` call fails. - - Most commonly indicates an unknown format name or buffer dimensions / - plane layout that the format does not allow (e.g. odd width for a - horizontally-subsampled YUV format). - """ - - -@dataclass -class Buffer: - """Plane data + format + dimensions, mirroring the C ``pixpat_buffer``. - - A passive struct: no allocation, no format knowledge, no - numpy/cv2 awareness. The caller decides plane shapes and strides - based on its own format knowledge (e.g. via ``pixutils``). - - Attributes: - planes: One buffer-protocol object per plane (e.g. ``bytearray``, - ``array.array``, ``numpy.ndarray``, ``mmap.mmap``, - ``memoryview``). Up to 4 planes are supported. Each plane - must hold at least ``strides[i] * plane_height`` bytes, - where ``plane_height`` is ``height`` for the main plane and - ``height // vertical_subsampling`` for chroma planes (e.g. - ``height // 2`` for the UV plane of ``NV12``). For - destination buffers each plane must be writable; for the - source side of :func:`convert` read-only objects (such as - ``bytes``) are accepted. - fmt: Pixel-format name; see :func:`supported_formats`. - width: Image width in pixels. Some formats constrain this — for - chroma-subsampled YUV formats it must be a multiple of the - horizontal subsampling factor (e.g. 2 for ``NV12``), and - packed formats add a further multiple from their - pixel-group size (e.g. 6 for ``P030``, 4 for ``SBGGR10P``). - height: Image height in pixels. For vertically-subsampled - formats (e.g. ``NV12``, ``YUV420``) it must be a multiple - of the vertical subsampling factor. - strides: Per-plane row stride in bytes. Must have the same - length as ``planes``. Strides larger than the minimum row - size are allowed. - """ - - planes: Sequence - fmt: str - width: int - height: int - strides: Sequence[int] = field(default_factory=list) - - -def supported_formats() -> list[str]: - """Return the list of pixel-format names accepted by :class:`Buffer`. - - Format names follow the convention used by `kms++` and `pixutils` - (e.g. ``"XRGB8888"``, ``"NV12"``, ``"YUYV"``) — not the DRM/V4L2 - four-character codes. - """ - n = _lib.pixpat_format_count() - return [_lib.pixpat_format_name(i).decode('ascii') for i in range(n)] - - -def is_supported(fmt: str) -> bool: - """Return whether ``fmt`` is a known pixel-format name. - - Equivalent to ``fmt in supported_formats()`` but cheaper — it does not - materialize the full list. - """ - return bool(_lib.pixpat_format_supported(fmt.encode('ascii'))) - - -def _serialize_pattern_params( - params: Optional[Union[str, Mapping[str, object]]], -) -> Optional[bytes]: - """Encode `params` for the C ABI. - - Accepts a ready-made string (passed through verbatim) or a mapping of - string keys to stringifiable values, which is serialized to the - ``"key=val,key=val"`` form pixpat parses on the C side. Returns None - if there is nothing to pass. - """ - if params is None: - return None - if isinstance(params, str): - return params.encode('ascii') - if isinstance(params, Mapping): - items = [] - for k, v in params.items(): - if not isinstance(k, str): - raise TypeError(f'pattern params keys must be str, got {type(k).__name__}') - sv = str(v) - if ',' in k or '=' in k: - raise ValueError(f'pattern params key contains , or =: {k!r}') - if ',' in sv or '=' in sv: - raise ValueError(f'pattern params value contains , or =: {sv!r}') - items.append(f'{k}={sv}') - return ','.join(items).encode('ascii') - raise TypeError(f'pattern params must be None, str, or a Mapping, got {type(params).__name__}') - - -def draw_pattern( - dst: Buffer, - pattern: Optional[str] = None, - *, - rec: Rec = Rec.BT601, - color_range: Range = Range.LIMITED, - num_threads: int = 0, - params: Optional[Union[str, Mapping[str, object]]] = None, -) -> None: - """Draw a test pattern into ``dst``. - - Args: - dst: Destination buffer; all planes must be writable. - pattern: Name of the pattern to draw. ``None`` selects the default - (equivalent to ``"kmstest"``). Recognized values: - - * ``"kmstest"`` — color gradients with ramps, in the style of - the original ``kmstest`` tool. The default. - * ``"smpte"`` — SMPTE RP 219-1 color bars (with PLUGE). - * ``"plain"`` — solid color fill from ``params["color"]``. - See `params` below. - * ``"checker"`` — black/white checkerboard. Optional - ``params["cell"]`` (decimal positive integer, default 8) - sets the cell size in pixels. ``"cell": "1"`` is a - 1-pixel chroma-subsampling stress test. - * ``"hramp"`` — four horizontal stripes (R, G, B, gray), - each a 0..max ramp along x. Combined per-channel and - luma quantization check. - * ``"vramp"`` — same as ``"hramp"`` rotated 90°: four - vertical columns ramping along y. - * ``"hbar"`` — horizontal bar (full image width, narrow - along y) over a black background. Required - ``params["pos"]`` (signed integer, top edge in pixels); - optional ``params["width"]`` (positive integer, default - 32). The bar is split into seven equal-width regions - colored white/red/white/green/white/blue/white. - * ``"vbar"`` — same as ``"hbar"`` rotated 90°: vertical - bar with ``pos`` measured along x. - * ``"dramp"`` — diagonal RGB ramp (R on x, G on y, - B on x+y). - * ``"zoneplate"`` — centered radial cosine pattern, - frequency ramping from DC at the center to Nyquist at - the longer edge. Useful for spotting scaling/aliasing. - - ``"smpte"`` is defined by the spec in BT.709 / Limited. - Pass ``rec=Rec.BT709, color_range=Range.LIMITED`` for - spec-correct output; other settings produce visibly-wrong - colors when drawing into RGB sinks (the caller's matrix is - applied to BT.709-encoded values). Callers are trusted — - pixpat does not silently override the spec. - rec: YCbCr matrix for YUV formats. Ignored for RGB / raw formats. - Defaults to :attr:`Rec.BT601`. - color_range: Quantization range for YUV formats. Ignored for - RGB / raw formats. Defaults to :attr:`Range.LIMITED`. - num_threads: Worker-thread count. ``0`` selects a sensible - default (one per online CPU, capped to a sane maximum); - ``1`` runs single-threaded with no thread-spawn overhead; - ``N > 1`` uses exactly ``N`` workers. Defaults to ``0``. - Output is bit-identical regardless of the chosen count. - params: Optional pattern-specific parameters. Either a mapping - (``{"color": "ff0000"}``) — serialized to ``"color=ff0000"`` - for the C ABI — or a raw ``"key=val,key=val"`` string. - Unknown keys are silently ignored; patterns that don't read - params (``kmstest``, ``smpte``) ignore this entirely. - Per-pattern keys: - - * ``"plain"`` reads ``"color"`` as a hex RGB(A) string with - an optional ``"0x"`` prefix. The hex-digit count selects - the layout: 6 → 8-bit ``RRGGBB``, 8 → 8-bit ``AARRGGBB`` - (alpha first), 12 → 16-bit ``RRRRGGGGBBBB``, 16 → 16-bit - ``AAAARRRRGGGGBBBB``. Missing or malformed ``"color"`` - raises :class:`PixpatError`. - * ``"checker"`` reads optional ``"cell"`` as a positive - decimal integer; default 8. A non-positive or non-numeric - value raises :class:`PixpatError`. - * ``"hbar"`` / ``"vbar"`` read required ``"pos"`` (signed - decimal integer, top/left edge of the bar in pixels; - negative values clip at the edge) and optional ``"width"`` - (positive decimal integer, bar thickness; default 32). - Missing/non-numeric ``pos`` or non-positive ``width`` - raises :class:`PixpatError`. - - Raises: - ValueError: ``dst.planes`` exceeds the maximum plane count, - ``dst.strides`` length does not match ``dst.planes``, - ``pattern`` is not one of the recognized names, or a - ``params`` mapping value contains a forbidden ``,`` or ``=``. - TypeError: A plane is read-only (e.g. ``bytes``, a read-only - ``memoryview``), or ``params`` has an unsupported type. - PixpatError: The underlying C call failed — typically an unknown - ``fmt``, dimensions / strides incompatible with the format, - or pattern parameters that the pattern rejected. - - Example: - >>> w, h = 640, 480 - >>> stride = w * 4 - >>> data = bytearray(stride * h) - >>> dst = Buffer([data], "XRGB8888", w, h, [stride]) - >>> draw_pattern(dst, "smpte") - >>> draw_pattern(dst, "plain", params={"color": "ff0000"}) - """ - if pattern is None: - pattern = 'kmstest' - elif pattern not in _VALID_PATTERNS: - raise ValueError( - f'unknown pattern {pattern!r}; expected one of {sorted(_VALID_PATTERNS)} or None' - ) - if num_threads < 0: - raise ValueError(f'num_threads must be >= 0, got {num_threads}') - - params_bytes = _serialize_pattern_params(params) - - c_buf = _Buffer() - opts = _PatternOpts() - opts.rec = int(rec) - opts.range = int(color_range) - opts.num_threads = num_threads - opts.params = params_bytes - - with _PinnedBuffers() as pins: - _fill_buffer( - c_buf, - pins, - dst.planes, - dst.fmt, - dst.width, - dst.height, - dst.strides, - writable=True, - ) - rc = _lib.pixpat_draw_pattern( - ctypes.byref(c_buf), pattern.encode('ascii'), ctypes.byref(opts) - ) - - if rc != 0: - raise PixpatError(f'pixpat_draw_pattern failed (rc={rc}); check format name and dimensions') - - -def convert( - dst: Buffer, - src: Buffer, - *, - rec: Rec = Rec.BT601, - color_range: Range = Range.LIMITED, - num_threads: int = 0, -) -> None: - """Convert pixel data from ``src`` into ``dst``. - - Both buffers must describe an image of the same width and height; - only the pixel format may differ. Any format accepted by - :func:`supported_formats` works as both source and destination in - the default build (custom build profiles can mark individual formats - read-only or write-only). Conversion is routed internally through a - 16-bit normalized RGB or YUV intermediate, so format-to-format - conversions in either direction (e.g. ``NV12`` -> ``YUV420``, - ``XRGB8888`` -> ``NV12``, ``SRGGB10`` -> ``BGR888``) are a single - call. Bayer sources are decoded with a 3x3 bilinear demosaic. - - ``src.planes`` may hold read-only buffers (e.g. ``bytes``, mmap'd - files, numpy arrays with ``writeable=False``); ``dst.planes`` must - be writable. - - Args: - dst: Destination buffer. - src: Source buffer. - rec: YCbCr matrix used when the conversion crosses the RGB/YUV - boundary. Ignored when ``src.fmt`` and ``dst.fmt`` share - the same color kind. Defaults to :attr:`Rec.BT601`. - color_range: Quantization range used when the conversion crosses - the RGB/YUV boundary. Ignored when ``src.fmt`` and - ``dst.fmt`` share the same color kind. Defaults to - :attr:`Range.LIMITED`. - num_threads: Worker-thread count. ``0`` selects a sensible - default (one per online CPU, capped to a sane maximum); - ``1`` runs single-threaded with no thread-spawn overhead; - ``N > 1`` uses exactly ``N`` workers. Defaults to ``0``. - Output is bit-identical regardless of the chosen count. - - Raises: - ValueError: ``dst`` and ``src`` have mismatched dimensions, a - plane sequence exceeds the maximum plane count, or a - strides length does not match its planes length. - TypeError: A ``dst`` plane is read-only. - PixpatError: The underlying C call failed — typically an - unknown format name, a format disabled in the current build - (or disabled in the requested direction), or dimensions / - strides incompatible with one of the formats. - - Example: - Cross-color-kind, multi-plane on both sides — paint an NV12 - source via :func:`draw_pattern`, then convert it to planar - YUV420: - - >>> w, h = 64, 32 - >>> y_src = bytearray(w * h) - >>> uv_src = bytearray(w * h // 2) - >>> draw_pattern(Buffer([y_src, uv_src], "NV12", w, h, [w, w]), - ... "smpte") - >>> y_dst = bytearray(w * h) - >>> u_dst = bytearray(w * h // 4) - >>> v_dst = bytearray(w * h // 4) - >>> convert( - ... Buffer([y_dst, u_dst, v_dst], "YUV420", w, h, - ... [w, w // 2, w // 2]), - ... Buffer([y_src, uv_src], "NV12", w, h, [w, w]), - ... ) - """ - if dst.width != src.width or dst.height != src.height: - raise ValueError( - f'dst dimensions {dst.width}x{dst.height} do not match ' - f'src dimensions {src.width}x{src.height}' - ) - if num_threads < 0: - raise ValueError(f'num_threads must be >= 0, got {num_threads}') - - c_dst = _Buffer() - c_src = _Buffer() - opts = _ConvertOpts() - opts.rec = int(rec) - opts.range = int(color_range) - opts.num_threads = num_threads - - with _PinnedBuffers() as pins: - _fill_buffer( - c_dst, - pins, - dst.planes, - dst.fmt, - dst.width, - dst.height, - dst.strides, - writable=True, - role='dst', - ) - _fill_buffer( - c_src, - pins, - src.planes, - src.fmt, - src.width, - src.height, - src.strides, - writable=False, - role='src', - ) - rc = _lib.pixpat_convert(ctypes.byref(c_dst), ctypes.byref(c_src), ctypes.byref(opts)) - - if rc != 0: - raise PixpatError( - f'pixpat_convert failed (rc={rc}); check format names, dimensions, ' - f'and that src.fmt is a supported source format' - ) - - -__all__ = [ - 'Buffer', - 'Rec', - 'Range', - 'PixpatError', - 'supported_formats', - 'is_supported', - 'draw_pattern', - 'convert', -] |
