Physically-based film grain rendering with GPU acceleration
Project description
SilverGrain
Physically-based film grain rendering for Python.
SilverGrain implements the photographic grain simulation algorithm from Newson et al. (2017), making it accessible as a friendly Python library and CLI tool. Unlike simple noise overlays, this approach models grain as a stochastic geometric process based on the actual physics of silver halide crystals in analog film.
The result: realistic, resolution-independent grain that scales cleanly to any output size.
Example
| Original | SilverGrain (luminance) |
|---|---|
Features
- Physically accurate: Models grain using Poisson point processes and Boolean geometry, matching how real film works
- Resolution independent: Render at any zoom level—grain structure remains consistent
- GPU accelerated: CUDA support for ~750× speedup on typical workloads
- Flexible processing: Apply grain to luminance only (preserves color) or per-channel (chromatic grain)
- Multiple interfaces: Use as a Python library, single-image CLI, batch processor, or dataset augmentation tool
- Adjustable strength: Blend grain with original image for subtle effects
Installation
# RECOMMENDED: With GPU acceleration (requires NVIDIA GPU + CUDA)
pip install silvergrain[gpu]
# CPU-only version (deathly slow, you have been warned)
pip install silvergrain
Quick Start
Command Line
Process a single image:
silvergrain input.png output.png --grain-radius 0.12
Batch process a directory:
silvergrain-batch images/ processed/ --grain-radius 0.15 --strength 0.8
Generate augmented variants:
silvergrain-augment clean_images/ augmented/ \
--count 10 \
--grain-radius 0.08:0.20 \
--strength 0.7:1.0
Python Library
from PIL import Image
from silvergrain import FilmGrainRenderer
# Load image
image = Image.open("input.png")
# Create renderer with desired grain characteristics
renderer = FilmGrainRenderer(
grain_radius=0.12, # Average grain size (smaller = finer grain)
n_monte_carlo=200, # Quality vs speed tradeoff
device='auto' # Use GPU if available
)
# Apply grain
output = renderer.process_image(image, mode='luminance', strength=1.0)
output.save("output.png")
How It Works
Traditional "film grain" effects just overlay noise patterns as gaussian, color, or luminance noise. SilverGrain does something more interesting: it simulates the actual stochastic geometry of photographic film grain.
The Physical Model
In real photographic film, light-sensitive silver halide crystals are randomly distributed across the emulsion. When exposed and developed, each crystal that absorbed a photon becomes an opaque grain. The key insight: darker regions have higher grain density, not larger grains.
SilverGrain models this using:
- Poisson point process: Grain centers placed randomly with density proportional to pixel brightness
- Boolean model: Each grain is a disk with random radius (optional log-normal distribution)
- Monte Carlo convolution: Simulate the optical filtering of the film-to-print process
This produces grain that:
- Scales correctly across brightness levels (more grain in shadows, less in highlights)
- Remains consistent when zooming or changing resolution
- Exhibits the characteristic "clumpy" structure of real film
The Practical Implementation
The Monte Carlo approach works by:
- For each output pixel, take N random samples from a Gaussian-offset neighborhood
- For each sample, determine if it's covered by a grain (using deterministic per-pixel RNG)
- Average the results to get the filtered grain value
This is parallel—perfect for GPU acceleration. The CUDA implementation typically runs 500x faster than CPU for typical images.
Usage Patterns
CLI Tools
silvergrain - Single Image Processing
Apply grain to one image with full control over parameters:
silvergrain input.png output.png \
--grain-radius 0.15 \
--grain-sigma 0.03 \
--n-monte-carlo 400 \
--mode luminance \
--strength 0.9
Key options:
--grain-radius: Mean grain size in pixels (0.05-0.3 typical)--grain-sigma: Grain size variation (0 = uniform, higher = more variation)--sigma-filter: Anti-aliasing strength (default 0.8)--n-monte-carlo: Sample count (higher = better quality, slower)--mode:luminance(color-preserving) orrgb(per-channel)--strength: Blend amount 0.0-1.0 (1.0 = full grain)--quality: Quick preset (fast,balanced,high)
silvergrain-batch - Batch Processing
Process multiple images with consistent settings:
# Process entire directory
silvergrain-batch images/ output/ --quality high
# In-place processing with suffix
silvergrain-batch images/ --grain-radius 0.12
# Recursive search
silvergrain-batch images/ output/ --recursive --strength 0.7
Key options:
- Output directory is optional—omit for in-place mode (adds
-grainy.pngsuffix) --recursive: Search subdirectories--simple: Minimal progress output
silvergrain-augment - Dataset Augmentation
Generate randomized variants for training data augmentation:
silvergrain-augment clean_images/ augmented/ \
--count 20 \
--grain-radius 0.08:0.20 \
--strength 0.7:1.0 \
--mode rand
Key features:
- Parameter ranges: Use
low:highsyntax for random sampling per variant - Fixed values: Use single numbers for consistent parameters
- Output structure: Creates
aug_0/,aug_1/, etc. with preserved filenames (perfect for paired dataloaders) --mode rand: Randomly choosesluminanceorrgbper variant
Example output structure:
augmented/
├── aug_0/
│ ├── image_001.png
│ └── image_002.png
├── aug_1/
│ ├── image_001.png
│ └── image_002.png
...
Library Usage
Basic Rendering
from PIL import Image
from silvergrain import FilmGrainRenderer
renderer = FilmGrainRenderer(grain_radius=0.12, n_monte_carlo=200)
image = Image.open("input.png")
output = renderer.render(image) # Full grain, same resolution
output.save("output.png")
Luminance vs RGB Modes
# Luminance mode: grain only affects brightness, preserves color
output = renderer.process_image(image, mode='luminance')
# RGB mode: independent grain per channel, can shift colors
output = renderer.process_image(image, mode='rgb')
Strength Blending
# Subtle grain at 50% strength
output = renderer.process_image(image, strength=0.5)
GPU Acceleration
# Explicit GPU (raises error if unavailable)
renderer = FilmGrainRenderer(device='gpu', grain_radius=0.12)
# Auto-detect (uses GPU if available, CPU otherwise)
renderer = FilmGrainRenderer(device='auto', grain_radius=0.12)
# Check which device is being used
print(renderer.device) # 'cpu' or 'gpu'
Zoom and Resolution Control
# Double the output resolution
output = renderer.render(image, zoom=2.0)
# Explicit output size
output = renderer.render(image, output_size=(1920, 1080))
Quality Presets
# Fast preview
renderer = FilmGrainRenderer(grain_radius=0.12, n_monte_carlo=100)
# Balanced (default)
renderer = FilmGrainRenderer(grain_radius=0.12, n_monte_carlo=200)
# High quality
renderer = FilmGrainRenderer(grain_radius=0.12, n_monte_carlo=400)
Parameter Guide
grain_radius (float, default 0.1)
Average grain radius in pixels. This is the most important parameter for controlling grain appearance.
- 0.05-0.08: Very fine grain (ISO 100-200 equivalent)
- 0.10-0.15: Medium grain (ISO 400-800)
- 0.20-0.30: Heavy grain (ISO 1600-3200)
Smaller values = finer, more subtle grain. Larger = coarser, more visible texture.
grain_sigma (float, default 0.0)
Standard deviation of grain size distribution (log-normal). Controls grain size variation.
- 0.0: All grains same size (uniform, can look artificial)
- 0.02-0.05: Subtle variation (realistic)
- 0.1+: High variation (artistic effect)
Small variation often looks more natural than perfectly uniform grain.
sigma_filter (float, default 0.8)
Gaussian filter strength for anti-aliasing. Simulates the optical blur of projection/viewing.
- 0.5-0.7: Sharper grain (more texture)
- 0.8-1.0: Smoother grain (more subtle)
- 1.2+: Very smooth (soft focus effect)
n_monte_carlo (int, default 800)
Number of samples per pixel. Higher = better quality but slower.
- 100-150: Fast preview
- 200-300: Balanced quality/speed
- 400-800: High quality
- 1000+: Diminishing returns (mostly overkill)
Quality scales roughly as √N, so doubling sample count gives ~40% quality improvement.
device (str, default 'auto')
- 'auto': Use GPU if available, fall back to CPU
- 'cpu': Force CPU rendering (always available)
- 'gpu': Force GPU rendering (errors if unavailable)
mode (str, default 'luminance')
- 'luminance': Apply grain only to brightness channel (preserves colors, more realistic for color film)
- 'rgb': Apply independent grain to R, G, B channels (can shift colors, more film-like color artifacts)
strength (float, default 1.0)
Blend factor between original and grained image.
- 0.0: Original image, no grain
- 0.5: 50/50 blend
- 1.0: Full grain effect
Performance
Benchmark results (1024×1024 gradient, n_monte_carlo=200):
- CPU: ~159 seconds (2.6 minutes)
- GPU (CUDA): ~0.21 seconds
GPU acceleration: ~750× speedup.
The GPU advantage scales dramatically with:
- Image size (CPU time scales quadratically, GPU stays fast)
- Monte Carlo sample count (more samples = bigger CPU penalty)
- Batch processing (GPU overhead amortizes across images)
Examples
See examples/ directory for complete working examples:
example_01_basic_grain.py- Default settingsexample_02_fine_grain.py- Subtle, fine grainexample_03_heavy_grain.py- Coarse, heavy grainexample_04_luminance_mode.py- Color-preserving grainexample_05_rgb_mode.py- Per-channel grainexample_06_blended_strength.py- Partial grain blendingexample_07_gpu_accelerated.py- GPU rendering
Technical Background
SilverGrain implements the algorithm described in:
Alasdair Newson, Julie Delon, and Bruno Galerne. "Realistic Film Grain Rendering." Image Processing On Line, 7:165–183, 2017. https://doi.org/10.5201/ipol.2017.192
The original paper provides a C++ reference implementation. SilverGrain reimplements the ideas directly from the paper in Python with:
- Modern NumPy/Numba architecture
- Optional GPU acceleration via CUDA
- Simplified API for common use cases
- CLI tools for practical workflows
Key Differences from the Paper
-
Algorithm selection: The paper describes both grain-wise and pixel-wise algorithms with automatic selection. SilverGrain currently implements only the pixel-wise approach, which provides good performance across all grain sizes and parallelizes efficiently on GPU.
-
Default parameters: Higher default sample counts (n_monte_carlo=800 vs paper's lower values) since GPU acceleration makes this practical.
-
Color processing: An explicit
modeparameter for luminance-only vs per-channel grain. -
Blending: A
strengthparameter for partial grain effects, not in original paper.
Why Not Just Use Noise?
Simple approaches like adding Gaussian noise or overlaying pre-made grain textures have several problems:
- Wrong brightness relationship: Real grain density increases with exposure (darker = more grain), but noise is uniform
- Resolution dependence: Noise patterns don't scale correctly when resizing
- Statistical properties: Real grain has spatial correlations (clumping) that random noise lacks
- Physical implausibility: Noise distributions don't match actual photographic processes
SilverGrain's physics-based approach produces grain that:
- Scales correctly across brightness levels
- Remains consistent at any resolution
- Exhibits realistic spatial structure
- Matches actual photographic characteristics
License
AGPLv3 - see LICENSE file for details.
Citation
If you use SilverGrain in research, please cite the original paper:
@article{newson2017realistic,
title={Realistic Film Grain Rendering},
author={Newson, Alasdair and Delon, Julie and Galerne, Bruno},
journal={Image Processing On Line},
volume={7},
pages={165--183},
year={2017},
doi={10.5201/ipol.2017.192}
}
Project details
Release history Release notifications | RSS feed
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 silvergrain-0.2.tar.gz.
File metadata
- Download URL: silvergrain-0.2.tar.gz
- Upload date:
- Size: 50.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01220a57c77fbff79748fd8f6a190aa1c709dce4283a18daf1a011cdc4ce5387
|
|
| MD5 |
0e73dac18faae7ab73f8313f69585186
|
|
| BLAKE2b-256 |
7bf1d960caddc6b1e03e57bfdf42fc69df9893c3ab02e254883864897e4a042a
|
File details
Details for the file silvergrain-0.2-py3-none-any.whl.
File metadata
- Download URL: silvergrain-0.2-py3-none-any.whl
- Upload date:
- Size: 47.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0984f5716fd18bc872a5b6410402958d4f49e4e9f07cfb43d9fb68f918e326b
|
|
| MD5 |
aca8166ba262d3df3f68a10b594ea5b4
|
|
| BLAKE2b-256 |
1a5985155bc27ab6b77d96e7a390866d8ccf750beab4c5707a84733141d69b9d
|