mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Linearly bin fires so weak heat spots stay round
Dropping each detection whole into its nearest pixel snapped spots by up to half a pixel and left weak lone fires as squares or plus signs once thresholded. Share each detection's weight between the four nearest pixel centres instead (linear binning, Wand 1994): spots sit at the true position and blur round, at the same build cost.
This commit is contained in:
1 parent
5ab8ddb4ca
commit
ef5c034dc9
3 files changed
+52
-18
No files matched your search
@@ -2184,7 +2184,9 @@ detections are under 20 MW and stay dim red. Glow sprites and fire-card
|
||||
accents read that ramp; confidence and brightness do not affect the color.
|
||||
|
||||
At far zoom (camera above ~3,000 km) the layer drapes a Snap Map–style
|
||||
heatmap instead of individual markers: the shown detections are binned on a
|
||||
heatmap instead of individual markers: the shown detections are linearly binned
|
||||
(each detection's weight shared between the four nearest pixel centres, so spots
|
||||
sit at the true position and stay round) on a
|
||||
global 2048 × 1024 grid (≈20 km per pixel), each weighted by its FRP in megawatts (so heat is
|
||||
radiated power, and raising the FRP slider does not dim areas where strong fires
|
||||
burn), Gaussian-blurred by one pixel so each fire stays a distinct spot about
|
||||
|
||||
+29
-12
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Snap Map–style heat raster for the Active Fires far-zoom bands: detections
|
||||
* are binned on an equirectangular grid (row 0 = north, column 0 = 180°W),
|
||||
* blurred, normalized on a log scale and mapped through HEAT_PALETTE. Pure:
|
||||
* no Cesium, no DOM, so the whole pipeline is unit-tested in node.
|
||||
* are linearly binned on an equirectangular grid (row 0 = north, column
|
||||
* 0 = 180°W), blurred, normalized on a log scale and mapped through
|
||||
* HEAT_PALETTE. Pure: no Cesium, no DOM, so the whole pipeline is unit-tested
|
||||
* in node.
|
||||
*/
|
||||
|
||||
/** Below this normalized heat nothing is painted: empty land stays empty. */
|
||||
@@ -54,19 +55,35 @@ export const HEAT_MIN_WEIGHT_MW = 0.5;
|
||||
const defaultWeight = (fire) =>
|
||||
Math.max(HEAT_MIN_WEIGHT_MW, Number(fire.frp) || 0);
|
||||
|
||||
/**
|
||||
* Linear binning (Wand 1994): each detection's weight is shared between the
|
||||
* four pixel centres around it in proportion to closeness, instead of
|
||||
* dropping whole into one pixel. Spots then sit at the true position rather
|
||||
* than snapping by up to half a pixel (~10 km), and a lone weak fire blurs
|
||||
* into a round spot instead of a square or a plus sign. Longitude wraps;
|
||||
* rows beyond the poles fold back onto the edge row.
|
||||
*/
|
||||
export function binFires(fires, { width, height, weight = defaultWeight }) {
|
||||
const grid = new Float32Array(width * height);
|
||||
const row = (y) => Math.min(height - 1, Math.max(0, y)) * width;
|
||||
const col = (x) => ((x % width) + width) % width;
|
||||
for (const fire of fires) {
|
||||
if (!Number.isFinite(fire.lat) || !Number.isFinite(fire.lon)) continue;
|
||||
const x = Math.min(
|
||||
width - 1,
|
||||
Math.max(0, Math.floor(((fire.lon + 180) / 360) * width)),
|
||||
);
|
||||
const y = Math.min(
|
||||
height - 1,
|
||||
Math.max(0, Math.floor(((90 - fire.lat) / 180) * height)),
|
||||
);
|
||||
grid[y * width + x] += weight(fire);
|
||||
const fx = ((fire.lon + 180) / 360) * width - 0.5;
|
||||
const fy = ((90 - fire.lat) / 180) * height - 0.5;
|
||||
const x0 = Math.floor(fx);
|
||||
const y0 = Math.floor(fy);
|
||||
const tx = fx - x0;
|
||||
const ty = fy - y0;
|
||||
const w = weight(fire);
|
||||
const top = row(y0);
|
||||
const bottom = row(y0 + 1);
|
||||
const left = col(x0);
|
||||
const right = col(x0 + 1);
|
||||
grid[top + left] += w * (1 - tx) * (1 - ty);
|
||||
grid[top + right] += w * tx * (1 - ty);
|
||||
grid[bottom + left] += w * (1 - tx) * ty;
|
||||
grid[bottom + right] += w * tx * ty;
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
@@ -28,22 +28,37 @@ test('palette runs ember to white-hot and starts fully transparent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('binFires drops each detection into its equirectangular pixel', () => {
|
||||
const grid = binFires([fire(89.9, -179.9, 1), fire(-89.9, 179.9, 1)], {
|
||||
test('binFires puts a detection at a pixel centre wholly in that pixel', () => {
|
||||
// 4×2 grid: pixel centres at lon −135/−45/45/135, lat ±45.
|
||||
const grid = binFires([fire(45, -135, 1), fire(-45, 135, 1)], {
|
||||
width: 4,
|
||||
height: 2,
|
||||
});
|
||||
assert.equal(grid[0], 1, 'north-west corner');
|
||||
assert.equal(grid[7], 1, 'south-east corner');
|
||||
assert.equal(grid[0], 1, 'north-west pixel');
|
||||
assert.equal(grid[7], 1, 'south-east pixel');
|
||||
assert.equal(
|
||||
grid.reduce((a, b) => a + b, 0),
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test('binFires shares a detection between the pixels around it', () => {
|
||||
const between = binFires([fire(45, 0, 1)], { width: 4, height: 2 });
|
||||
assert.equal(between[1], 0.5, 'half to the pixel west of it');
|
||||
assert.equal(between[2], 0.5, 'half to the pixel east of it');
|
||||
const quarter = binFires([fire(45, -112.5, 1)], { width: 4, height: 2 });
|
||||
assert.equal(quarter[0], 0.75, 'closer centre takes more');
|
||||
assert.equal(quarter[1], 0.25);
|
||||
const wrapped = binFires([fire(45, 180, 1)], { width: 4, height: 2 });
|
||||
assert.equal(wrapped[3], 0.5, 'the antimeridian wraps');
|
||||
assert.equal(wrapped[0], 0.5);
|
||||
const polar = binFires([fire(90, -135, 1)], { width: 4, height: 2 });
|
||||
assert.equal(polar[0], 1, 'beyond the last row folds back onto it');
|
||||
});
|
||||
|
||||
test('binFires weights each detection by its radiative power', () => {
|
||||
const grid = binFires(
|
||||
[fire(0.1, 0.1, 250), fire(0.2, 0.2, 0), fire(0.3, 0.3, Number.NaN)],
|
||||
[fire(45, 45, 250), fire(45, 45, 0), fire(45, 45, Number.NaN)],
|
||||
{ width: 4, height: 2 },
|
||||
);
|
||||
assert.equal(grid[2], 250 + 2 * HEAT_MIN_WEIGHT_MW);
|
||||
|
||||
Reference in new issue
Block a user