Tuning the Threshold and Detection Parameters#
panache’s dynamic-threshold mode ("dynamic_threshold": true) re-derives the SPM
flood-fill threshold for every scene and every plume from the scene’s own gradient
field, rather than applying one fixed number. That adaptivity is controlled by a
small set of parameters, several of which are easy to misread from their names
alone. This page works through the algorithm step by step with real numbers, gives
symptom tables for setting each parameter too high or too low, and reports the
results of running panache on real SEXTANT satellite data while sweeping each
parameter through a range of values.
It complements the workflow and masks vignettes: those show what a run produces, this one explains why the plume masks come out the shape they do, and what to change if they look wrong.
1. How one scene is thresholded, step by step#
For each plume, panache shoots a fan of transects outward from the configured
starting_points pixel and reads SPM values step by step, moving away from the
river mouth. The example below uses real-world values from the Gironde plume
(BAY_OF_BISCAY, August 2000): minimal_threshold ≈ 1.6 g m⁻³, maximal_threshold ≈ 37 g m⁻³, quantile_to_use = 0.2.
Suppose one westward transect reads:
pixel: 0 1 2 3 4 5 6 7 8 9
SPM: [180, 90, 45, 22, 12, 8, 5, 3, 2, 1.5] g m⁻³
Step 1: maximal_threshold = 37 clips the first three pixels, which are
close enough to the mouth to still be at the ceiling:
[37, 37, 37, 22, 12, 8, 5, 3, 2, 1.5]
Clipped pixels also have their gradient forced to +∞, marking them “saturated
near-mouth zone, not a plume edge candidate.”
Step 2: gradients are computed (successive differences, +∞ excluded from
ranking):
gradients: [+∞, +∞, 15, 10, 4, 3, 2, 1, 0.5]
Step 3: only the steepest 90% of finite gradients survive
(15 × 0.9 = 13.5). Only the gradient of 15 clears that bar. The SPM value at
that pixel, 22 g m⁻³, becomes this transect’s plume-edge candidate.
Step 4: edge candidates are pooled across every fan direction. Suppose across 12 transect directions the collected edge values are:
[22, 19, 25, 18, 20, 23, 17, 21, 24, 19, 20, 22] g m⁻³
Step 5: quantile_to_use = 0.2 takes the p20 of those values, ≈ 19 g m⁻³.
Step 6: minimal_threshold = 1.6 is applied as a floor:
max(19, 1.6) = 19. The final scene threshold is 19 g m⁻³.
Note
Without maximal_threshold clipping the near-mouth pixels, the huge gradient
between pixels 0 and 1 (180 → 90, Δ = 90) would dominate every transect,
anchoring the detected threshold at roughly 90 g m⁻³. The flood fill would then
include only the densest near-mouth pixels, and the diffuse offshore plume would
be missed entirely. This is the single most common cause of under-sized plume
masks; see Test 1 below for a real example.
2. What each parameter controls#
Parameter |
Role |
|---|---|
|
Where along the transect the algorithm is allowed to look for a plume edge. Clips SPM above the ceiling before gradients are computed, forcing the search to look past the concentrated near-mouth turbidity. Lowering it pushes the search further offshore; raising it lets the algorithm settle for an edge closer to the mouth. |
|
A floor on the final threshold. If the gradient search returns something unrealistically low (e.g. |
|
How inclusive the threshold is within the pool of edge candidates. Given the 12 candidates from the example above (17-25 g m⁻³): |
3. Diagnostic: symptoms of a mis-set parameter#
Parameter |
Set too high |
Set too low |
|---|---|---|
|
Effectively no clipping happens; the huge near-mouth gradient dominates every transect and anchors the threshold close to it. Only the densest near-mouth pixels survive. Detected area is tiny and confined to the estuary. |
The clipping covers most of the plume body, not just the turbid mouth. The gradient field collapses and the algorithm falls back to |
|
On a typical high-turbidity day the computed threshold already exceeds this and the floor stays inactive; but on a low-turbidity day (e.g. after calm weather) it can force the threshold up past what the gradient search actually found. The plume on that day is missed entirely, a spurious gap in the time series. |
The floor is always inactive. On scenes where the gradient search fails outright (e.g. cloud gaps break every transect), the algorithm falls back to this value as the detected threshold. The flood fill spreads across the ocean. |
|
Only the densest core near the mouth exceeds the threshold; the diffuse offshore plume body is invisible. Area and centroid are systematically biased toward the estuary mouth. |
Some edge candidates may be artefacts (e.g. resuspension patches that produce a steep, but not plume-related, gradient). A very low quantile selects the lowest-SPM artefact as the threshold. The flood fill leaks into resuspension areas or open ocean. |
4. Where the auto-estimated bounds come from#
When dynamic_threshold: true and a plume’s minimal_threshold/maximal_threshold
are left as null (the default for all built-in zone presets), panache estimates
them automatically rather than requiring hand-tuned values. This happens once per
batch, before any scene is processed, from a window of pixels around the river
mouth, pooled across every loaded scene
(_compute_batch_thresholds,
runner.py). The bounds stay fixed for the whole run rather than varying scene to
scene. The quantiles used are configurable:
{
"near_mouth_lower_quantile": 0.25,
"near_mouth_upper_quantile": 0.75
}
(defaults shown; minimal_threshold uses the lower quantile, maximal_threshold
the upper one).
4.1 The sampling window: shape and size#
Two further, less obvious settings control which pixels feed that estimate:
{
"near_mouth_window_shape": "circle",
"near_mouth_radius_pixels": 15
}
"circle" (the default) samples every water pixel within a Euclidean radius of
near_mouth_radius_pixels around the starting point; "square" instead samples a
square block using Chebyshev distance, where near_mouth_radius_pixels is the
half-width (1 → a 3×3 window, 2 → 5×5, 3 → 7×7). On the ~0.01° native SEXTANT
grid, the default 15-pixel radius spans roughly 15 km in every direction from the
mouth, comfortably reaching past the immediate turbid core into more ambient water.
Warning
A tighter window is not a free upgrade. Test 3
below shows that shrinking to a 3×3 square does sharpen the near-mouth SPM
distribution as expected, but the resulting bounds become dominated by whichever
one or two pixels happen to be most turbid that day. On lower-turbidity days that
can push minimal_threshold above every SPM value in the whole scene, killing
detection outright, on 33% of days in one real test. A moderate window (5×5-7×7,
or a smaller-radius circle) captures most of the benefit without that failure mode.
4.2 The gradient-steepness cutoff#
Step 3 of the worked example above (only the steepest 90% of finite gradients survive) is controlled by a separate parameter:
{
"gradient_steepness_fraction": 0.9
}
This is not a percentile of the gradient distribution across the transect,
despite reading that way. It is the fraction of the single steepest finite
gradient found anywhere on that transect that a point’s own gradient must clear
to be kept as an edge candidate. Concretely, for gradients [15, 10, 4, 3, 2, 1, 0.5], a cutoff of 0.9 keeps only the 15 (15 × 0.9 = 13.5); a true
90th-percentile filter over the same values would instead keep the top decile
of the whole distribution, a much larger and more forgiving set. In practice this
means the default setting isolates a single sharp step per transect and discards
the gentler “shoulder” of secondary gradients that a real SPM front usually also
has, biasing the detected edge toward the single most abrupt near-mouth feature,
the same direction of bias as maximal_threshold set too high.
No published river-plume or ocean-front detection method was found that thresholds on a fraction of a single scene’s own maximum gradient. The closest analogues in the literature both threshold on a statistic of the pooled gradient distribution instead:
Fixed, empirically-tuned magnitude thresholds, set once from many scenes, not re-derived per scene. Li et al. (2023), detecting the Yellow River Estuary turbid/clear-water demarcation with Canny edge detection on SPM imagery, report gradient-magnitude thresholds of 0.05-0.20 chosen by inspection across the whole dataset. doi:10.3389/fmars.2023.1234631
Percentile-of-distribution thresholds, the convention in the Belkin-O’Reilly ocean-front detection literature that underlies most published front climatologies: a representative downstream application sets its regional gradient threshold at the 70th percentile computed over all suitable pixels in a multi-decadal pooled dataset, not a fraction of any single scene’s maximum. doi:10.1016/j.jmarsys.2008.11.018
No gradient threshold at all. Tavora et al. (2023)’s PLUMES algorithm classifies plume pixels by statistical similarity to user-placed control points at the river mouth and in background water, explicitly avoiding any fixed or scene-relative threshold. doi:10.3389/fmars.2023.1215327
5. Evidence from real SEXTANT data#
The three sensitivity tests below were run on real SEXTANT SPM satellite data
(not synthetic test fixtures) to check the reasoning above against actual plume
detection outcomes. All used dynamic-threshold mode; nothing here changes any
zone preset default; every value below is exactly what came out of a real
panache run.
Test 1: near_mouth_lower/upper_quantile (Bay of Seine, full year 2009)#
The zone-default quantiles (p25/p75) systematically under-detect the Seine plume: visually, the flagged area misses much of the visibly high-SPM water on the daily maps. Lowering the upper quantile from 0.75 to 0.50 (and the lower from 0.25 to 0.10) roughly doubles the mean daily area and lowers the mean threshold from 9.65 to 5.99 g m⁻³:
mean area (km²) |
median area (km²) |
mean threshold (g m⁻³) |
|
|---|---|---|---|
Default (p25/p75) |
208.7 |
136.9 |
9.65 |
Loosened (p10/p50) |
418.9 |
265.4 |
5.99 |
This does close roughly half the gap to an independent surface-area proxy for the
Seine’s freshwater influence for that year, but it also introduces 9 days out of
365 (2.5%) where the batch-pooled minimal_threshold drops low enough that the
flood fill “leaks” across several thousand km² of open water unrelated to river
discharge, confirmed by cross-checking against daily river-discharge records
(these leak days sit at or below the year’s median discharge, so they are not
real high-flow events). A less extreme lower quantile (0.15-0.20) is likely to
capture most of the area gain while avoiding that failure mode, but has not been
tested.
Test 2: gradient_steepness_fraction (Bay of Seine, June 2009)#
Sweeping the cutoff from the default 0.9 down to 0.1 in steps of 0.1, on a
30-day sample:
|
mean area (km²) |
mean threshold (g m⁻³) |
area vs. default |
|---|---|---|---|
0.9 (default) |
262.6 |
3.49 |
n/a |
0.7 |
261.2 |
3.42 |
+5% |
0.5 |
300.3 |
3.22 |
+28% |
0.3 |
399.9 |
2.85 |
+80% |
0.1 |
417.5 |
2.80 |
+88% |
Unlike Test 1, no zero-pixel days and no runaway leak days appeared at any
setting: minimal_threshold (a separate, independent mechanism, see §2) still
guards against spillover regardless of how the steepness cutoff is set. Most of
the area gain arrives by 0.4-0.5; pushing further toward 0.1 adds
comparatively little for a much larger departure from the percentile-of-distribution
convention used in the published literature (§4.2), so a moderate value in that
range looks like a more defensible middle ground than either extreme.
Test 3: near-mouth sampling window (Gulf of Lion, June 2009)#
Comparing the default 15-pixel circle against square windows of decreasing size,
holding near_mouth_lower/upper_quantile at the zone default:
Window |
mean area (km²) |
Grand Rhône threshold, mean (g m⁻³) |
zero-pixel days (/30) |
|---|---|---|---|
circle, radius 15 (default) |
511.8 |
2.07 |
0 |
square 7×7 |
262.8 |
2.97 |
1 |
square 5×5 |
137.5 |
3.59 |
3 |
square 3×3 |
32.4 |
5.09 |
10 |
The hypothesis that a 15-pixel circle over-samples background water is confirmed:
shrinking to a 3×3 square roughly quadruples the pooled near-mouth SPM bounds.
But the outcome is not cleaner detection, it is a near-total loss of it. A 3×3
window is dominated by whichever single pixel is most turbid that specific day,
so on lower-turbidity days the resulting minimal_threshold can exceed the SPM
value everywhere in the scene. Ten of thirty days (33%) produced zero detected
plume pixels for both rivers simultaneously, confirmed in the run log
("No plume detected ... for any starting point") on days that were otherwise
100% cloud-free under the default circle. 5×5 and 7×7 recover substantially (3
and 1 zero-pixel days respectively), suggesting the useful range is a moderate
window rather than the tightest one.
6. Every other zone-preset parameter#
utils.py’s define_parameters() returns several further keys per zone, each of
which is a required field in the parameters block for a custom (non-zone)
config. maximal_threshold, minimal_threshold, and quantile_to_use are
covered above.
searching_strategies
: Maps each plume name to one of four named fan presets: northward_fan,
southward_fan, eastward_fan, westward_fan. Resolved into pixel-direction
vectors used by every step that walks outward from the river mouth (the
flood-fill BFS, the gradient transects, the post-fill cleanup passes). Wrong
preset: pointed back at the coastline instead of open water, the flood fill
and gradient transects hit land or the domain edge within a pixel or two of the
start point and detection collapses almost everywhere.
bathymetric_threshold
: A single zone-wide depth cutoff (metres) that strips any pixel shallower than
it from the final plume mask. It does not affect land/flood-fill
avoidance, which is always fixed at true sea level regardless of this setting.
Three of the four built-in zones use 0; GULF_OF_LION uses 20, since the
Rhône’s resuspension signal sits closer to shore. Too high: strips
genuinely shallow plume water, since river plumes are shallow-water phenomena
by nature. Too low: shallow-water artefacts that survived the earlier
resuspension filter are not caught by this final safety net.
starting_points
: One (lat, lon) coordinate per plume: the seed pixel for the flood fill and
gradient transects. Relocated automatically to the nearest finite neighbour if
cloud-covered on a given day (logged when it happens). Misconfigured: on
land, or well outside the true turbid signal, the flood fill either never
starts or starts below threshold and never grows.
core_of_the_plumes
: A second, trusted coordinate placed inside the expected plume body (not
necessarily at the mouth), used after a raw mask exists: to pick the correct
connected blob when the flood fill produces several disconnected shapes, as the
estuary centre for the resuspension-distance filter, and during the
dilate-and-merge step. The two coordinates are often near-identical, but can
differ when the river mouth sits very close to the coastline: starting_points
is then placed right at the coast, while core_of_the_plumes is offset
slightly offshore so shape-selection reliably finds the right blob (see the
README’s starting_points and core_of_the_plumes section for a worked
example from BAY_OF_SEINE). Misconfigured: if placed nearer a spurious blob
than the true plume, the shape-selection fallback (nearest blob by centroid
distance) keeps the wrong one outright.
lat_range_of_plume_area / lon_range_of_plume_area
: The bounding box used for data loading, polygon restriction, cloud-coverage
screening, and plot extent. Too wide: more file I/O and RAM than needed,
and irrelevant open-ocean turbidity can dilute the cloud-coverage/confidence
statistics or (if wide enough) risk cross-contamination between two rivers’
plumes in the same run. Too narrow: genuine offshore plume extent is
truncated at the box edge, indistinguishable in the output from the plume
genuinely ending there.
threshold_of_cloud_coverage_in_percentage
: The percentage of NaN water pixels inside the domain above which a scene is
skipped entirely (no detection attempted). All four built-in zones use 25.
Too high: mostly-cloudy scenes still get processed, producing low-confidence
guesses from a handful of visible pixels. Too low: routine, minor cloud
cover trips the skip on most days, producing spurious gaps in the time series.
maximal_bathymetric_for_zone_with_resuspension
: A per-plume depth cutoff (metres) used, together with distance from the core
and SPM level, to flag and remove coastal resuspension patches that are
shallow and turbid but not river-plume water. Too high: legitimately
shallow plume water becomes eligible for removal, undersizing the mask.
Too low: genuine resuspension patches survive into the final mask,
oversizing it with non-plume turbidity.
minimal_distance_from_estuary_for_zone_with_resuspension
: A per-plume distance (km) from core_of_the_plumes beyond which the criterion
above applies. Too high: near-shore resuspension close to (but not at) the
mouth survives and contaminates the mask. Too low: a genuine, nearby plume
lobe can be mistaken for resuspension and stripped.
max_steps_for_the_directions
: A per-plume integer bounding how far each fan transect extends, in pixels,
from the starting point, used both in the gradient search and in a post-fill
edge-cleanup pass. Zone presets range from 35-40 (Seine, Rhône plumes) to 100
(Gironde, Loire), reflecting how far offshore each plume genuinely disperses.
Too high: the transect reaches into unrelated open-ocean variability,
adding noise for little benefit. Too low: the transect never reaches the
true offshore edge of a widely-dispersing plume, biasing the derived threshold
toward the turbid core.
fixed_threshold
: A per-plume static SPM value (g m⁻³), the lowest-priority option in the
threshold resolution order (used only when dynamic_threshold: false and
neither spm_threshold nor global_threshold_quantile is set). Too high:
the flood fill never starts on days where peak SPM does not reach it,
systematic false negatives. Too low: the flood fill spreads into ambient
background water on every scene, since a static value cannot adapt to
day-to-day or seasonal variability.
river_mouth_to_exclude
: A dict of named secondary-channel coordinates (e.g. a tidal canal) whose own
turbid signal should be subtracted from the main plume mask. Misconfigured:
omitting a real secondary outflow lets its plume merge into and contaminate the
main one; a mis-placed or oversized entry can instead carve a real chunk out of
the main plume.
See also#
Workflow Vignette: a complete run from config file to output.
Using PlumeMasks.nc to Extract Daily SPM Values: recovering per-day SPM values from
PlumeMasks.nc.API Reference: full parameter reference for
define_parameters,find_SPM_threshold, and the rest of the detection pipeline.