Graduated ND filter transition calculator
A sky metering EV 15 over land at EV 12 means 3 stops of contrast and a 0.9 hard GND: enter both readings and the horizon to get the filter strength and where its transition goes on a 100 mm slab. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Sky ev
- 15
- Foreground ev
- 12
- Horizon type
- Flat
- Horizon percent
- 40
- Keep stops
- 0
- Filter height mm
- 100
- Delta stops
- 3
- Needed stops
- 3
Landscape photographers routinely meter a bright sky that wants a short exposure and a darker foreground that wants a longer one. A graduated neutral density filter darkens only the upper part of the frame so a single raw file can hold highlight texture without crushing the land. This graduated ND filter transition calculator accepts spot-metered exposure values for the sky and the foreground, subtracts them to obtain the contrast in stops, and recommends the nearest commercial GND strength from one to five stops. It also places a hard, soft, or reverse transition against the horizon so you know where the fade should sit in the frame and on a 75, 100, or 150 millimetre filter.
Hard, soft, and reverse transitions on the horizon
Strength is only half of a graduated ND decision. The other half is the transition: the fade between the dark and clear halves, and where that fade sits relative to the horizon. On a 100 millimetre filter that same percent becomes millimetres: a horizon at forty percent with a hard ten percent band occupies 35 to 45 millimetres from the bottom of the slab.
Near the top or bottom edge the band is clamped to the filter so you never get a start below zero or an end above one hundred.
What you can do with it
Seascape and lake preflight
Meter the bright sky and the darker water or sand, then take the recommended hard-edge GND and the millimetre marks so the fade sits on a straight horizon before the light drops.
Mountain and forest skylines
Use an irregular horizon to get a soft-edge strength and a wide transition band that can hide behind peaks instead of cutting a visible stripe through trees.
Sunset reverse-graduate planning
When the sun sits on the horizon, convert the sky-to-land EV gap into a reverse GND and a fade that starts on the horizon and opens toward the zenith.
FAQ
Which graduated ND filter do I need for a 3-stop difference?
Spot-meter the sky and the foreground. If the sky reads EV 15 and the land EV 12, the gap is 3 stops and the match is a 0.9 GND, also called ND8, which lets 12.5% of the light through.
Hard, soft or reverse graduated filter: which should I use?
A hard edge suits a straight horizon such as the sea or a lake. A soft edge with a wide band hides behind mountains or trees, and a reverse graduate, which fades upward, is for a sun sitting on the horizon.
Where should the transition sit on the filter?
On the horizon. With a flat horizon at 40% of the frame and a hard band of 10%, the fade runs from 35 to 45 mm from the bottom of a 100 mm filter.
What if the sky is more than five stops brighter?
The recommendation stops at a five-stop filter and adds a note that stacking filters or blending exposures will work better than a single slab. Under half a stop of contrast, no filter is suggested.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/hobby/photo-graduated-nd \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"sky_ev":15,"foreground_ev":12,"horizon_type":"flat"}'const res = await fetch("https://api.kit.forhosting.com/hobby/photo-graduated-nd", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"sky_ev": 15,
"foreground_ev": 12,
"horizon_type": "flat"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/hobby/photo-graduated-nd",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"sky_ev": 15,
"foreground_ev": 12,
"horizon_type": "flat"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/hobby/photo-graduated-nd", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"sky_ev":15,"foreground_ev":12,"horizon_type":"flat"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"sky_ev":15,"foreground_ev":12,"horizon_type":"flat"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/hobby/photo-graduated-nd", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"sky_ev": 15,
"foreground_ev": 12,
"horizon_type": "flat"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "hobby.photo_graduated_nd",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
Invalid meters, inverted sky and land readings, and out-of-range horizons fail with invalid_input and are not billed.
How sky and foreground EV become a GND strength
A graduated ND filter is specified in photographic stops, which are the same units as exposure value. Values under half a stop collapse to zero and the label no filter, because a one-stop slab would over-darken a nearly balanced scene. Values above five clamp to a five-stop recommendation and add a short note that stacking or exposure blending will serve better than a single slab. If a spot meter (or the camera in spot mode) reports sky_ev of fifteen for the bright sky and foreground_ev of twelve for the darker land, the scene holds three stops of contrast: delta_stops equals sky_ev minus foreground_ev. That three-stop gap is exactly what a 0.9 hard or soft GND is sold to remove. Optional keep_stops lets you leave a little natural contrast so the sky does not look as dark as the grass; needed_stops is then delta_stops minus keep_stops. The engine rounds needed_stops to the nearest integer in the closed set zero through five, which matches the commercial ladder ND2, ND4, ND8, ND16, and ND32 (optical densities 0.3, 0.6, 0.9, 1.2, and 1.5). The dark portion transmits one over two to the power of recommended_stops, so a three-stop GND passes 12.5 percent of the skylight. remaining_contrast_stops is the scene contrast left after that recommended slab. None of this invents a meter reading: if the sky is actually darker than the land the request is rejected, because a sky-weighted graduate would make the imbalance worse rather than better.
Hard, soft, and reverse transitions on the horizon
Set horizon_type to flat for a seascape, lake, or prairie with a straight line; the calculator answers with a hard-edge style and a nominal ten percent band centered on the horizon so the fade does not smear into trees or water. Set horizon_type to irregular for mountains, forests, or a broken skyline; the answer is a soft-edge style with a fifty percent band so the gradient can hide behind peaks instead of drawing a visible stripe across them. Set horizon_type to sunset when the sun sits on the horizon and the brightest band is a strip rather than the zenith; the answer is a reverse graduate that stays clear below the horizon, is darkest on the horizon line, and fades toward the top over about thirty percent of the frame. horizon_percent is measured from the bottom of the frame (zero at the bottom edge, one hundred at the top) and defaults to fifty. Optional filter_height_mm overrides the 100 millimetre default so a 75 millimetre or 150 millimetre holder reports start, end, and width in its own units.
Input contract, failures, and where the numbers are used
Optional output keys that do not apply, such as the stacking note when contrast is within five stops, are omitted rather than set to null. POST JSON with required finite sky_ev and foreground_ev in the interval from minus twenty to thirty, and horizon_type set to the string flat, irregular, or sunset. Optional horizon_percent must lie in zero to one hundred inclusive, optional keep_stops in zero to ten, and optional filter_height_mm in twenty to two hundred. Aliases such as ev_sky, fg_ev, style, and filter_mm are accepted so worksheets and typed forms reuse the same contract. On success the body echoes the inputs, then reports delta_stops, needed_stops, recommended_stops, optical_density, nd_factor, nd_label, gnd_label, transition_style, the start, end, center, and width of the fade in both percent and millimetres, remaining_contrast_stops, transmission and transmission_percent of the dark half, and a filter_needed flag. A formula string and the method tag graduated_nd_transition make API clients self-describing. Missing meters, non-finite numbers, an inverted sky that is darker than the land, an unknown horizon_type, or out-of-range percents fail with invalid_input naming the field. Those failures do not consume prepaid balance. Pair this endpoint with your own holder marks or live-view overlay if you need a drawing; the capability only computes stops and placement and never touches image bytes.
Technical questions
How do I calculate which graduated ND filter I need?
Send sky_ev and foreground_ev from a spot meter plus horizon_type. The API returns delta_stops = sky_ev − foreground_ev and recommended_stops as the nearest commercial 1–5 stop GND (or no filter under half a stop).
What is the difference between hard, soft, and reverse transitions?
horizon_type flat selects a hard ~10% band for straight horizons. irregular selects a soft ~50% band for broken skylines. sunset selects a reverse fade from the horizon toward the zenith, clear below.
How is horizon_percent measured?
From the bottom of the frame: 0 is the bottom edge, 100 is the top, and 50 (the default) is mid-frame. The same percent is applied to filter_height_mm to mark the slab in millimetres.
What does keep_stops do?
It is the contrast you want to leave after the filter. needed_stops = (sky_ev − foreground_ev) − keep_stops. Leave 0 to fully balance, or 1 to keep a one-stop natural difference between sky and land.
What does the graduated ND filter calculator cost?
The browser tool is free. API requests use prepaid KIT balance at $0.002 per successful call; invalid input is not billed.