mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad369eb0de | ||
|
|
0b5ae6386e |
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
- Halve the time the satellite cloud image takes to prepare on the 3D Tiles
|
||||||
|
map by applying its transparency curve from a lookup table, in place
|
||||||
|
(mazy1998, #1713).
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
export const INFRARED_ALPHA_T0 = 0.4;
|
export const INFRARED_ALPHA_T0 = 0.4;
|
||||||
export const INFRARED_ALPHA_T1 = 0.7;
|
export const INFRARED_ALPHA_T1 = 0.7;
|
||||||
|
|
||||||
/** Return display pixels without changing RGB or the source RGBA bytes.
|
function infraredAmount(strength) {
|
||||||
|
return strength === 'full'
|
||||||
|
? 0
|
||||||
|
: strength === 'filtered' || !Number.isFinite(strength)
|
||||||
|
? 1
|
||||||
|
: Math.max(0, Math.min(100, strength)) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scale each pixel's alpha in place; RGB is left as it is. The scale depends
|
||||||
|
* only on the brightest channel, so it is worked out once for each of the 256
|
||||||
|
* byte values instead of once per pixel: a 4096×2048 image has 8.4 million
|
||||||
|
* pixels and the power curve per pixel held the page for over 100 ms. The
|
||||||
|
* table holds the same numbers the per-pixel formula produced, so the output
|
||||||
|
* bytes are unchanged.
|
||||||
* `strength` is 0–100: zero is the full image and 100 is the cloud key.
|
* `strength` is 0–100: zero is the full image and 100 is the cloud key.
|
||||||
*/
|
*/
|
||||||
export function infraredAlpha(rgba, strength = 'filtered') {
|
export function applyInfraredAlpha(pixels, strength = 'filtered') {
|
||||||
const pixels = new Uint8ClampedArray(rgba);
|
const amount = infraredAmount(strength);
|
||||||
const amount =
|
|
||||||
strength === 'full'
|
|
||||||
? 0
|
|
||||||
: strength === 'filtered' || !Number.isFinite(strength)
|
|
||||||
? 1
|
|
||||||
: Math.max(0, Math.min(100, strength)) / 100;
|
|
||||||
if (amount === 0) return pixels;
|
if (amount === 0) return pixels;
|
||||||
for (let i = 0; i < pixels.length; i += 4) {
|
const scale = new Float64Array(256);
|
||||||
|
for (let value = 0; value < 256; value++) {
|
||||||
// Match Cesium's czm_srgbToLinear conversion in draped imagery.
|
// Match Cesium's czm_srgbToLinear conversion in draped imagery.
|
||||||
const linearMax =
|
const linearMax = (value / 255) ** 2.2;
|
||||||
(Math.max(rgba[i], rgba[i + 1], rgba[i + 2]) / 255) ** 2.2;
|
|
||||||
const t = Math.max(
|
const t = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(
|
Math.min(
|
||||||
@@ -26,7 +33,21 @@ export function infraredAlpha(rgba, strength = 'filtered') {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const filtered = t * t * (3 - 2 * t);
|
const filtered = t * t * (3 - 2 * t);
|
||||||
pixels[i + 3] = rgba[i + 3] * (1 - amount + filtered * amount);
|
scale[value] = 1 - amount + filtered * amount;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pixels.length; i += 4) {
|
||||||
|
const r = pixels[i];
|
||||||
|
const g = pixels[i + 1];
|
||||||
|
const b = pixels[i + 2];
|
||||||
|
const max = r > g ? (r > b ? r : b) : g > b ? g : b;
|
||||||
|
pixels[i + 3] = pixels[i + 3] * scale[max];
|
||||||
}
|
}
|
||||||
return pixels;
|
return pixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return display pixels without changing RGB or the source RGBA bytes.
|
||||||
|
* `strength` is 0–100: zero is the full image and 100 is the cloud key.
|
||||||
|
*/
|
||||||
|
export function infraredAlpha(rgba, strength = 'filtered') {
|
||||||
|
return applyInfraredAlpha(new Uint8ClampedArray(rgba), strength);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
|
applyInfraredAlpha,
|
||||||
infraredAlpha,
|
infraredAlpha,
|
||||||
INFRARED_ALPHA_T0,
|
INFRARED_ALPHA_T0,
|
||||||
INFRARED_ALPHA_T1,
|
INFRARED_ALPHA_T1,
|
||||||
@@ -61,3 +62,43 @@ test('full infrared preserves every channel including partial and zero alpha', (
|
|||||||
]);
|
]);
|
||||||
assert.deepEqual(infraredAlpha(pixels, 'full'), pixels);
|
assert.deepEqual(infraredAlpha(pixels, 'full'), pixels);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the lookup table gives the per-pixel curve byte for byte, in place', () => {
|
||||||
|
// The transfer as it was written per pixel, before the table.
|
||||||
|
const perPixel = (rgba, amount) => {
|
||||||
|
const out = new Uint8ClampedArray(rgba);
|
||||||
|
for (let i = 0; i < out.length; i += 4) {
|
||||||
|
const linearMax =
|
||||||
|
(Math.max(rgba[i], rgba[i + 1], rgba[i + 2]) / 255) ** 2.2;
|
||||||
|
const t = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
1,
|
||||||
|
(linearMax - INFRARED_ALPHA_T0) /
|
||||||
|
(INFRARED_ALPHA_T1 - INFRARED_ALPHA_T0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
out[i + 3] = rgba[i + 3] * (1 - amount + t * t * (3 - 2 * t) * amount);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
// Every brightest-channel value in each channel, against a spread of alphas.
|
||||||
|
const bytes = [];
|
||||||
|
for (let value = 0; value < 256; value++)
|
||||||
|
for (const alpha of [0, 1, 37, 128, 200, 254, 255]) {
|
||||||
|
bytes.push(value, 0, 0, alpha);
|
||||||
|
bytes.push(value >> 1, value, 0, alpha);
|
||||||
|
bytes.push(0, value >> 2, value, alpha);
|
||||||
|
}
|
||||||
|
const source = new Uint8ClampedArray(bytes);
|
||||||
|
for (const strength of [1, 33, 50, 99, 100, 'filtered', Number.NaN]) {
|
||||||
|
const amount = Number.isFinite(strength) ? strength / 100 : 1;
|
||||||
|
const pixels = source.slice();
|
||||||
|
assert.equal(applyInfraredAlpha(pixels, strength), pixels);
|
||||||
|
assert.deepEqual(pixels, perPixel(source, amount), `strength ${strength}`);
|
||||||
|
}
|
||||||
|
for (const strength of [0, 'full']) {
|
||||||
|
const pixels = source.slice();
|
||||||
|
assert.deepEqual(applyInfraredAlpha(pixels, strength), source);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
drawWeatherPixels,
|
drawWeatherPixels,
|
||||||
closeWeatherPixels,
|
closeWeatherPixels,
|
||||||
} from './source.js';
|
} from './source.js';
|
||||||
import { infraredAlpha } from './infraredAlpha.js';
|
import { applyInfraredAlpha } from './infraredAlpha.js';
|
||||||
|
|
||||||
export const MAX_MOSAIC_BYTES = 4 * 1024 * 1024;
|
export const MAX_MOSAIC_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -31,7 +31,8 @@ export function processInfraredImage(
|
|||||||
|
|
||||||
function applyInfraredTransfer(context, canvas, mode) {
|
function applyInfraredTransfer(context, canvas, mode) {
|
||||||
const pixels = context.getImageData(0, 0, canvas.width, canvas.height);
|
const pixels = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
pixels.data.set(infraredAlpha(pixels.data, mode));
|
// In place: a copy of a 4096×2048 image is 33 MB, made and written back.
|
||||||
|
applyInfraredAlpha(pixels.data, mode);
|
||||||
context.putImageData(pixels, 0, 0);
|
context.putImageData(pixels, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,28 @@ function canvas() {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('the transfer is applied to the canvas pixels in place, with no copy written back', () => {
|
||||||
|
let read = null;
|
||||||
|
const output = {};
|
||||||
|
output.getContext = () => ({
|
||||||
|
drawImage() {},
|
||||||
|
getImageData: () => {
|
||||||
|
read = { data: new Uint8ClampedArray([194, 0, 0, 200]) };
|
||||||
|
// A second full-size buffer copied over the first is what this guards.
|
||||||
|
read.data.set = () => {
|
||||||
|
throw new Error('the canvas pixels were overwritten from a copy');
|
||||||
|
};
|
||||||
|
return read;
|
||||||
|
},
|
||||||
|
putImageData: (pixels) => {
|
||||||
|
output.pixels = pixels;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
processInfraredImage({ width: 1, height: 1 }, 'filtered', () => output);
|
||||||
|
assert.equal(output.pixels, read);
|
||||||
|
assert.deepEqual([...read.data], [194, 0, 0, 98]);
|
||||||
|
});
|
||||||
|
|
||||||
test('decoded pixels are processed once on a same-sized canvas', () => {
|
test('decoded pixels are processed once on a same-sized canvas', () => {
|
||||||
const image = { width: 1, height: 1 };
|
const image = { width: 1, height: 1 };
|
||||||
const output = processInfraredImage(image, 'filtered', canvas);
|
const output = processInfraredImage(image, 'filtered', canvas);
|
||||||
|
|||||||
Reference in new issue
Block a user