Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions examples/vermont-cog-comparison/src/tile-loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export type TileTextureData = {
/**
* Tile loader for 3- or 4-band pixel-interleaved COGs.
*
* Fetches the source tile, asserts pixel-interleaved layout, expands
* 3-band data to RGBA via `addAlphaChannel` (WebGL2 has no rgb-only 8-bit
* texture format), and uploads as `rgba8unorm`. For 4-band data the
* source-side alpha (NIR) is overridden by the shader pipeline (see
* Fetches the source tile and expands 3-band data to RGBA via
* `addAlphaChannel` (WebGL2 has no rgb-only 8-bit texture format), which also
* interleaves a band-separate tile, then uploads as `rgba8unorm`. For 4-band
* data the source-side alpha (NIR) is overridden by the shader pipeline (see
* `SetAlpha1`).
*/
export async function getTileDataRGBA(
Expand All @@ -28,11 +28,7 @@ export async function getTileDataRGBA(
): Promise<TileTextureData> {
const { device, x, y, signal } = options;
const tile = await image.fetchTile(x, y, { signal, boundless: false });
const array = addAlphaChannel(tile.array);
if (array.layout === "band-separate") {
throw new Error("Vermont COGs are expected to be pixel-interleaved");
}
const { width, height, data } = array;
const { width, height, data } = addAlphaChannel(tile.array);
const texture = device.createTexture({
data,
format: "rgba8unorm",
Expand Down
16 changes: 8 additions & 8 deletions packages/deck.gl-geotiff/src/geotiff/geotiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ import type {
ConcurrencyLimiter,
Priority,
RasterArray,
RasterArrayPixelInterleaved,
} from "@developmentseed/geotiff";
import { GeoTIFF } from "@developmentseed/geotiff";
import { GeoTIFF, toPixelInterleaved } from "@developmentseed/geotiff";
import type { Converter } from "proj4";

/**
* Add an alpha channel to an RGB image array.
*
* Only supports input arrays with 3 (RGB) or 4 (RGBA) channels. If the input is
* already RGBA, it is returned unchanged.
* already RGBA, it is returned unchanged. Band-separate input is interleaved
* first, so the result is always pixel-interleaved.
*/
export function addAlphaChannel(rgbImage: RasterArray): RasterArray {
export function addAlphaChannel(
rgbImage: RasterArray,
): RasterArrayPixelInterleaved {
const { height, width } = rgbImage;

if (rgbImage.layout === "band-separate") {
// This should be pretty easy to do by just returning an additional array of
// 255s
// But not sure if we'll want to do that, because it's fine to upload 3
// separate textures.
throw new Error("Band-separate images not yet implemented.");
return addAlphaChannel(toPixelInterleaved(rgbImage));
}

if (rgbImage.data.length === height * width * 4) {
Expand Down
16 changes: 11 additions & 5 deletions packages/deck.gl-geotiff/src/geotiff/render-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
WhiteIsZero,
} from "@developmentseed/deck.gl-raster/gpu-modules";
import type { GeoTIFF, Overview } from "@developmentseed/geotiff";
import { parseColormap } from "@developmentseed/geotiff";
import { parseColormap, toPixelInterleaved } from "@developmentseed/geotiff";
import type { Device, SamplerProps, Texture } from "@luma.gl/core";
import type { GetTileDataOptions } from "../cog-layer.js";
import { addAlphaChannel } from "./geotiff.js";
Expand Down Expand Up @@ -161,16 +161,22 @@ function createUnormPipeline(

let numSamples = samplesPerPixel;

// A PlanarConfiguration=2 image decodes to one array per band, which can't be
// uploaded as a single texture. Interleave it so the rest of this function -
// and the shader pipeline built above - sees the same thing it would for a
// pixel-interleaved image. Uploading one texture per band and sampling a
// sampler2DArray would avoid this copy, but needs the render pipeline to know
// how many textures a tile has; see #159.
if (array.layout === "band-separate") {
array = toPixelInterleaved(array);
}

if (samplesPerPixel === 3) {
// WebGL2 doesn't have an RGB-only texture format; it requires RGBA.
array = addAlphaChannel(array);
numSamples = 4;
}

if (array.layout === "band-separate") {
throw new Error("Band-separate images not yet implemented.");
}

const textureFormat = inferTextureFormat(
// Add one sample for added alpha channel
numSamples,
Expand Down
70 changes: 70 additions & 0 deletions packages/deck.gl-geotiff/tests/geotiff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type {
RasterArray,
RasterArrayBandSeparate,
} from "@developmentseed/geotiff";
import { describe, expect, it } from "vitest";
import { addAlphaChannel } from "../src/geotiff/geotiff.js";

/** A band-separate raster holding one array per band, two pixels wide. */
function bandSeparate(bands: number[][]): RasterArrayBandSeparate {
return {
layout: "band-separate",
count: bands.length,
width: bands[0]!.length,
height: 1,
mask: null,
bands: bands.map((band) => new Uint8Array(band)),
} as RasterArrayBandSeparate;
}

describe("addAlphaChannel", () => {
it("pads a pixel-interleaved RGB array", () => {
const rgb = {
layout: "pixel-interleaved",
count: 3,
width: 2,
height: 1,
mask: null,
data: new Uint8Array([1, 2, 3, 4, 5, 6]),
} as unknown as RasterArray;

const rgba = addAlphaChannel(rgb);

expect(rgba.count).toEqual(4);
expect(Array.from((rgba as any).data)).toEqual([
1, 2, 3, 255, 4, 5, 6, 255,
]);
});

it("interleaves a band-separate RGB array before padding it", () => {
const rgba = addAlphaChannel(
bandSeparate([
[1, 4],
[2, 5],
[3, 6],
]),
);

expect(rgba.layout).toEqual("pixel-interleaved");
expect(rgba.count).toEqual(4);
expect(Array.from((rgba as any).data)).toEqual([
1, 2, 3, 255, 4, 5, 6, 255,
]);
});

// Already four channels, so there is nothing to pad - only to interleave
it("interleaves a band-separate RGBA array unchanged", () => {
const rgba = addAlphaChannel(
bandSeparate([
[1, 5],
[2, 6],
[3, 7],
[0, 255],
]),
);

expect(rgba.layout).toEqual("pixel-interleaved");
expect(rgba.count).toEqual(4);
expect(Array.from((rgba as any).data)).toEqual([1, 2, 3, 0, 5, 6, 7, 255]);
});
});
66 changes: 66 additions & 0 deletions packages/deck.gl-geotiff/tests/render-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,69 @@ describe("RGB with mask", async () => {
expect(renderPipeline[1]?.module.name).toEqual("mask-texture");
});
});

describe("band-separate tiles", () => {
/**
* A PlanarConfiguration=2 tile, as `fetchTile` decodes one: a separate array
* per band. Handed to `getTileData` directly rather than read from a fixture,
* because the only band-separate fixture is int8, which
* `inferRenderPipeline` does not build a pipeline for.
*/
function bandSeparateTile(bandCount: number) {
return {
array: {
layout: "band-separate" as const,
count: bandCount,
width: 2,
height: 1,
mask: null,
// Band b holds [b, b + bandCount], so an interleaved result reads
// 0, 1, .., bandCount - 1, bandCount, ..
bands: Array.from(
{ length: bandCount },
(_, b) => new Uint8Array([b, b + bandCount]),
),
},
};
}

async function _getTileData(geotiff: GeoTIFF, bandCount: number) {
const { getTileData } = inferRenderPipeline(geotiff, MOCK_DEVICE as any);
const image = { fetchTile: async () => bandSeparateTile(bandCount) };
return await getTileData(
image as any,
{
device: MOCK_DEVICE,
x: 0,
y: 0,
} as any,
);
}

it("interleaves an RGBA tile into a single texture", async () => {
const geotiff = await loadGeoTIFF("cog_uint8_rgba", "rasterio");

const { texture, width, height } = await _getTileData(geotiff, 4);

expect((texture as any).format).toEqual("rgba8unorm");
expect(Array.from((texture as any).data)).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
expect(width).toEqual(2);
expect(height).toEqual(1);
});

// WebGL2 has no RGB-only texture format, so a 3-band tile is interleaved and
// then padded to RGBA, same as a pixel-interleaved one
it("interleaves and pads an RGB tile", async () => {
const geotiff = await loadGeoTIFF(
"uint8_rgb_deflate_block64_cog",
"rasterio",
);

const { texture } = await _getTileData(geotiff, 3);

expect((texture as any).format).toEqual("rgba8unorm");
expect(Array.from((texture as any).data)).toEqual([
0, 1, 2, 255, 3, 4, 5, 255,
]);
});
});
7 changes: 7 additions & 0 deletions packages/geotiff/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
export type {
PackBandsToRGBAOptions,
RasterArray,
RasterArrayBandSeparate,
RasterArrayBase,
RasterArrayPixelInterleaved,
RasterTypedArray,
} from "./array.js";
export {
packBandsToRGBA,
reorderBands,
toBandSeparate,
toPixelInterleaved,
} from "./array.js";
export type { AssembleTilesOptions } from "./assemble.js";
export { assembleTiles } from "./assemble.js";
export { parseColormap } from "./colormap.js";
Expand Down
Loading