revela
An image signal processing pipeline written in NumPy, from which synthesisable Verilog is generated.
An ISP turns what a camera sensor actually produces — a single-colour-per-pixel mosaic sitting on a pedestal, with the sensor's noise, its dead pixels and its lens's shading baked in — into an image. revela is that pipeline, written as NumPy, at the arithmetic the hardware will really use, and compiled to RTL.
def kernel(pixel, offset, bit_depth):
"""One pixel path: add the signed offset, saturate to the datapath range."""
return (pixel.astype(np.int32) + offset).clip(0, (1 << bit_depth) - 1)
That is not a model of the hardware. It is the hardware: it is traced into Verilog, and it is the reference the Verilog is checked against, bit for bit, under Verilator. There is exactly one of it.
What makes this different
Most ISP projects have two of everything: a floating-point reference model that describes the intent, and an RTL implementation that approximates it. Then they spend their lives arguing about the difference between the two and calling it "quantisation error". It isn't. The fixed-point algorithm is a different algorithm — different rounding, different saturation, different LUT breakpoints — and measuring its distance from a float algorithm measures nothing useful.
revela has one model per block, written directly at the hardware's arithmetic:
integer dtypes with explicit widths, explicit rounding, explicit saturation, LUTs
where the hardware will have LUTs, shifts where the hardware will shift. No
np.float64 appears anywhere in revela/blocks/. That model is the
specification, the golden reference, and the thing the RTL is generated from.
Image quality is measured where it means something — at pipeline level, against reference images. Never per block.
Relationship to np2hw
np2hw is the generator: a compiler that traces NumPy into a streaming line-based IR and emits synthesisable Verilog, with line buffers, shift registers, edge handling and config registers built for you.
revela is the ISP, and np2hw's flagship proof case. The two are separate projects and revela depends on np2hw — it does not vendor or fork it. That separation is load-bearing in both directions: np2hw stays a general NumPy→RTL compiler rather than growing ISP-shaped special cases, and revela gets to be a real ISP rather than a compiler test suite. When revela needs something np2hw cannot express, the fix goes into np2hw as a generic capability at the NumPy level — never as a revela-side workaround.
revela emits no Verilog. Block datapaths are traced from the NumPy models by
np2hw; the top level is built by np2hw.compose(), which instantiates the
generated cores and wires the netlist; the AXI4-Lite register file in front of it
is emitted by np2hw.control_wrap(). revela supplies the models, the register
declarations and their descriptions, the design JSON, and the software — the
register map with per-instance address allocation, sensor descriptions, the 3A
control loops and the host API.
The split at the control plane is the same one: revela decides where every register lives, np2hw decides what an AXI4-Lite slave looks like. Neither holds a copy of the other's half, which is why the emitted JSON register map and the emitted decode cannot drift apart — a test reads the addresses back out of the generated Verilog and compares them with the map.
Architecture in one diagram
sensor ─▶ [ blacklevel ] ─▶ [ lsc ] ─▶ [ demosaic ] ─▶ [ ccm ] ─▶ [ gamma ] ─▶ out
│ │ │
╰────────────────┴──── stats ───▶ AE / AWB (host, frame rate)
pipe @ 0x0000 ── width, height, window, bayer_phase, bit_depth ──▶ (wires to all)
Blocks are chained by a direct valid/ready stream — no bus between stages, no
DRAM round-trip. One pixel per clock, and backpressure composes all the way back
to the sensor interface. AXI4-Stream Video appears only as an adapter at the
pipeline boundary, where you are talking to somebody else's IP.
Quick start
git clone https://github.com/lanserge/revela
cd revela
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]" # np2hw arrives from PyPI
pytest # includes the bit-exact model-vs-RTL tests
Verilator must be on the path — apt install verilator or brew install verilator. The bit-exact tests skip without it; everything else still runs.
A pipeline is described in JSON and nowhere else — a netlist of named block instances and the connections between their ports:
{
"name": "revela_mono_imx219_basic",
"sensor": { "name": "imx219", "mode": "binned_2x2" },
"inputs": [{ "name": "sensor" }],
"outputs": [{ "name": "video" }],
"nodes": [
{ "instance": "blacklevel", "block": "blacklevel" },
{ "instance": "whitebalance", "block": "whitebalance" },
{ "instance": "gamma", "block": "gamma" },
{ "instance": "stats", "block": "stats" }
],
"connections": [
{ "from": "sensor", "to": "blacklevel.in" },
{ "from": "blacklevel.out", "to": "whitebalance.in" },
{ "from": "whitebalance.out", "to": "gamma.in" },
{ "from": "gamma.out", "to": "video" },
{ "from": "blacklevel.out", "to": "stats.in" }
]
}
A netlist rather than a list, because a real ISP is not a chain. Note the last
connection: stats taps the datapath — deliberately PRE-whitebalance,
because AWB must meter the illuminant's cast, and a loop that meters its own
correction chases its tail. The tap consumes the stream and produces no
pixels, so the image path runs blacklevel → whitebalance → gamma to the
output. A list of blocks cannot say either of those things — which is the
error the format is shaped to prevent.
A node may also override a declared register's build-time attributes —
"registers": { "gain": { "frac": 12 } } — values only, never addresses; the
model, the RTL, the register map and the host all follow the one overridden
declaration.
python examples/build_pipeline.py pipelines/mono/imx219/basic/pipeline.json
Designs live in pipelines/<topology>/<sensor>/<variant>/, holding the netlist,
every tuning for it, and a git-ignored build/:
pipelines/mono/imx219/basic/
pipeline.json structure — nodes and connections
profiles/indoor.json values — registers + 3A tuning
profiles/outdoor.json values — same structure, different numbers
build/ generated — .v, register map, register docs
A profile carries values and names the sensor it was tuned for, so one pipeline serves several sensors and scenes. It cannot add a block or rewire the graph. Values layer: block reset → sensor-derived → profile, and the resolved settings record which layer each value came from.
Inputs carry no addresses. The netlist and the profile are inputs, and both
schemas reject an addresses key outright. The register map is the output.
That is what stops a builder GUI from emitting a map that disagrees with the
hardware it generated — and a test proves a described pipeline rebuilds to
byte-identical Verilog and addresses, so allocation is a pure function of
structure.
The register map JSON is the authoritative address map. The host API and the register-map documentation are both generated from it; nothing anywhere hardcodes an address.
The rules this project is built on
Four, and they are not style preferences — they are why the thing works. They are stated in full in docs/design-rules.md.
- One model per block, at the hardware's arithmetic. No float reference, no float-versus-fixed comparison.
- Parameters are declared once, per block, and allocated globally. Blocks declare local offsets; the pipeline assigns each block instance a base address. A stereo pipeline instantiates every block twice, and every block is independently testable — both fall out of that, and neither works without it.
- Verification is bit-exact agreement between the NumPy model and the generated Verilog under Verilator via cocotb. Identical, not "close enough".
- Generated Verilog must be readable. Meaningful signal names, module names matching the block, and each parameter's description carried through from its declaration. Somebody licensing this will read the output in review and hand it to their own verification team. That is a product requirement.
Status
blacklevel, whitebalance, gamma, rgb_gamma, ccm, logo, bilinear,
bicubic and the two-stage adaptive hamilton_adams (ha_green, ha_rb) are
complete end to end — model, generated Verilog, bit-exact cocotb tests,
fixed-point variants through build-time overrides — and contain no
hand-written Verilog: each block, phase mux, window and LUT included, is
one np2hw trace of its NumPy model. bilinear is the first block whose tap
combination is selected by CFA position over a shared line-buffered window,
and the first to emit a three-channel stream — its model ends in
np.stack([r, g, b], axis=-1), and the wire packing belongs to the stream
layer alone.
pipe, stats, the register map with its AXI4-Lite control plane, address
allocation, build-time register overrides, the sensor schema and the
composition layer are implemented. The remaining blocks (lsc, defect,
the corrected demosaics (malvar — patent-gated until 2027-01-25 — and
menon), rgb2yuv, sharpen) are declared stubs, each with its intent
documented in its own file. Every declared stub and
documented gap is a sponsorable goal — the
sponsorable label
carries the full slate, scoped and ready to fund.
stats has a model and a register map but no generated RTL: accumulation over a
region is a reduction, which np2hw does not trace yet. It is the one block that
does not currently satisfy rule 3, and that is stated rather than papered over
with a test that only exercises the NumPy side.
Licence
Solderpad Hardware License v2.1 — see LICENSE. SHL-2.1 is a wraparound over Apache 2.0, so the Apache text is included as LICENSE-APACHE, with a NOTICE file. Every source file carries:
# SPDX-License-Identifier: Apache-2.0 WITH SHL-2.1
Solderpad is the right licence here because Apache 2.0 was written for software and its terms do not cleanly describe what happens when a design is fabricated. SHL-2.1 extends the definitions to cover hardware: "Object form" includes FPGA bitstreams, mask works and physical instantiation, and "Rights" covers design right and semiconductor topography rights, not just copyright. You may, at your option, treat any work released under it as released under Apache 2.0.
Verilog generated by revela carries the same licence, because its datapath is derived from the models; generated files say so in their header. The structure np2hw emits around it comes with no obligations at all — see docs/LICENSING.md.
A commercial licence, with support and indemnity, is available on request.
What licence applies to the Verilog revela generates, what you owe by shipping it, and what the commercial licence adds: docs/LICENSING.md. Every dependency and its licence: docs/THIRD-PARTY.md.
Contributing
Contributions require a signed CLA before a pull request is merged — see CONTRIBUTING.md. It is a licence grant, not an assignment: you keep your copyright. Two things that will get a sensor contribution rejected on sight: register tables transcribed out of the Linux kernel media drivers (they are GPL-2.0 and incompatible with this licence), and per-unit calibration data committed as if it described the sensor model.
Funding
Developed independently. Detail in FUNDING.md; enquiries to s.rabykin@gmail.com.
- Commercial licence — patent grant, indemnity, support SLA, and access to advanced blocks and verification collateral.
- Sponsor a block — fund a declared stub (
lsc,defect,demosaic,rgb2yuv,sharpen) or a documented gap; thesponsorablelabel is the slate. Done means bit-exact under the four rules, and the work lands here openly, immediately — sponsorship buys ordering and named credit, not exclusivity. - Sponsor a sensor — fund characterisation of a sensor you need; you pick it and the conditions, get the data first, and it is then published here for all.
- Consulting — ISP bring-up, tuning, custom blocks.
- GitHub Sponsors — recurring support.
revela™ is a trademark of Serge Rabyking. The licence does not grant rights to the name — see TRADEMARK.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file revela-0.1.0.tar.gz.
File metadata
- Download URL: revela-0.1.0.tar.gz
- Upload date:
- Size: 267.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00acdaeb3bf2d097fffda1e35d4d3881de98974aa51dfafbc7c9b5147f11a64a
|
|
| MD5 |
44655cb1576bea6a17ffa3a0bd7f2835
|
|
| BLAKE2b-256 |
a9dc49cd0757ec9f35fb409d23562b24c24cabf02c84708b66aeff2794b32548
|
Provenance
The following attestation bundles were made for revela-0.1.0.tar.gz:
Publisher:
publish.yml on lanserge/revela
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
revela-0.1.0.tar.gz -
Subject digest:
00acdaeb3bf2d097fffda1e35d4d3881de98974aa51dfafbc7c9b5147f11a64a - Sigstore transparency entry: 2480023259
- Sigstore integration time:
-
Permalink:
lanserge/revela@11e129438c424c1d878162776e90f2bac67004ff -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/lanserge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@11e129438c424c1d878162776e90f2bac67004ff -
Trigger Event:
release
-
Statement type:
File details
Details for the file revela-0.1.0-py3-none-any.whl.
File metadata
- Download URL: revela-0.1.0-py3-none-any.whl
- Upload date:
- Size: 154.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46dd65eda6d7c8e3a2f8cb759abcc16798471c6743edbe603f0264e24d931867
|
|
| MD5 |
865470d9806d65ea65a9e0b618ed6d07
|
|
| BLAKE2b-256 |
a25558df7a765364c8314893c501021369cdbf52ef47a708200507616641fd49
|
Provenance
The following attestation bundles were made for revela-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on lanserge/revela
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
revela-0.1.0-py3-none-any.whl -
Subject digest:
46dd65eda6d7c8e3a2f8cb759abcc16798471c6743edbe603f0264e24d931867 - Sigstore transparency entry: 2480023495
- Sigstore integration time:
-
Permalink:
lanserge/revela@11e129438c424c1d878162776e90f2bac67004ff -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/lanserge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@11e129438c424c1d878162776e90f2bac67004ff -
Trigger Event:
release
-
Statement type: