fovea
fovea is image processing for Rust where the compiler catches colorspace mistakes, channel-order bugs, and lossy conversions before your code runs.
If you have ever shipped a bug because someone passed BGR where RGB was expected, resized gamma-encoded pixels with bilinear interpolation, or treated a camera SDK byte buffer as "probably u16 grayscale", fovea was built for you.
In fovea, BGR and RGB are different types. Blending gamma-encoded pixels is a compile error. Byte layout is a contract, not a comment.
What fovea prevents
- BGR/RGB confusion —
Bgr8andRgb8are distinct types. Passing one where the other is expected is a compile error. - Gamma-incorrect interpolation —
Bilinearresize requires pixels inLinearSpace. It will not compile forSrgb8; linearize first, explicitly. - Silent data loss — there is no implicit "just convert this image" path. Lossy conversions are named strategies:
Luminance,Narrow,FullRange,SrgbGamma,Clamp. - Raw byte misuse —
PlainPixelis an unsafe layout contract. Once a pixel type implements it, byte-level access is guaranteed by the type system. - Over-promising APIs — traits describe exactly what an operation needs: random access, row access, contiguous storage, byte layout, or linear arithmetic.
The compiler catches the image bug
Nearest-neighbor resize copies pixels, so it works for gamma-encoded sRGB. Bilinear resize blends neighboring samples, so fovea requires linear-light pixels first.
use Size;
use ;
use ;
use ;
let srgb = generate;
// ✓ This compiles: nearest-neighbor copies samples without blending them.
let preview: = resize;
assert_eq!;
// ✓ This compiles: explicit sRGB decode before interpolation.
let linear: = convert_image;
let resized: = resize;
assert_eq!;
The version below does not compile, and that is the point:
use fovea::Size;
use fovea::image::Image;
use fovea::pixel::Srgb8;
use fovea::transform::{Bilinear, resize};
let srgb = Image::fill(4, 3, Srgb8::new(128, 64, 32));
// ✗ Bilinear interpolation blends samples. Srgb8 is gamma-encoded.
let _: Image<Srgb8> = resize(&srgb, Size::new(8, 6), Bilinear);
Linearize first with SrgbGamma, resize in RgbF32 or MonoF32, then encode back if you need an sRGB output image.
When NOT to use fovea
- You need real-time video decode: use FFmpeg bindings.
- You need deep-learning tensor pipelines: use
candle,ort, or your tensor runtime of choice. - You want the broadest possible codec support with the simplest API: use the
imagecrate. - You only need to resize a JPEG in a web app and do not care about pixel semantics: use the
imagecrate.
When fovea is the right tool
- You read pixel data directly from industrial, scientific, or machine-vision cameras.
- Correctness matters more than convenience: inspection, metrology, robotics, medical, lab automation.
- You want the compiler to enforce colorspace and pixel-format discipline across a team.
- You need guaranteed byte layout for camera SDK buffers, memory-mapped images, GPU upload, or FFI.
- You want algorithms to state their real requirements in trait bounds instead of runtime checks.
Install
For PNG, JPEG, or BMP I/O, add fovea-io with the codec features you need:
Getting started
Create an image, decode sRGB samples into linear light, modify pixels through the contiguous slice, and encode back to sRGB:
use ;
use ;
use ;
let srgb = generate;
let mut linear: = convert_image;
for px in linear.as_mut_slice
let display: = convert_image;
assert_eq!;
For a longer first pass, start with the docs.rs guide: fovea::guide.
Core types
| Type or trait | Use it when |
|---|---|
Image<P> |
You own a heap-allocated image with runtime dimensions. |
ImageArray<P, W, H> |
Width and height are compile-time constants. |
ImageRef<'a, P> / ImageRefMut<'a, P> |
You want a borrowed view over existing storage, including strided ROIs. |
ImageView / ImageViewMut |
An algorithm only needs random pixel access. |
RasterImage / RasterImageMut |
An algorithm should process row slices efficiently. |
ContiguousImage / ContiguousImageMut |
The whole image is one dense pixel slice. |
PlainImage / PlainImageMut |
You need byte access to contiguous PlainPixel storage. |
SubView / SubViewMut |
You need zero-copy regions of interest, tiles, or sliding windows. |
The image traits intentionally mirror Rust's slice model: borrow views when you can, allocate only when you mean to.
Pixel types
| Family | Examples | Meaning |
|---|---|---|
| Mono | Mono8, Mono16, MonoF32, Mono<12> |
One-channel intensity pixels. |
| RGB/BGR | Rgb8, Bgr8, RgbF32 |
Linear color pixels with explicit channel order. |
| sRGB | Srgb8, Srgba8, SrgbMono8 |
Gamma-encoded display/file pixels. Not linear-light. |
| Alpha | Rgba8, Bgra8, MonoA16 |
Pixels with explicit alpha channels. |
| Indexed/labels | Indexed8, Label32, bool |
Palette indices, connected-component labels, binary masks. |
Important distinction: Rgb8 and Srgb8 may both store three u8 channels, but they do not mean the same thing. Rgb8 is linear-light RGB. Srgb8 is gamma-encoded sRGB. Algorithms that blend pixels can require the former and reject the latter.
Modules
| Module | Start here | One job |
|---|---|---|
image |
Image, ImageView, SubView |
Storage, views, rows, ROIs, tiles, and neighborhoods. |
pixel |
Srgb8, RgbF32, PlainPixel, LinearSpace |
Pixel vocabulary and the traits that make illegal operations unrepresentable. |
transform |
convert_image, resize, combine_images |
Image-producing operations: conversion, resize, geometry, convolution, morphology. |
analyze |
histogram, integral_image, connected_components |
Image analysis that produces data about an image. |
border |
Clamp, Mirror, Skip |
Boundary behavior for neighborhood operations. |
guide |
guide::faq, guide::pixel_types |
Task-oriented docs.rs pages for common questions. |
FAQ
Where do I start if I just want to load a PNG and resize it?
Use fovea-io to decode, match the returned pixel enum once, convert sRGB images to linear pixels with SrgbGamma, call resize(..., Bilinear), then encode.
Why does Bilinear fail for Srgb8?
Because bilinear interpolation blends samples, and blending gamma-encoded samples is physically wrong. Use NearestNeighbor if you are only copying samples; otherwise linearize first.
What type should I use for a 12-bit monochrome camera?
Use Mono<12> when you want the bit depth represented in the pixel type. Use Mono16 when the camera SDK already expands samples to full 16-bit storage and you want simpler integration.
How do I process a large image in parallel?
For contiguous per-pixel work, use as_slice() / as_mut_slice() and choose your own parallel runtime. For region-local work, split into tiles. For in-place mutation, into_tiles_mut() yields disjoint mutable tiles.
More answers are in fovea::guide::faq.
Crate ecosystem
| Crate | Published? | Purpose |
|---|---|---|
fovea |
crates.io + docs.rs | Core image types, pixels, analysis, and transforms. |
fovea-io |
crates.io + docs.rs | Feature-gated PNG, JPEG, and BMP codecs. |
fovea-display |
crates.io + docs.rs | Display conversion strategies, texture metadata, and debug windows. |
fovea-derive |
crates.io + docs.rs | Derive macros re-exported by fovea. |
fovea-examples |
repo only | End-to-end programs that combine the crates. |
Design principles
fovea is designed around a small set of explicit principles: types are the spec, concerns are orthogonal, traits layer progressively, conversions are named, and layout is a contract. The short version is this:
The compiler is the first reviewer.
License
Licensed under the MIT License.