Skip to main content

FerrumPixel

Rust-accelerated image preprocessing for Python.

Install · Quickstart · API reference · Semantics · Concurrency · Performance


FerrumPixel gives you the operations an ML pipeline actually needs — resize, crop, rotate, brightness, contrast, equalize_histogram, sharpen, normalize — behind a small chainable API that hands you a NumPy array at the end. The pixel work happens in Rust, with the GIL released for the whole of every call. It is built for the layer between your web framework and your model, where preprocessing runs on every request and the GIL decides how much of your machine you actually get to use.

import ferrumpixel as fp

arr = (fp.load("image.jpg")
       .resize(512, 512)
       .sharpen()
       .normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
       .numpy())        # (512, 512, 3) float32, RGB

Table of contents


What it replaces, and why

The pipeline FerrumPixel targets is the familiar PIL → OpenCV → NumPy chain: open with PIL, convert to an array, run a few OpenCV ops, normalize with NumPy. That works, but it carries three costs.

The GIL. Those libraries release the GIL unevenly, and a mixed chain reacquires it repeatedly, so preprocessing serialises against the rest of your process. FerrumPixel releases the GIL for the entire duration of every call, single-image and batch alike. In a threaded server that is the difference between preprocessing blocking your request handlers and running alongside them.

Copies. Each hand-off between PIL, OpenCV and NumPy is a conversion and usually a copy. FerrumPixel keeps one buffer in Rust for the whole chain and copies out exactly once, when you call .numpy().

Undefined semantics. In the usual chain, rotation direction, contrast pivot and rounding behaviour depend on which library you happened to reach for. FerrumPixel fixes them once and versions them — see Pixel semantics.

Batches are the case FerrumPixel is strongest at: they dispatch across Rayon's thread pool with the GIL released for the whole call, so throughput scales with cores rather than with the interpreter.

Install

pip install ferrumpixel

Wheels are abi3, so a single wheel per platform covers CPython 3.9 through 3.13. No Rust toolchain is needed to install.

Platform Architectures
Linux (manylinux 2.17+) x86_64, aarch64
macOS x86_64, arm64
Windows x64

Only dependency is numpy. On a platform without a prebuilt wheel, pip falls back to the sdist, which requires a Rust toolchain (1.70+).

CPython only. abi3 is a CPython ABI, so PyPy is not supported.

Quickstart

import ferrumpixel as fp

# Load from a path, or straight from bytes — an upload body, an S3 object, ...
image = fp.load("image.jpg")
image = fp.load(request_body_bytes)

# Chain operations; nothing is computed lazily, each call does its work now
arr = (fp.load("image.jpg")
       .resize(512, 512, fp.Interpolation.BILINEAR)
       .crop(0, 0, 480, 480)
       .rotate(90)
       .brightness(0.1)
       .contrast(1.2)
       .equalize_histogram()
       .sharpen(strength=0.5)
       .normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
       .numpy())

arr.shape, arr.dtype        # ((480, 480, 3), dtype('float32'))

To write an image back out, skip normalize — it converts to float32, and save needs the uint8 path:

fp.load("image.jpg").resize(512, 512).sharpen().save("optimized.jpg")

Batches

load_batch decodes in parallel, every operation runs in parallel, and .numpy() stacks the result into one tensor:

arr = (fp.load_batch(["a.jpg", "b.jpg", "c.jpg"])
       .resize(224, 224)
       .normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
       .numpy())

arr.shape                   # (3, 224, 224, 3)  ->  (N, H, W, C)

Stacking requires uniform dimensions, so resize before .numpy(). A ragged batch raises ValueError rather than padding or guessing.

Core concepts

The chain

Every operation returns something chainable, and every call executes immediately — there is no lazy graph and no .compute(). .numpy() and .save() are the terminal operations that get data back out of Rust.

Input is always converted to 8-bit RGB on load:

Input Result
RGB unchanged
Grayscale expanded to 3 identical channels
RGBA / palette with alpha alpha is dropped, not composited

If you need alpha-aware compositing, do it before handing the image to FerrumPixel.

The aliasing contract

This is the one part of the API that can surprise you, so it is worth reading once.

Operations fall into two groups:

  • Shape- or dtype-changingresize, crop, rotate, sharpen, normalize. These allocate and return a new Image. The receiver is untouched.
  • Neitherbrightness, contrast, equalize_histogram. These mutate the buffer in place and return the same object.
a = fp.load("x.jpg")

b = a.resize(256, 256)      # b is independent; a is unchanged
c = a.brightness(0.1)       # c IS a  ->  True; a was modified too

Chained left to right this never bites you, because you never hold an intermediate:

result = fp.load("x.jpg").resize(256, 256).brightness(0.1).numpy()   # fine

It only matters if you bind an intermediate and expect it to stay put:

base = fp.load("x.jpg")
thumb = base.resize(64, 64)      # safe: new object
bright = base.brightness(0.2)    # base is now brighter too
dark = base.brightness(-0.2)     # applied on top of the previous change

The reason for the split is allocation: forcing brightness to allocate a full copy would measurably hurt long chains for no benefit in the common case. .clone() is specified in design.md §1.3 as the escape hatch but is not yet implemented — until it lands, call fp.load(...) twice if you need two independent handles.

API reference

fp.load(path_or_bytes)

Decode a single image.

Parameter Type Description
path_or_bytes str | bytes A filesystem path, or the encoded bytes of an image

Returns Image, always 8-bit RGB.

Formats: PNG, JPEG, BMP, WebP, TIFF and GIF all decode. PNG and JPEG are the formats under test in CI.

Raises OSError if the path is missing or unreadable, ValueError if the data is not a decodable image, TypeError if the argument is neither str nor bytes.

The GIL is released for the decode.

fp.load_batch(paths_or_bytes)

Decode many images in parallel.

Parameter Type Description
paths_or_bytes Sequence[str | bytes] Paths, encoded bytes, or a mix

Returns BatchImage, in the order given.

Raises the same exceptions as load. A single bad entry fails the whole call rather than yielding a partial batch, so the returned batch always has exactly one image per input.

fp.Interpolation

Resampling filter for resize. Members are upper-case per PEP 8.

Member Notes
NEAREST Fastest, blocky. Use for masks and label maps, where interpolating between class indices would invent values that mean nothing.
BILINEAR Default. Good quality for preprocessing, cheap.
BICUBIC Catmull-Rom. Sharper than bilinear on upscale.
LANCZOS3 Highest quality, most expensive. Best for large downscales.

fp.Image

A single decoded image. Construct with load, never directly.

Columns: New? indicates whether the method returns a new object (per the aliasing contract).

Method Signature New?
resize (w, h, interpolation=BILINEAR) new
crop (x, y, w, h) new
rotate (degrees, expand=False) new
sharpen (strength=1.0) new
normalize (mean, std) new
brightness (delta) in place
contrast (factor) in place
equalize_histogram () in place
numpy ()
save (path)

resize(w, h, interpolation=BILINEAR)

Resize to exactly w × h pixels. Aspect ratio is not preserved — the target size is used as given. Compute the aspect-correct size yourself if you need it.

Raises ValueError if either dimension is 0.

img.resize(224, 224)
img.resize(1024, 768, fp.Interpolation.LANCZOS3)

crop(x, y, w, h)

Crop a w × h region whose top-left corner is at (x, y), measured from the top-left of the image.

Raises ValueError if the region extends past the bounds. It is never silently clamped — a crop that does not fit is a bug in the caller, not something to paper over.

rotate(degrees, expand=False)

Rotate counter-clockwise for positive angles, matching PIL. (OpenCV's rotate constants turn the other way — see Pixel semantics.)

expand Behaviour
False (default) Keep the original dimensions; corners rotate out of frame and are lost
True Grow the canvas so the whole rotated image fits

Newly exposed area is filled with black. Rotation always resamples into a fresh buffer, so it returns a new object regardless of expand.

img.rotate(90)                  # 640x480 -> 640x480, corners clipped
img.rotate(30, expand=True)     # 640x480 -> 795x736

sharpen(strength=1.0)

Sharpen with a 3×3 convolution, blending between the identity kernel and PIL's ImageFilter.SHARPEN.

strength Effect
0.0 No-op
1.0 Matches PIL's SHARPEN (default)
> 1.0 Over-sharpens

Known deviation: the outermost 1-pixel ring is zero-filled (black), because out-of-bounds taps are treated as zero rather than replicated. Crop 1 pixel afterwards if that border matters. See Known deviations.

normalize(mean, std)

Convert to float32 as (pixel / 255 - mean) / std, applied per channel.

Parameter Type Description
mean Sequence[float] Exactly 3 per-channel means, RGB order
std Sequence[float] Exactly 3 per-channel standard deviations

This changes dtype from uint8 to float32, so it is normally the last step before .numpy(). Afterwards save and all the uint8-only operations are unavailable. Raises ValueError unless both sequences have exactly 3 elements.

# ImageNet statistics
img.normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

brightness(delta)

Add delta to every channel, where delta is a fraction of full scale — 0.1 shifts by 25.5 of 255 levels. Negative darkens. Results are clamped to the valid range, not wrapped.

Mutates in place and returns self.

contrast(factor)

Scale contrast about mid-grey: each channel becomes (v - 128) * factor + 128, clamped. 1.0 is a no-op, above 1 increases contrast, below 1 flattens toward grey.

Mutates in place and returns self.

The pivot is 128, not 0. OpenCV's convertScaleAbs scales about 0, so the same factor gives different results in the two libraries.

equalize_histogram()

Equalize the histogram of each channel independently.

Mutates in place and returns self.

Operating per-channel on RGB rather than on a luminance channel can shift colour balance on strongly tinted images. That is intentional and fixed as of 1.0.0.

numpy()

Copy the buffer out as a NumPy array of shape (H, W, C) in RGB order. dtype is uint8, or float32 once normalize has been applied.

This copies rather than sharing memory, so the array stays valid and independent no matter what happens to the Image afterwards.

save(path)

Encode and write to path. Format is chosen from the extension; PNG, JPEG, BMP, WebP, TIFF and GIF all work.

Only valid on the uint8 path. Calling it after normalize raises ValueError, because a normalized float buffer has no meaningful encoding as an 8-bit image. Raises OSError if the path is unwritable.

fp.BatchImage

Images transformed together in parallel. Construct with load_batch, never directly.

Every method mirrors Image with identical parameters and the same aliasing behaviour, applied across the whole batch. The only differences:

  • No save. Write images individually if you need files on disk.
  • numpy() returns (N, H, W, C) and requires uniform shape and dtype across the batch.
batch = fp.load_batch(paths)
arr = (batch
       .resize(224, 224)          # new batch
       .brightness(0.1)           # in place, returns the same batch
       .normalize(mean=..., std=...)
       .numpy())                  # (N, 224, 224, 3) float32

numpy() raises ValueError if the batch is empty, or if its images disagree on shape or dtype. Ragged batches are out of scope; call resize first to make them uniform.

Errors short-circuit: if crop falls outside any one image, the whole call fails rather than returning partial results.

Pixel semantics

These are fixed as of 1.0.0. Changing any of them is a major version bump, however small the diff — see techstack.md §6.

Behaviour FerrumPixel Note
Rotation direction Counter-clockwise for positive angles PIL convention. OpenCV's ROTATE_90_CLOCKWISE turns the other way
Rotation centre Pixel-grid centre, ((w-1)/2, (h-1)/2) Not w/2, which is off by half a pixel
Contrast pivot Mid-grey: (v - 128) * factor + 128 OpenCV's convertScaleAbs scales about 0
Brightness v + delta * 255, clamped delta is a 0–1 fraction, not a raw level
Normalize (pixel / 255 - mean) / std, float32 Per channel, RGB order
Rounding f32 → u8 rounds Does not truncate
Channel order RGB throughout Not BGR
Histogram equalization Per channel on RGB Not on a luminance channel

Every operation is parity-tested against a PIL → OpenCV → NumPy reference within documented per-op tolerances, and the benchmark suite refuses to report timings if that gate fails. Methodology in benchmark.md §7.

Concurrency and threading

GIL release

Every operation wraps its compute in py.allow_threads, so the GIL is free for the whole call. Other Python threads make progress while FerrumPixel works. This does not parallelise a single image internally — it prevents one slow transform from stalling the interpreter.

Batch parallelism

BatchImage dispatches across Rayon's global thread pool. The pool defaults to one worker per logical CPU.

export RAYON_NUM_THREADS=4

Containers: set this explicitly. Rayon sizes its pool from the logical CPU count of the host and does not read cgroup CPU limits. In a container with a 2-CPU quota on a 64-core host, it will spawn 64 workers and thrash. Pin RAYON_NUM_THREADS to your quota.

Thread count affects scheduling only, never output — this is covered by a test.

Web frameworks

In FastAPI or any ASGI framework, declare preprocessing handlers as plain def, not async def:

@app.post("/preprocess")
def preprocess(file: UploadFile = File(...)):     # def, not async def
    arr = fp.load(file.file.read()).resize(512, 512).numpy()
    return {"shape": arr.shape}

An async def handler runs directly on the event loop, so synchronous CPU-bound work inside it blocks every other in-flight request — the service then handles exactly one request at a time no matter how many clients connect. A plain def handler is dispatched to the threadpool, which is what lets the GIL release turn into actual parallelism. In our own load test this was the difference between throughput flat at ~34 req/s and throughput scaling from 23 to 140 req/s across a 1→100 concurrency ramp.

A complete worked example is in examples/fastapi_service/.

Error handling

Everything maps onto Python builtins — there is no custom exception hierarchy to learn or catch.

Condition Raises
Missing or unreadable path OSError
Unwritable destination in save OSError
Undecodable image data ValueError
Crop outside image bounds ValueError
mean/std not exactly 3 elements ValueError
save after normalize ValueError
Empty or ragged batch in numpy() ValueError
Argument neither str nor bytes TypeError

Type checking

The package ships inline type stubs and a PEP 561 py.typed marker, so mypy and pyright pick up the API with no extra stub package:

img: fp.Image = fp.load("a.jpg")
arr = img.resize(512, 512).numpy()

fp.load(123)                     # error: no overload matches argument type "int"
img.resize("512", 512)           # error: incompatible type "str"; expected "int"

Performance

Measured on an 8-core Zen 3 machine. Full methodology and numbers in benchmark.md and the Phase 4 report.

Workload vs. PIL/OpenCV/NumPy baseline
Batch full chain, N=128 1.88x faster
Batch full chain, N=8 1.42x faster
resize alone 1.90x faster
Peak RSS during batch 0.88x (uses less memory)
Single-image full chain 0.54x — slower

Two honest observations.

Batch is where FerrumPixel wins, and the advantage grows with batch size — 1.42x at N=8 to 1.88x at N=128 — because Rayon's dispatch overhead amortises over more work while the baseline's per-image throughput stays flat.

Single-image full chains are currently slower than OpenCV. The cause is sharpen: it runs a scalar 3×3 convolution against OpenCV's hand-tuned SIMD filter2D, and at ~51 ms on a 1024² image it dominates the chain. If your workload is single-image and sharpen-heavy, OpenCV is still faster today. Vectorising it is the top item on the post-1.0 list. Chains without sharpen fare considerably better.

Under concurrent load the picture is different again: FerrumPixel holds a lower p99 than the baseline at high concurrency (1200 ms vs 1900 ms at 100 concurrent clients) because the GIL release keeps the tail from blowing out, even though median throughput still favours the baseline.

Known deviations and limitations

These are documented behaviour as of 1.0.0, not pending fixes. Correcting the first two would change output pixels and therefore requires a major version bump.

  • sharpen leaves a 1-pixel zero-filled border. OpenCV reflects and PIL replicates at the edge; FerrumPixel treats out-of-bounds taps as zero. Interior pixels match OpenCV to within 1 level.
  • equalize_histogram normalises its CDF by total, where OpenCV uses total - cdf_min. Outputs differ by up to 10 levels on the reference image. This is an accepted algorithmic divergence, not a bug.
  • .clone() is not implemented. It is specified in design.md §1.3 as the escape hatch from in-place aliasing. Additive, so it can land in a 1.x release.
  • Ragged batches are unsupported. BatchImage.numpy() raises rather than padding or returning a ragged structure.
  • Alpha is dropped on load, not composited.
  • No GPU support, and no zero-copy NumPy interop — .numpy() always copies.
  • PyPy is unsupported, since abi3 is a CPython ABI.

Versioning and stability

Semantic versioning, with one project-specific rule: any change to pixel semantics is a major bump, regardless of how small the code change is. Interpolation defaults, rounding, channel order, rotation direction and contrast pivot are all covered.

  • Major — pixel semantics change, or an API removal
  • Minor — new operations, new optional keyword arguments
  • Patch — bug fixes that do not change documented behaviour

Full policy in techstack.md §6. Release history in CHANGELOG.md.

Project documentation

Doc Purpose
design.md API design — signatures, aliasing semantics, pixel-semantics contract
benchmark.md Benchmark methodology, target numbers, correctness gate
architecture.md Component boundaries, data flow, concurrency model
prd.md Goals, non-goals, user stories, success metrics
techstack.md Every dependency, why it was chosen, versioning policy
libraryScope.md In-scope/out-of-scope boundaries per dependency, rejected alternatives
phases.md Implementation roadmap and per-phase exit criteria
agents.md Conventions and guardrails for AI coding agents working in this repo
CHANGELOG.md Version history

Contributors: start with prd.md → architecture.md → design.md → techstack.md. AI agents: read agents.md first, always.

Building from source

git clone https://github.com/ShivamMalge/FerrumPixel.git
cd FerrumPixel
python -m venv .venv && source .venv/bin/activate
pip install maturin pytest numpy pillow opencv-python-headless pytest-benchmark
maturin develop --release -m crates/ferrumpixel-py/Cargo.toml

pytest tests/python -v                              # test suite
pytest benches/test_bench.py --benchmark-disable    # correctness gate
cargo test --workspace                              # Rust tests
cargo clippy --workspace -- -D warnings

The correctness gate must pass before any benchmark will report a number — a fast wrong answer is not a result.

License

Dual licensed under MIT or Apache-2.0, at your option.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ferrumpixel-1.0.0.tar.gz (51.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

ferrumpixel-1.0.0-cp39-abi3-win_amd64.whl (2.9 MB view details)

Uploaded CPython 3.9+Windows x86-64

ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.3 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (3.1 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

ferrumpixel-1.0.0-cp39-abi3-macosx_11_0_arm64.whl (2.9 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

ferrumpixel-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl (3.1 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file ferrumpixel-1.0.0.tar.gz.

File metadata

  • Download URL: ferrumpixel-1.0.0.tar.gz
  • Upload date:
  • Size: 51.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ferrumpixel-1.0.0.tar.gz
Algorithm Hash digest
SHA256 870ba0f241f2a57e475092c917f97297a560e638706ddb675c55d857a8de7f8a
MD5 fb7c43d670fcb57ccfe9ccea396b0cdd
BLAKE2b-256 ca6e9672bc5c1d7f153bb065ffd6e8838de32ca9a211e9774493bb5e017b1d22

See more details on using hashes here.

Provenance

The following attestation bundles were made for ferrumpixel-1.0.0.tar.gz:

Publisher: release.yml on ShivamMalge/FerrumPixel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ferrumpixel-1.0.0-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: ferrumpixel-1.0.0-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 2.9 MB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ferrumpixel-1.0.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 35b944a832d606ddd9229a94c7a17176ffaf788a8acf6abc324d88723494c5f1
MD5 1531915c9ea4085bc2885fec0cfc6b20
BLAKE2b-256 02ab2e61311717fbc02ddab2ce18a1b009d38551df320d72f1653219e05ef3de

See more details on using hashes here.

Provenance

The following attestation bundles were made for ferrumpixel-1.0.0-cp39-abi3-win_amd64.whl:

Publisher: release.yml on ShivamMalge/FerrumPixel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a2093f9a4fa253b1b95bea3629d106db13ae492093423a544b8bf76b3321c8cd
MD5 73bf8f86bcd517168fa8a73f17b0b7dd
BLAKE2b-256 05e0e383f16f36cdd4ecd4a3733b7ebe1e03938e6227d76a91ae5b01cdf6db1d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on ShivamMalge/FerrumPixel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 56c1d604194620d950bb396ee46af22a6d6ba31ef85e6a3f62fd0f9447c9267b
MD5 4600f1d5796103fb06ece6764f4ee1cc
BLAKE2b-256 1f752d0351e8a3b0bbb23231ef98db3bf2cc97a459d2089dfe8890bd1346cd4b

See more details on using hashes here.

Provenance

The following attestation bundles were made for ferrumpixel-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on ShivamMalge/FerrumPixel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ferrumpixel-1.0.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for ferrumpixel-1.0.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0dba376a8dc868d44b4320cd53fa23152bfe60086d2b9607bf60f1e46c54bc8b
MD5 2e6550c1242273030ee9805f1cfd1822
BLAKE2b-256 c2039ab335fe2b72c6a92ec9b778f822ae906bb6a8b62d573c5546c936838c91

See more details on using hashes here.

Provenance

The following attestation bundles were made for ferrumpixel-1.0.0-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on ShivamMalge/FerrumPixel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ferrumpixel-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for ferrumpixel-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 8b88cc6fa6a3694b5373253327df0efcaedf2f1dd2cff8b72e0e702bb44f1727
MD5 3a401654ab7179bb87087ad9c84095da
BLAKE2b-256 e1a7c1faea66eb30676b3f70dc47f216df0aa0ce4aef0e18b55ab2ba9d1c7b32

See more details on using hashes here.

Provenance

The following attestation bundles were made for ferrumpixel-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on ShivamMalge/FerrumPixel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0 This release

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page