ComfyUI Node

Film Damage

Dust, hair and scratches that know which side of the negative they sit on

By jeremieLouvaert·Created 6 months ago·Updated 10 days ago· 107
Film Damage
  • image
  • image
  • defect_mask
defect_originnegative (dust prints white)
film_typeColor neg (C-41)
print_gamma2.0
density0.50
dust_amount1.0
dust_size0.35
dirt_amount1.0
dirt_size1.1
hair_amount1.0
hair_length140
scratch_count3
scratch_width0.8
scratch_sidebase (refractive, neutral)
scratch_depth0.50
layer_density0.70
transport_axisauto (along long edge)
softness0.0
base_scratch_cast0.00
vary_per_frametrue
seed42

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.

CategoryAKURATE/Darkroom/Film

Inputs (21)

NameTypeDefaultDescription
imageIMAGE
defect_originoptCOMBOnegative (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_typeoptCOMBOColor neg (C-41)Drives emulsion-scratch colour via dye-layer order. B&W has no dye layers, so scratches stay neutral
print_gammaoptFLOAT2.00.5–4Paper grade / scanner-inversion contrast. Negative-plane only: it is the exponent magnitude, so it sets how hard dust prints out
densityoptFLOAT0.500–5Master defect count multiplier across all classes. 0 = passthrough. Raise this, not the sizes, for a dirtier frame
dust_amountoptFLOAT1.00–3Dust count multiplier (gamma-distributed count and size)
dust_sizeoptFLOAT0.350.05–4Dust mean radius, ref-px @1024 long edge. Default ~22-41um at real scale; larger reads as confetti, not dust
dirt_amountoptFLOAT1.00–3Dirt/lint count multiplier. Larger, softer and lower opacity than dust, more strongly clustered
dirt_sizeoptFLOAT1.10.1–8Dirt mean radius, ref-px @1024 long edge
hair_amountoptFLOAT1.00–3Hair/fibre count multiplier. Short and long strands share one generator
hair_lengthoptFLOAT14020–600Long-strand length, ref-px @1024 long edge (short strands are 0.35x this)
scratch_countoptINT30–40Number of scratches. They run along the transport axis with bounded lateral wander
scratch_widthoptFLOAT0.80.1–12Scratch core width, ref-px @1024. The literature 3-10px band describes heavily damaged archive film; stills want far less
scratch_sideoptCOMBObase (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_depthoptFLOAT0.500–1Emulsion side only. Removes dye layers top-down: C-41 walks yellow -> red -> black, reversal walks blue -> cyan -> white
layer_densityoptFLOAT0.700.1–2.5Optical density of dye removed per layer. Real maxima run 2-3, which drives straight to clear base and clips
transport_axisoptCOMBOauto (along long edge)Scratches follow the film transport. Auto = along the long edge (still 35mm). Cine runs vertically through the gate, hence vertical tramlines
softnessoptFLOAT0.00–8Optical 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_castoptFLOAT0.000–1TASTE 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_frameoptBOOLEANtrueBatch of N gets N different defect fields (as a real roll does). Off = the identical field on every frame
seedoptINT420–4294967295Seeds every defect class: counts, sizes, positions, paths and scratch envelopes

Outputs (2)

NameTypeDescription
imageIMAGE
defect_maskMASK