A professional-grade film negative to positive conversion tool written in Rust. Invers processes scanned film negatives (color and black & white) and converts them to high-quality positive images with film-specific corrections, tone curves, and color matrices.
- Orange Mask-Aware Inversion: Intelligent algorithm that properly accounts for the orange mask in color negative film, eliminating the common blue cast problem
- Intelligent Base Estimation: Automatically detects film base color from image borders with validation for orange mask characteristics
- Multiple Inversion Modes: Mask-aware (default), linear, logarithmic, and divide-blend algorithms
- Advanced Tone Curves: Asymmetric curves with independent toe/shoulder controls, plus traditional S-curves
- Auto-Adjustments: Auto-levels, auto-color, auto-exposure, and auto-white-balance with configurable parameters
- Shadow Recovery: Adaptive shadow lift based on image analysis
- High-Quality Output: 16-bit TIFF export in linear Rec. 2020 colorspace
- Parallel Processing: Multi-threaded pipeline using Rayon for fast batch processing
- Film Presets: YAML-based preset system for film-specific settings
- Modular Architecture: Clean separation of concerns with dedicated modules for pipeline stages, auto-adjustments, and configuration
Invers follows a modular Rust workspace architecture designed for maintainability and extensibility:
-
invers-core: Core library with modular subsystems
pipeline/- Processing stages (base estimation, inversion, tone mapping)auto_adjust/- Auto-correction algorithms (levels, color, exposure, white balance)cb_pipeline/- Curves-based processing pipeline inspired by Negative Lab Promodels/- Data structures for presets, profiles, and conversion optionsconfig/- Configuration management with defaults and testing utilitiesgpu/- Optional GPU acceleration via WGPU (feature-gated)
-
invers-cli: Command-line interface with modular command structure
- Each command (
convert,batch,analyze,preset,init) in its own module - Debug commands available in debug builds
- Each command (
-
invers-gui: Qt-based GUI (planned)
The easiest way to install invers is via Homebrew:
brew tap narrowstacks/invers
brew install inversThis installs pre-built binaries for:
- macOS (Intel and Apple Silicon)
- Linux (x86_64)
GUI is not yet packaged, as it's not finished!
If you prefer to build from source or need a platform not covered by Homebrew:
- Rust 1.70 or later
- Cargo (included with Rust)
# Clone the repository
git clone https://github.com/narrowstacks/invers.git
cd invers
# Build release version (recommended)
cargo build --release --features gpu
# The binary will be at ./target/release/invers# Run tests
cargo test
# Check code without building
cargo check
# Format code
cargo fmt
# Lint code
cargo clippy# Convert a single negative with automatic settings
invers convert negative.tif
# Convert with warm white balance
invers convert negative.tif --white-balance warm
# Specify output location and format
invers convert negative.tif --out ./converted/ --export tiff16# Auto-detect film base color
invers analyze negative.tif
# Analyze specific region (x,y,width,height)
invers analyze negative.tif --roi 100,100,500,500
# Save base estimation for reuse across a roll
invers analyze negative.tif --save base.json# Process multiple files (shares base estimation from first image)
invers batch *.tif --out ./converted/
# Batch with parallel threads and shared base
invers batch *.tif \
--threads 8 \
--out ./converted/
# Use pre-analyzed base for consistent results across a roll
invers batch *.tif --base-from base.json --out ./converted/
# Process each image independently (different rolls mixed together)
invers batch *.tif --per-image --out ./converted/# Set up user config directory with default presets
invers init
# Generate shell completions
invers completions zsh > ~/.zfunc/_inversinvers convert [OPTIONS] <INPUT>
Arguments:
<INPUT> Input file or directory
Output Options:
-o, --out <PATH> Output directory or file path
--export <FORMAT> Export format: tiff16 (default) or dng
Processing Options:
-w, --white-balance <PRESET> White balance preset [default: auto]
Values: auto, none, neutral, warm, cool
--exposure <FLOAT> Exposure compensation (1.0 = no change, >1.0 = brighter)
--base <R,G,B> Manual base RGB values (use 'invers analyze' to find these)
--bw Force black and white conversion mode
General Options:
--silent Suppress non-essential output (timing, progress)
--cpu Force CPU-only processing (GPU used by default)
-v, --verbose Enable verbose output (config loading, processing details)
-h, --help Print help
Analyze an image to estimate film base color. Use this to find base RGB values that can be reused across multiple frames from the same roll.
invers analyze [OPTIONS] <INPUT>
Arguments:
<INPUT> Input file to analyze
Options:
--roi <X,Y,W,H> ROI for base estimation (x,y,width,height)
--base-method <METHOD> Base estimation method [default: regions]
Values: regions, border
--border-percent <PERCENT> Border percentage for "border" method [default: 5.0]
--json Output as JSON (machine-readable)
-s, --save <FILE> Save analysis to file (JSON format)
-v, --verbose Show detailed analysis output
-h, --help Print help
Process multiple files with shared settings. By default, assumes all images are from the same roll and shares base estimation from the first image.
invers batch [OPTIONS] [INPUTS]...
Arguments:
[INPUTS]... Input files or directories
Base Estimation:
--base-from <FILE> Base estimation file (JSON from 'analyze --save')
--base <R,G,B> Manual base RGB values
--per-image Estimate base per-image instead of sharing from first
Output Options:
--export <FORMAT> Export format: tiff16 (default) or dng
-o, --out <DIR> Output directory
Processing Options:
-w, --white-balance <PRESET> White balance preset [default: auto]
--exposure <FLOAT> Exposure compensation (1.0 = no change)
--bw Force black and white conversion mode
General Options:
-r, --recursive Recursively search directories for images
-j, --threads <N> Number of parallel threads
--silent Suppress non-essential output
-v, --verbose Enable verbose output
--cpu Force CPU-only processing
--dry-run List files that would be processed without processing
-h, --help Print help
Set up user configuration directory with default presets. Safe to run multiple times - won't overwrite existing files unless --force is used.
invers init [OPTIONS]
Options:
--force Force overwrite of existing files
-h, --help Print help
Generate shell completions for your shell of choice.
invers completions <SHELL>
Arguments:
<SHELL> Shell to generate completions for
Values: bash, zsh, fish, powershell
Examples:
bash: invers completions bash > ~/.bash_completion.d/invers
zsh: invers completions zsh > ~/.zfunc/_invers
fish: invers completions fish > ~/.config/fish/completions/invers.fish
--config-path Show the path to the config file being used and exit
The conversion pipeline processes images through these stages:
Input Image (TIFF/PNG)
│
▼
┌─────────────────────────────────────┐
│ Decode to f32 linear RGB (0.0-1.0) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Base Estimation (auto or manual) │
│ • Detect film base color │
│ • Calculate mask profile │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Invert to Positive │
│ • MaskAware: shadow floor correct │
│ • Linear: (base - neg) / base │
│ • Log: density-based inversion │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Shadow Lift (adaptive/fixed) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Auto-Adjustments │
│ • Auto-levels (histogram stretch) │
│ • Auto-color (neutralize casts) │
│ • Auto-exposure (normalize) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Color Matrix (3×3 correction) │
│ (skipped for MaskAware mode) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Tone Curve (S-curve/asymmetric) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Export to TIFF16 │
└─────────────────────────────────────┘
The default mask-aware inversion mode compensates for the orange mask in color negative film:
- Base Detection: Analyzes the film base to determine its orange characteristics
- Standard Inversion: Inverts each channel:
positive = 1.0 - (negative / base) - Shadow Floor Correction: Subtracts per-channel floor values from green and blue to remove the blue cast that would otherwise result from inverting the orange mask
The correction uses dye impurity values (how much extra light each dye layer absorbs) to calculate shadow floors:
- Magenta layer: Absorbs some blue light (impurity ~0.5)
- Cyan layer: Absorbs some green light (impurity ~0.3)
The correction strength is automatically scaled based on how "orange" the detected base is.
Film presets are YAML files that define film-specific conversion parameters.
name: "Generic Color Negative"
base_offsets: [0.0, 0.0, 0.0]
color_matrix:
- [1.10, -0.02, -0.08]
- [-0.02, 1.05, -0.03]
- [-0.08, -0.03, 1.15]
tone_curve:
curve_type: "asymmetric" # linear, s-curve, or asymmetric
strength: 0.4
toe_strength: 0.4 # Shadow lift (0.0-1.0)
shoulder_strength: 0.3 # Highlight compression (0.0-1.0)
toe_length: 0.25 # Where toe region extends
shoulder_start: 0.75 # Where shoulder begins
notes: "General-purpose color negative preset"| Preset | Description |
|---|---|
generic-color-negative.yml |
General-purpose color negative |
generic-color-negative-asymmetric.yml |
Color negative with asymmetric curve |
generic-bw.yml |
Black & white negative |
fuji-superia-400.yml |
Fuji Superia 400 specific |
optimized-standard.yml |
Optimized general-purpose |
- Linear: No curve applied (pass-through)
- S-Curve: Symmetric contrast enhancement with adjustable strength
- Asymmetric: Film-like curve with independent toe and shoulder controls for natural-looking conversions
Default pipeline settings can be configured in pipeline_defaults.yml:
inversion_mode: "mask-aware" # Default inversion algorithm
auto_levels_enabled: true
auto_levels_clip_percent: 0.1
auto_color_enabled: true
auto_exposure_enabled: true
auto_exposure_target: 0.18
shadow_lift_mode: "percentile"
highlight_compression: 0.95- Film presets:
~/invers/presets/film/ - Scan profiles:
~/invers/presets/scan/ - Pipeline config:
~/invers/pipeline_defaults.yml
invers/
├── crates/
│ ├── invers-core/ # Core conversion library
│ │ ├── src/
│ │ │ ├── lib.rs # Library exports
│ │ │ ├── models/ # Data structures (modular)
│ │ │ │ ├── mod.rs # Module exports
│ │ │ │ ├── base_estimation.rs
│ │ │ │ ├── cb.rs # Curves-based pipeline models
│ │ │ │ ├── convert_options.rs
│ │ │ │ ├── preset.rs
│ │ │ │ └── scan_profile.rs
│ │ │ ├── pipeline/ # Processing pipeline (modular)
│ │ │ │ ├── mod.rs # Main pipeline orchestration
│ │ │ │ ├── base_estimation.rs
│ │ │ │ ├── inversion.rs
│ │ │ │ └── tone_mapping.rs
│ │ │ ├── auto_adjust/ # Auto-adjustment algorithms (modular)
│ │ │ │ ├── mod.rs
│ │ │ │ ├── color.rs
│ │ │ │ ├── exposure.rs
│ │ │ │ ├── levels.rs
│ │ │ │ ├── parallel.rs
│ │ │ │ └── white_balance.rs
│ │ │ ├── cb_pipeline/ # Curves-based pipeline (NLP-inspired)
│ │ │ │ ├── mod.rs
│ │ │ │ ├── histogram.rs
│ │ │ │ ├── layers.rs
│ │ │ │ └── white_balance.rs
│ │ │ ├── config/ # Configuration system (modular)
│ │ │ │ ├── mod.rs
│ │ │ │ ├── defaults.rs
│ │ │ │ └── testing.rs
│ │ │ ├── gpu/ # GPU acceleration (optional)
│ │ │ │ ├── mod.rs
│ │ │ │ ├── context.rs
│ │ │ │ ├── buffers.rs
│ │ │ │ ├── pipeline.rs
│ │ │ │ └── shaders/
│ │ │ ├── decoders.rs # Image format decoders
│ │ │ ├── exporters.rs # Output format writers
│ │ │ ├── presets.rs # Preset management
│ │ │ ├── color.rs # Color space utilities
│ │ │ ├── diagnostics.rs # Debug utilities
│ │ │ └── testing.rs # Test utilities
│ │ └── Cargo.toml
│ │
│ ├── invers-cli/ # Command-line interface
│ │ ├── src/
│ │ │ ├── main.rs # CLI entry point
│ │ │ ├── lib.rs # Shared utilities
│ │ │ └── commands/ # Command implementations (modular)
│ │ │ ├── mod.rs
│ │ │ ├── analyze.rs
│ │ │ ├── batch.rs
│ │ │ ├── convert.rs
│ │ │ ├── debug.rs # Debug-only commands
│ │ │ ├── init.rs
│ │ │ └── preset.rs
│ │ └── Cargo.toml
│ │
│ └── invers-gui/ # GUI application (planned)
│ ├── src/
│ │ └── main.rs
│ └── Cargo.toml
│
├── config/
│ └── pipeline_defaults.yml
├── profiles/
│ └── film/ # Film preset files
│
├── Cargo.toml # Workspace manifest
└── README.md
- Working Colorspace: Linear RGB (Rec. 2020 primaries)
- Internal Precision: 32-bit floating point (0.0-1.0 range)
- Output Precision: 16-bit integer (TIFF16)
- Channel Order: RGB
- Sample multiple regions (borders, corners, center)
- Calculate median values per channel
- Validate candidates:
- Brightness threshold (>0.25)
- Noise statistics
- Orange mask characteristics (for color film)
- Select best candidate or fall back to center region
- Return medians and noise statistics
- Parallel processing for images >100k pixels
- Chunk-based processing (256-pixel chunks) for cache efficiency
- Single-pass histogram computation
- Partial sorting for median calculation (O(n) vs O(n log n))
- TIFF (8-bit, 16-bit, 32-bit)
- PNG (8-bit, 16-bit)
- RAW formats (planned: CR2, CR3, NEF, ARW, etc.)
- TIFF16 (16-bit linear)
- Linear DNG (planned)
- Core conversion pipeline
- TIFF/PNG decoding
- Base estimation
- Orange mask-aware inversion
- Tone curves (S-curve, asymmetric)
- Color matrix correction
- Auto-adjustments
- TIFF16 export
- CLI interface
- Preset system
- Modular architecture (core library and CLI)
- Curves-based pipeline (NLP-inspired algorithms)
- RAW format support
- Linear DNG export
- GUI application
- ICC profile embedding
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Built with Rust and the amazing ecosystem of image processing crates
- Inspired by professional film scanning workflows and tools like Grain2Pixel.
- Evan Dorsky's Why is Color Negative Film Orange?