Skip to content

Commit f316042

Browse files
authored
feat(tiler-sharp): support uint32 and uint8 source datasets for color-ramp (#3391)
### Motivation We are getting datasets that have other datatypes than float32 that need to be expanded into 4 band RGBA so basemaps can render them. A common type is a one band `uint8` which is generally a grey scale dataset which can be directly band expanded from `0` to `r:0, g:0, b:0, alpha: 255` and `255` to `r:255, g:255, b:255, alpha: 255` ### Modifications Added support for `uint8` and `uint32` with the tiler piplines TODO: it would be nicer to handle the datatypes more generically, to hopefully avoid giant switch case statements, we should in theory be able to trust that lerc gives us the correct typed array for instance. ### Verification Added unit tests and rendered it locally ![image](https://github.com/user-attachments/assets/f8788700-b9d3-470c-a7d7-9aabe8067f0f)
1 parent 46f8fb8 commit f316042

File tree

5 files changed

+148
-17
lines changed

5 files changed

+148
-17
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import assert from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
4+
import { Tiff } from '@basemaps/shared';
5+
import { CompositionTiff } from '@basemaps/tiler';
6+
7+
import { DecompressedInterleaved } from '../decompressor.js';
8+
import { PipelineColorRamp } from '../pipeline.color.ramp.js';
9+
10+
const FakeTiff = { images: [{ noData: -9999, resolution: [0.1, -0.1] }] } as unknown as Tiff;
11+
const FakeComp = { asset: FakeTiff, source: { x: 0, y: 0, imageId: 0 } } as CompositionTiff;
12+
13+
describe('pipeline.color-ramp', () => {
14+
it('should color-ramp a float32 DEM with default ramp', async () => {
15+
const bytes: DecompressedInterleaved = {
16+
pixels: new Float32Array([-9999, 0, 100]),
17+
depth: 'float32',
18+
channels: 1,
19+
width: 3,
20+
height: 1,
21+
};
22+
23+
const output = await PipelineColorRamp.process(FakeComp, bytes);
24+
25+
assert.equal(output.channels, 4);
26+
27+
assert.equal(String(output.pixels.slice(0, 4)), '0,0,0,0');
28+
assert.equal(String(output.pixels.slice(4, 8)), '167,205,228,255');
29+
});
30+
31+
it('should color-ramp a uint8', async () => {
32+
const bytes: DecompressedInterleaved = {
33+
pixels: new Uint8Array([0, 128, 255]),
34+
depth: 'uint8',
35+
channels: 1,
36+
width: 3,
37+
height: 1,
38+
};
39+
40+
const output = await PipelineColorRamp.process(FakeComp, bytes);
41+
42+
assert.equal(output.channels, 4);
43+
44+
assert.equal(String(output.pixels.slice(0, 4)), '0,0,0,255');
45+
assert.equal(String(output.pixels.slice(4, 8)), '128,128,128,255');
46+
assert.equal(String(output.pixels.slice(8, 12)), '255,255,255,255');
47+
});
48+
49+
it('should color-ramp a uint32', async () => {
50+
const bytes: DecompressedInterleaved = {
51+
pixels: new Uint32Array([0, 2 ** 31, 2 ** 32 - 1]),
52+
depth: 'uint32',
53+
channels: 1,
54+
width: 3,
55+
height: 1,
56+
};
57+
58+
const output = await PipelineColorRamp.process(FakeComp, bytes);
59+
60+
assert.equal(output.channels, 4);
61+
62+
assert.equal(String(output.pixels.slice(0, 4)), '0,0,0,255');
63+
assert.equal(String(output.pixels.slice(4, 8)), '128,128,128,255');
64+
assert.equal(String(output.pixels.slice(8, 12)), '255,255,255,255');
65+
});
66+
});

packages/tiler-sharp/src/pipeline/decompressor.lerc.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,41 @@ export const LercDecompressor: Decompressor = {
99
await Lerc.load();
1010
const bytes = Lerc.decode(tile);
1111

12-
if (bytes.pixelType !== 'F32') {
13-
throw new Error(`Lerc: Invalid output pixelType:${bytes.pixelType} from:${source.source.url.href}`);
14-
}
1512
if (bytes.depthCount !== 1) {
1613
throw new Error(`Lerc: Invalid output depthCount:${bytes.depthCount} from:${source.source.url.href}`);
1714
}
1815
if (bytes.pixels.length !== 1) {
1916
throw new Error(`Lerc: Invalid output bandCount:${bytes.pixels.length} from:${source.source.url.href}`);
2017
}
2118

22-
return {
23-
pixels: bytes.pixels[0] as Float32Array,
24-
width: bytes.width,
25-
height: bytes.height,
26-
channels: 1,
27-
depth: 'float32',
28-
};
19+
switch (bytes.pixelType) {
20+
case 'F32':
21+
return {
22+
pixels: bytes.pixels[0] as Float32Array,
23+
width: bytes.width,
24+
height: bytes.height,
25+
channels: 1,
26+
depth: 'float32',
27+
};
28+
case 'U32':
29+
return {
30+
pixels: bytes.pixels[0] as Uint32Array,
31+
width: bytes.width,
32+
height: bytes.height,
33+
channels: 1,
34+
depth: 'uint32',
35+
};
36+
case 'U8':
37+
return {
38+
pixels: bytes.pixels[0] as Uint8Array,
39+
width: bytes.width,
40+
height: bytes.height,
41+
channels: 1,
42+
depth: 'uint8',
43+
};
44+
}
45+
46+
throw new Error(`Lerc: Invalid output pixelType:${bytes.pixelType} from:${source.source.url.href}`);
2947
},
3048
};
3149

packages/tiler-sharp/src/pipeline/decompressor.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { CompositionTiff } from '@basemaps/tiler';
22
import { Tiff } from '@cogeotiff/core';
33

4+
export interface DecompressedInterleavedUint32 {
5+
pixels: Uint32Array;
6+
depth: 'uint32';
7+
channels: number;
8+
width: number;
9+
height: number;
10+
}
11+
412
export interface DecompressedInterleavedFloat {
513
pixels: Float32Array;
614
depth: 'float32';
@@ -18,7 +26,10 @@ export interface DecompressedInterleavedUint8 {
1826
}
1927

2028
// One buffer containing all bands
21-
export type DecompressedInterleaved = DecompressedInterleavedFloat | DecompressedInterleavedUint8;
29+
export type DecompressedInterleaved =
30+
| DecompressedInterleavedFloat
31+
| DecompressedInterleavedUint8
32+
| DecompressedInterleavedUint32;
2233

2334
export interface Decompressor {
2435
type: 'image/webp' | 'application/lerc';

packages/tiler-sharp/src/pipeline/pipeline.color.ramp.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ export class ColorRamp {
4242
}
4343
}
4444

45-
export const ramp = new ColorRamp(DefaultColorRamp);
45+
export const Ramps: Record<DecompressedInterleaved['depth'], ColorRamp> = {
46+
float32: new ColorRamp(DefaultColorRamp),
47+
uint8: new ColorRamp(`0 0 0 0 255\n255 255 255 255 255`),
48+
uint32: new ColorRamp(`0 0 0 0 255\n${2 ** 32 - 1} 255 255 255 255`),
49+
};
4650

4751
export const PipelineColorRamp: Pipeline = {
4852
type: 'color-ramp',
@@ -56,6 +60,8 @@ export const PipelineColorRamp: Pipeline = {
5660
height: data.height,
5761
};
5862

63+
const ramp = Ramps[data.depth];
64+
5965
const size = data.width * data.height;
6066
const noData = comp.asset.images[0].noData;
6167

packages/tiler-sharp/src/pipeline/pipeline.resize.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export function cropResize(
2424

2525
// Currently very limited supported input parameters
2626
if (data.channels !== 1) throw new Error('Unable to crop-resize more than one channel got:' + data.channels);
27-
if (data.depth !== 'float32') throw new Error('Unable to crop-resize other than float32 got:' + data.depth);
2827

2928
// Area of the source data that needs to be resampled
3029
const source = { x: 0, y: 0, width: data.width, height: data.height };
@@ -93,7 +92,8 @@ function resizeNearest(
9392
const invScale = 1 / target.scale;
9493

9594
// Resample the input tile into the output tile using a nearest neighbor approach
96-
const outputBuffer = new Float32Array(target.width * target.height);
95+
const ret = getOutputBuffer(data, target);
96+
const outputBuffer = ret.pixels;
9797
for (let y = 0; y < target.height; y++) {
9898
let sourceY = Math.round((y + 0.5) * invScale + source.y);
9999
if (sourceY > maxHeight) sourceY = maxHeight;
@@ -106,7 +106,36 @@ function resizeNearest(
106106
}
107107
}
108108

109-
return { pixels: outputBuffer, width: target.width, height: target.height, depth: 'float32', channels: 1 };
109+
return ret;
110+
}
111+
112+
function getOutputBuffer(source: DecompressedInterleaved, target: Size): DecompressedInterleaved {
113+
switch (source.depth) {
114+
case 'uint8':
115+
return {
116+
pixels: new Uint8Array(target.width * target.height),
117+
width: target.width,
118+
height: target.height,
119+
depth: source.depth,
120+
channels: 1,
121+
};
122+
case 'float32':
123+
return {
124+
pixels: new Float32Array(target.width * target.height),
125+
width: target.width,
126+
height: target.height,
127+
depth: source.depth,
128+
channels: 1,
129+
};
130+
case 'uint32':
131+
return {
132+
pixels: new Uint32Array(target.width * target.height),
133+
width: target.width,
134+
height: target.height,
135+
depth: source.depth,
136+
channels: 1,
137+
};
138+
}
110139
}
111140

112141
function resizeBilinear(
@@ -120,7 +149,8 @@ function resizeBilinear(
120149

121150
const maxWidth = Math.min(comp.source.width, data.width) - 2;
122151
const maxHeight = Math.min(comp.source.height, data.height) - 2;
123-
const outputBuffer = new Float32Array(target.width * target.height);
152+
const ret = getOutputBuffer(data, target);
153+
const outputBuffer = ret.pixels;
124154
for (let y = 0; y < target.height; y++) {
125155
const sourceY = Math.min((y + 0.5) * invScale + source.y, maxHeight);
126156
const minY = Math.floor(sourceY);
@@ -167,5 +197,5 @@ function resizeBilinear(
167197
}
168198
}
169199

170-
return { pixels: outputBuffer, width: target.width, height: target.height, depth: 'float32', channels: 1 };
200+
return ret;
171201
}

0 commit comments

Comments
 (0)