How this works
Each frame is measured, then corrected with one per-channel curve applied on the GPU — the same transform for stills and video.
The frame is decoded to linear light. The darkest half-percent of each channel is taken as an estimate of the veiling light from backscatter and subtracted there; doing that in linear light is the part that matters, because backscatter adds to radiance, so removing it in gamma-encoded space bends the shadows. Per-channel gain then comes from a Minkowski-norm grey-world assumption. Gains are capped rather than allowed to invent red out of nothing, so deep footage comes back desaturated instead of pink.
This cannot be right everywhere in a frame. Attenuation is exponential in distance, so the correction that suits a subject at arm's length is wrong for the reef behind it. Undoing that properly needs a per-pixel depth estimate, which a single image does not carry.
If a GoPro's .LRV proxy is present beside a clip, the measurement is taken from that instead. The estimate is a histogram on a 512 px downscale, and a proxy already is that downscale, so the answer is the same — but decoding it is cheap enough to sample the clip many times more densely, which is what a long dive over changing depth needs. The proxy is checked against the real clip at three points first, and ignored if its duration, framing or measurements disagree.
Measurement runs on a downscaled copy — the estimate is histogram-based, so a 512 px sample gives the same answer as the full frame. For video the estimate is taken on frames spread through the clip, smoothed, then held fixed unless you ask it to follow the light. Stills are ordered by capture time and smoothed along that sequence, so a card's worth of shots ends up consistent rather than each one corrected into its own look.
Everything runs in this page, on the machine you are reading it on. There is no server: the page is a single HTML file, and whoever hosts it only sends you that file. Decoding, correcting and encoding all happen in your own browser, on your own GPU, and your footage is never uploaded anywhere. Once the page has loaded you can disconnect the network and it keeps working.
That is also why a clip can fail for reasons specific to your machine — the browser's encoder is your graphics card's encoder, with its real limits. Test video path reports what yours can do.
Free, and yours to keep. If it saved you an evening of grading, you can buy me a coffee — entirely optional, and nothing in the tool changes either way.
Licences
This page's own code is yours to host, share and modify as you like. Two libraries are bundled, both under permissive terms:
Mediabunny — demuxing, decoding, encoding and muxing. Mozilla Public License 2.0. Bundled unmodified; the source is at that link, which is what the MPL asks you to pass on.
JSZip — packing the downloads. Dual MIT or GPL-3.0; used here under the MIT terms. Bundled unmodified.
Nothing here is under a copyleft licence that reaches your own code, so hosting this page or handing it to a friend carries no obligation beyond keeping these notices.