Film Damage
Dust, hair and scratches that know which side of the negative they sit on
- image
- image
- defect_mask
The name undersells it. Film Damage isn't a stack of scanned dust-overlay PNGs blended onto your image - it's dust, dirt, hair and scratches generated procedurally, fully seeded, with the sign and color of every defect derived from physics instead of painted on. This is the Darkroom pack's answer to the thing that makes generated photos read as fake: they're too clean. Real scans are filthy. Give a portrait a couple of hair strands and a scratch and the brain stops checking for tells.
It's one of the author's pet peeves, honestly. Jeremie Louvaert (photographer, senior CG artist, animation background) built all 59 Darkroom nodes because he was tired of "film emulation" that was a color grade with a stock name slapped on it. Film Damage is the same ethos applied to damage: no vibe, just optics.
How it works. A defect is treated as an optical event at a plane, which means it applies a constant density offset - and where it sits in the chain decides whether it comes out white or dark. Set defect_origin to negative and dust blocks the printing light, so it prints white; switch to positive / scan-side and the same field reads dark. On a C-41 negative, an emulsion-side scratch removes dye layers top-down, so scratch_depth walks the scratch yellow → red → black. On reversal film it walks blue → cyan → white, and B&W has no dye layers at all, so scratches stay neutral. Counts and sizes are gamma-distributed (the Ivanova et al. method), with sizes anchored to real particle scale - the default dust_size lands around 22–41 μm at real-world size.
The inputs that matter:
defect_origin- the single most important knob. One dropdown that flips the whole look from white specks (negative) to dark specks (scan-side).film_type- Color neg / B&W / Reversal. Drives scratch color via dye-layer order.density- master defect-count multiplier. 0 is a passthrough. The tooltip nails the workflow: raise this, not the sizes, for a dirtier frame.scratch_side+scratch_depth- base scratches are refractive grooves that print white (wet-gate cancels them in real scanning); emulsion scratches lose dye and take color.seed- seeds every defect class: counts, sizes, positions, paths, scratch envelopes.
Outputs. Two: image and defect_mask. The mask is the pro move. Wire it into an inpainting graph to remove the "damage" from a real restoration workflow, or feed it as a strength mask so the worst of the filth stays off faces. And since a batch of N gets N different defect fields (a real roll varies frame to frame), you can rough up a whole contact sheet and every frame gets its own dirt - vary_per_frame off, and the identical field hits every frame instead.
Install. Same as the rest of the pack - ComfyUI Manager, search "Darkroom" (it's a single pack, so installing it gives you all 59 nodes), or:
cd ComfyUI/custom_nodes/
git clone https://github.com/jeremieLouvaert/ComfyUI-Darkroom.git
pip install -r ComfyUI-Darkroom/requirements.txt
Restart ComfyUI and it's under AKURATE/Darkroom/Film. Dependencies are light: scipy and opensimplex, nothing else, no API keys, no GPU required.
Gotchas. Two things actually bite. First, dust_size past ~1.0 stops reading as dust and starts reading as confetti - if it looks like a party, drop the sizes and keep density up. Second, this node is not allowed inside the pack's LUT bake chain (it uses pixel neighborhoods, which corrupts a 3D LUT lattice) - apply it after LUT Bake Extract, to graded_photo. And the softness default is 0.6 on the negative plane but 1.6 on the positive plane, because scanner-glass dirt sits off the image plane and defocuses. That's the kind of detail that makes the output feel real rather than decorated.
Inputs (21)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| defect_originopt | COMBO | negative (dust prints white) | Where the defect sits in the chain. This alone flips the sign: on a negative it blocks PRINTING light so the print goes lighter; on a positive it just blocks the image light |
| film_typeopt | COMBO | Color neg (C-41) | Drives emulsion-scratch colour via dye-layer order. B&W has no dye layers, so scratches stay neutral |
| print_gammaopt | FLOAT | 2.00.5–4 | Paper grade / scanner-inversion contrast. Negative-plane only: it is the exponent magnitude, so it sets how hard dust prints out |
| densityopt | FLOAT | 0.500–5 | Master defect count multiplier across all classes. 0 = passthrough. Raise this, not the sizes, for a dirtier frame |
| dust_amountopt | FLOAT | 1.00–3 | Dust count multiplier (gamma-distributed count and size) |
| dust_sizeopt | FLOAT | 0.350.05–4 | Dust mean radius, ref-px @1024 long edge. Default ~22-41um at real scale; larger reads as confetti, not dust |
| dirt_amountopt | FLOAT | 1.00–3 | Dirt/lint count multiplier. Larger, softer and lower opacity than dust, more strongly clustered |
| dirt_sizeopt | FLOAT | 1.10.1–8 | Dirt mean radius, ref-px @1024 long edge |
| hair_amountopt | FLOAT | 1.00–3 | Hair/fibre count multiplier. Short and long strands share one generator |
| hair_lengthopt | FLOAT | 14020–600 | Long-strand length, ref-px @1024 long edge (short strands are 0.35x this) |
| scratch_countopt | INT | 30–40 | Number of scratches. They run along the transport axis with bounded lateral wander |
| scratch_widthopt | FLOAT | 0.80.1–12 | Scratch core width, ref-px @1024. The literature 3-10px band describes heavily damaged archive film; stills want far less |
| scratch_sideopt | COMBO | base (refractive, neutral) | Which FACE of the film. Base = a refractive groove (removes no dye, wet-gate cancels it) so it prints white. Emulsion = material loss, colour set by depth |
| scratch_depthopt | FLOAT | 0.500–1 | Emulsion side only. Removes dye layers top-down: C-41 walks yellow -> red -> black, reversal walks blue -> cyan -> white |
| layer_densityopt | FLOAT | 0.700.1–2.5 | Optical density of dye removed per layer. Real maxima run 2-3, which drives straight to clear base and clips |
| transport_axisopt | COMBO | auto (along long edge) | Scratches follow the film transport. Auto = along the long edge (still 35mm). Cine runs vertically through the gate, hence vertical tramlines |
| softnessopt | FLOAT | 0.00–8 | Optical blur of the defect, ref-px. 0 = auto (0.6 negative-plane, 1.6 positive-plane: scanner-glass dirt sits off the image plane so it is more defocused) |
| base_scratch_castopt | FLOAT | 0.000–1 | TASTE ONLY, off by default. Green/cyan tint on base-side scratches. This is an inference from wet-gate physics plus orange-mask channel gain, not a sourced causal link |
| vary_per_frameopt | BOOLEAN | true | Batch of N gets N different defect fields (as a real roll does). Off = the identical field on every frame |
| seedopt | INT | 420–4294967295 | Seeds every defect class: counts, sizes, positions, paths and scratch envelopes |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| defect_mask | MASK | — |