Skip to content
AI360Xpert
Beta

Low-Light Enhancement

Low-light enhancement rescues near-black photos by separating illumination from true colour, brightening the light without bleaching the scene.

Retinex splitting separates dim light from true colour so brightening lifts scenes without bleaching them
Retinex splitting separates dim light from true colour so brightening lifts scenes without bleaching them

Why Does This Exist?

Night photos bury scenes in darkness with noise riding along. Global brightening lifts both: grey wash, amplified grain, clipped highlights around every lamp. The damage mixes illumination failure with sensor noise, and one slider cannot split them.

Retinex methods decompose each pixel into illumination LL and reflectance RR: enhance LL, preserve RR, denoise jointly. Zero-reference variants learn the decomposition without paired dark-bright photos, which barely exist. The task map is image-to-image tasks, and the grain side is image denoising.

Think of It Like This

Restoring a charcoal sketch under dim bulbs

A gallery shows charcoal sketches under dying bulbs. Visitors see grey murk, but the sketch holds full contrast: dark lines, white paper. Replacing bulbs reveals the art; scrubbing the sketch itself destroys it.

Illumination is the bulbs and reflectance is the sketch. Brighten bulbs, preserve lines. The work fails when murk ate the lines entirely: no bulb reveals strokes never drawn, which is the information limit below.

How It Actually Works

Retinex writes observed I=R∘LI = R \circ L, element-wise reflectance times illumination. Networks estimate LL as smooth (large blur-like structures) and RR as detail, then remap LL upward with gamma or learned curves while RR stays fixed:

I=R∘L,L′=Lγ,  γ<1 brightensI = R \circ L, \qquad L' = L^{\gamma}, \; \gamma < 1 \text{ brightens}

Zero-reference losses penalise spatial inconsistency, overexposure, and colour drift without any bright ground truth. Denoising runs jointly because brightening multiplies hidden noise visibly.

Worked example

Pixel value 20/255=0.07820/255 = 0.078. Gamma γ=0.5\gamma = 0.5 lifts it to 0.078=0.28\sqrt{0.078} = 0.28, back to 71/25571/255: nearly four times brighter. Reflectance preserved means a red wall stays red-shifted rather than washing grey. Noise at ±3\pm 3 levels becomes ±6\pm 6 after lifting, which is why the pipeline denoises after, never before, the brightening step.

Code

Gamma lifting with the brightness math exposed.

def gamma_lift(v: int, gamma: float = 0.5) -> int:    """Brighten one 8-bit pixel through a gamma curve."""    return round(255 * (v / 255) ** gamma)

print(gamma_lift(20))  # -> 71 nearly four times brighterprint(gamma_lift(200))  # -> 226 highlights compress, not clip

Watch Out For

Amplifying noise into confetti

Dark regions hide heavy noise that brightening multiplies into coloured speckle. Pipelines that enhance before denoising print confetti across shadows.

Order the pipeline enhance-then-denoise or joint. Validate shadow crops at full zoom; global metrics forgive speckle averages hide.

Colour casts from channel-independent curves

Independent per-channel brightening shifts white balance: sodium lamps turn everything orange-amplified, moonlight goes blue. Reflectance preservation must span channels jointly.

Constrain colour ratios during enhancement or correct white balance after. Check neutrals: grey walls should stay grey.

The Quick Version

  • Low-light photos mix dim illumination with noise; one slider cannot split them.
  • Retinex decomposes I=R∘LI = R \circ L and brightens LL while fixing RR.
  • Our pixel lifts 20→7120 \to 71 under gamma 0.50.5 while highlights compress safely.
  • Zero-reference losses train without paired dark-bright photos.
  • Denoise after brightening, and keep colour ratios joint across channels.