# `filmgrain` - Realistic Film Grain Simulation `filmgrain` is a Python script that applies realistic film grain to digital images. It implements the procedural grain synthesis method described by Zhang et al. (2023) in "Procedural Synthesis of Film Grain Noise". This method leverages GPU acceleration via the `warp` library for efficient processing. ## Features * Implements the film grain model by Zhang et al. (2023). * GPU accelerated using `warp-lang` (requires a CUDA-capable NVIDIA GPU). * **Adjustable Grain Parameters:** * `--mu_r`: Mean grain radius (relative to pixel size), controlling grain size. * `--sigma`: Standard deviation of the Gaussian filter for noise blurring (`sigma_filter`), controlling grain softness/sharpness. * **Monochrome Option:** Apply spectrally uniform (monochromatic) grain based on image luminance, or apply independent RGB grain. * Precomputes a variance lookup table (LUT) for performance. * Supports RAW (Sony ARW, DNG), TIFF, PNG, and JPG inputs. * Outputs TIFF (16-bit uncompressed), PNG (8-bit), or JPG (8-bit). ## Requirements * **NVIDIA GPU:** A CUDA-enabled NVIDIA graphics card is required as `warp-lang` currently targets CUDA. * **CUDA Toolkit:** Ensure you have a compatible version of the CUDA Toolkit installed. * **`uv`:** For easy execution and dependency management. ## Usage The script is designed to be run with `uv`. ### Direct Execution with `uv` (Recommended) You can run `filmgrain` directly from its Git repository URL: ```bash uv run https://git.dws.rip/dubey/filmsim/raw/branch/main/filmgrain -- [options] ``` **Example:** ```bash # Ensure input.png is in your current directory or provide a full path # Output will be output_grained.jpg uv run https://git.dws.rip/dubey/filmsim/raw/branch/main/filmgrain -- input.png output_grained.jpg --mu_r 0.1 --sigma 0.8 --mono ``` ### Local Execution If you have cloned the repository or downloaded the script: 1. Navigate to the script's directory or ensure it's in your PATH. 2. Run the script: ```bash uv run ./filmgrain -- [options] ``` Or, if `filmgrain` is in the current directory and executable: ```bash ./filmgrain [options] ``` ## Arguments ### Positional Arguments: * `input_image`: Path to the input image. * Supported formats: TIFF, PNG, JPG, Sony ARW, DNG. * `output_image`: Path to save the output image with film grain. * Output format is determined by the file extension: * `.tiff` / `.tif`: 16-bit uncompressed TIFF. * `.png`: 8-bit PNG. * `.jpg` / `.jpeg`: 8-bit JPG (quality 95). ### Options: * `--mu_r FLOAT`: Mean grain radius (relative to pixel size). (Default: `0.1`) * Controls the apparent size of the grain particles. Larger values mean larger grains. * Must be positive. * `--sigma FLOAT`: Standard deviation of the Gaussian Filter for noise blurring (`sigma_filter`). (Default: `0.8`) * Controls the sharpness or softness of the grain. Higher values result in softer, more blurred grain. * Must be positive. * The paper suggests `sigma_filter` should ideally be >= `3 * mu_r` for the model's approximations to hold well. The script will issue a warning if this condition is not met. * `--seed INT`: Random seed for noise generation. (Default: `42`) * Allows for reproducible grain patterns. * `--mono`: Apply monochrome film grain across channels based on luminance. (Default: `False` - apply RGB grain) * If enabled, grain is generated based on the image's luminance and applied uniformly to R, G, and B channels. * If disabled, grain is generated and applied independently for each color channel (R, G, B), leading to colored grain. ## Workflow 1. **Load Image:** * Reads the input image. RAW files (ARW, DNG) are post-processed using `rawpy` (16-bit output, camera WB, no auto-bright, linear gamma). Other formats (TIFF, PNG, JPG) are read using `imageio`. * The image is converted to a 0-1 float32 range. 2. **Precompute Variance LUT:** * Calculates a lookup table (LUT) for the variance of the Boolean-Poisson (BP) noise model. This involves numerically integrating a complex function derived from the grain model, significantly speeding up per-pixel variance calculation later. 3. **Prepare Gaussian Kernel:** * Creates a 2D Gaussian kernel based on the `sigma_filter` parameter. This kernel is used to blur the initial noise, shaping the grain's appearance. The kernel radius is determined by `sigma_filter`. 4. **Process Channels (RGB or Monochrome):** * **Monochrome Mode (`--mono`):** * The input image is converted to grayscale (luminance). * `generate_noise_kernel` (Warp kernel): Generates initial random noise for the grayscale image. The noise variance at each pixel is determined by the pixel's intensity (u_val) and the precomputed variance LUT, scaled by `mu_r` and `sigma_filter`. * `convolve_2d_kernel` (Warp kernel): Convolves the generated noise with the 2D Gaussian kernel to produce the filtered (shaped) grain. * This single-channel filtered noise is then used for all R, G, and B output channels. * **RGB Mode (default):** * The process above (noise generation and convolution) is performed independently for each color channel (R, G, B) of the input image. * Different random seeds are used for each channel (seed, seed+1, seed+2) to ensure decorrelated grain patterns. * If the input is grayscale, the R channel's noise is copied to G and B. 5. **Add Noise and Clip:** * `add_rgb_noise_and_clip_kernel` (Warp kernel): Adds the filtered noise (either monochrome or per-channel RGB) to the original image channels. * The result is clipped to the 0.0 - 1.0 range. 6. **Save Output:** * The final image with added grain is copied from the GPU back to the CPU. * It's converted to the appropriate bit depth (16-bit for TIFF, 8-bit for PNG/JPG) and saved. ## Technical Details (from Zhang et al. 2023) * **Boolean-Poisson (BP) Model:** The grain is modeled as randomly placed circular grains (disks) of radius `mu_r`. The local density of these grains depends on the image intensity. * **Variance Calculation:** The core of the model involves calculating the variance of the noise that would result from this BP process. This variance is intensity-dependent. The `w_func` and `CB_const_radius_unit` functions in the script are related to the geometric calculations of overlapping disks, which are part of this variance derivation. The integral of `CB_const_radius_unit * x` gives the key component for variance, which is precomputed into the LUT. * **Filtering:** The raw noise generated based on this variance is then filtered (convolved) with a Gaussian kernel (`h` in the paper, controlled by `sigma_filter`) to achieve the final grain appearance. The paper notes that the filter's standard deviation `sigma_filter` should be significantly larger than `mu_r` (e.g., `sigma_filter >= 3 * mu_r`) for certain approximations in their model to be accurate.