Skip to main content

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)

The pipeline context has one owner and reaches every block as wires, never as a copy per block: a resolution change is one write, not N that must all land in the same frame. Where each fact comes FROM is the design's to declare — anything named in stream.context arrives as an input port carried by the stream itself and gets no register, because a register holding a second copy of a header fact is a second answer to one question. What stays a register is what is genuinely a choice: a crop window is software's to pick, a line length is not.

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.

Where the pixels come from

revela generates an ISP, not a receiver: something upstream has to put raw Bayer on that valid/ready stream. Nothing here requires a particular source — the pipeline takes samples and its context, whatever produced them — but there is a complete path that works today, and it is worth naming because the pieces are designed to fit:

piece what it does
bayerlink the wire format: raw Bayer over an ordinary video link, self-described by a header line — geometry, CFA phase and depth travel with the frame
np2hw.video_in the receiver: decodes that link and aligns sample depth on the way in, so downstream the depth is a build fact
revela the ISP: this project
bayerlink-fpga all three on a PYNQ-Z2, with the board glue — the worked example
picam2hdmi a source at the other end: a Raspberry Pi, genlocked, any libcamera sensor

Two things make the seam work, and both are worth knowing before wiring anything to this ISP:

The boundary is traced, not declared. revela generate writes <name>_build.json beside the Verilog: each output's data_bits, channels and channel_bits, the geometry the cores were built around, and the latency. Read it — do not restate it. A three-channel word is three fields wide because the models packed it that way, not because anyone chose the number, and an integrator that writes the width down is correct until the day the pipeline changes and then produces a picture rather than an error.

The geometry in that file is a maximum. The cores get neither the stream's end-of-line nor the header's width, so a frame wider than they were built for is not something they can notice — it shears, with the CFA phase inverted on alternate rows and no status bit anywhere. Whoever hands them a wider one must crop first; bayerlink-fpga's shim shows one way, cropping to the built width and saying so in a sticky truncated.

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.

revela generate pipelines/mono/imx219/basic/pipeline.json --out build --clock-mhz 148.5

That is the shipping path: it builds the design, proves the generated Verilog bit-exact against the same NumPy models under Verilator, and only then writes the pack — Verilog, register map, SystemRDL, and a FuseSoC core manifest. It refuses to emit anything it has not verified, so a failure there is a verification failure rather than a tool problem. The register documentation is part of that pack, not a later step. To read a design rather than ship it, examples/build_pipeline.py writes the same pack -- through the same emitter -- and prints the address map, the stream topology and a profile's resolved values.

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, SystemRDL, 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.

  1. One model per block, at the hardware's arithmetic. No float reference, no float-versus-fixed comparison.
  2. 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.
  3. Verification is bit-exact agreement between the NumPy model and the generated Verilog under Verilator via cocotb. Identical, not "close enough".
  4. 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; the sponsorable label 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.

Metadata

Release files for revela 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for revela 0.2.0
File Size Uploaded
revela-0.2.0.tar.gz 285.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for revela 0.2.0
File Interpreter ABI Platform
revela-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 454.5 kB

Release files / revela-0.2.0.tar.gz

Download URL revela-0.2.0.tar.gz
Size 285.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4eeb2d46cac6a46fcaff88f4ff9cdd9896e41f9e54b2d8d92091dcde0c0ef00f
BLAKE2b-256 checksum
How to use checksums
14bcf647b943f0fbff2f3ad32fbbd46fbf6e4fb7bf25ac581e799fdaa322fbd8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.

Transparency log

Release files / revela-0.2.0-py3-none-any.whl

Download URL revela-0.2.0-py3-none-any.whl
Size 169.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aa69bdc858d87f2198c8b9ebc13ac0ca052b04bc16380aabe17fc3afded32277
BLAKE2b-256 checksum
How to use checksums
2b5fae71df905c3c41ee49d51b4b895cae4ab7d5eee0806f29aee12b0b028cc6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.1

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release 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