Skip to content
Draft
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
18 changes: 15 additions & 3 deletions packages/lambda-tiler/src/cli/render.tile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfigImagery, ConfigProviderMemory, ConfigTileSetRaster } from '@basemaps/config';
import { base58, ConfigImagery, ConfigProviderMemory, ConfigTileSetRaster, isBase58 } from '@basemaps/config';
import { initConfigFromUrls } from '@basemaps/config-loader';
import { Tile, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
import { fsa, FsaLocalCache, LogConfig, setDefaultConfig } from '@basemaps/shared';
Expand All @@ -14,21 +14,33 @@ const pipeline = process.argv[4];

if (sourceRaw == null || tilePath == null) {
// eslint-disable-next-line no-console
console.log(`Usage: render-tile <source-imagery> tilePath
console.log(`Usage: render-tile <source-imagery|source-config> tilePath
eg:

# render tile: {z:3, x:2, y:3} as a webmercator png
render-tile /home/data/nz 3/2/3.png

# Render a false-color pipeline
render-tile /home/data/nz 3/2/3.png false-color


render-tile s3://linz-basemaps/config.json.gz 3/2/3.png false-color
`);
process.exit(1);
}

fsa.middleware.push(FsaLocalCache);

const source = fsa.toUrl(sourceRaw);
function decodePath(sourcePath: string): URL {
if (isBase58(sourcePath)) {
try {
const text = Buffer.from(base58.decode(sourcePath)).toString();
if (text.startsWith('s3:')) return fsa.toUrl(text);
} catch {}
}
return fsa.toUrl(sourcePath);
}
const source = decodePath(sourceRaw);
const tile = fromPath(tilePath);
let tileMatrix: TileMatrixSet | null = null;

Expand Down
113 changes: 88 additions & 25 deletions packages/tiler-sharp/src/pipeline/pipeline.resize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ export function cropResize(
target.scale = comp.resize.scale;
}

const invScale = 1 / target.scale;
const invScaleX = 1 / (comp.resize?.scaleX ?? target.scale);
const invScaleY = 1 / (comp.resize?.scaleY ?? target.scale);

if (comp.crop) {
source.x = Math.round(comp.crop.x * invScale);
source.y = Math.round(comp.crop.y * invScale);
source.width = Math.round(comp.crop.width * invScale);
source.height = Math.round(comp.crop.height * invScale);
source.x = comp.crop.x * invScaleX;
source.y = comp.crop.y * invScaleY;
source.width = comp.crop.width * invScaleX;
source.height = comp.crop.height * invScaleY;

target.width = comp.crop.width;
target.height = comp.crop.height;
Expand Down Expand Up @@ -86,19 +87,20 @@ function resizeNearest(
): DecompressedInterleaved {
const maxWidth = Math.min(comp.source.width, data.width) - 1;
const maxHeight = Math.min(comp.source.height, data.height) - 1;
const invScale = 1 / target.scale;
const invScaleX = 1 / (comp.resize?.scaleX ?? target.scale);
const invScaleY = 1 / (comp.resize?.scaleY ?? target.scale);
const ret = getOutputBuffer(data, target);
const outputBuffer = ret.pixels;

// FIXME! LERC is band interleaved rather than pixel interleaved
// const channelOffset = data.width * data.height * 1;
// const resizeSource = new Set();
for (let y = 0; y < target.height; y++) {
let sourceY = Math.round((y + 0.5) * invScale + source.y);
let sourceY = Math.floor((y + 0.5) * invScaleY + source.y);
if (sourceY > maxHeight) sourceY = maxHeight;

for (let x = 0; x < target.width; x++) {
let sourceX = Math.round((x + 0.5) * invScale + source.x);
let sourceX = Math.floor((x + 0.5) * invScaleX + source.x);
if (sourceX > maxWidth) sourceX = maxWidth;

const targetOffset = (y * target.width + x) * ret.channels;
Expand Down Expand Up @@ -157,7 +159,11 @@ export function resizeBilinear(
target: Size & { scale: number },
noData?: number | null,
): DecompressedInterleaved {
const invScale = 1 / target.scale;
const invScaleX = 1 / (comp.resize?.scaleX ?? target.scale);
const invScaleY = 1 / (comp.resize?.scaleY ?? target.scale);
if (invScaleX > 2 || invScaleY > 2) {
return resizeArea(data, comp, source, target, noData);
}

const maxWidth = Math.min(comp.source.width, data.width) - 2;
const maxHeight = Math.min(comp.source.height, data.height) - 2;
Expand All @@ -172,36 +178,26 @@ export function resizeBilinear(
const needsRounding = !data.depth.startsWith('float');

for (let y = 0; y < target.height; y++) {
const sourceY = Math.min((y + 0.5) * invScale + source.y, maxHeight);
const sourceY = Math.min((y + 0.5) * invScaleY + source.y, maxHeight);
const minY = Math.floor(sourceY);
const maxY = minY + 1;

for (let x = 0; x < target.width; x++) {
const sourceX = Math.min((x + 0.5) * invScale + source.x, maxWidth);
const sourceX = Math.min((x + 0.5) * invScaleX + source.x, maxWidth);
const minX = Math.floor(sourceX);
const maxX = minX + 1;

for (let i = 0; i < ret.channels; i++) {
const outPx = (y * target.width + x) * data.channels + i;

// Bilinear interpolation for upscaling
const minXMinY = data.pixels[(minY * data.width + minX) * data.channels + i];
if (minXMinY === noData) {
outputBuffer[outPx] = noData;
continue;
}
const maxXMinY = data.pixels[(minY * data.width + maxX) * data.channels + i];
if (maxXMinY === noData) {
outputBuffer[outPx] = noData;
continue;
}
const minXMaxY = data.pixels[(maxY * data.width + minX) * data.channels + i];
if (minXMaxY === noData) {
outputBuffer[outPx] = noData;
continue;
}
const maxXMaxY = data.pixels[(maxY * data.width + maxX) * data.channels + i];
if (maxXMaxY === noData) {
outputBuffer[outPx] = noData;

if (minXMinY === noData || maxXMinY === noData || minXMaxY === noData || maxXMaxY === noData) {
outputBuffer[outPx] = noData ?? 0;
continue;
}

Expand All @@ -213,7 +209,74 @@ export function resizeBilinear(
const weightD = xDiff * yDiff;

const pixel = minXMinY * weightA + maxXMinY * weightB + minXMaxY * weightC + maxXMaxY * weightD;
outputBuffer[outPx] = needsRounding ? Math.round(pixel) : pixel;
}
}
}

return ret;
}

export function resizeArea(
data: DecompressedInterleaved,
comp: CompositionTiff,
source: BoundingBox,
target: Size & { scale: number },
noData?: number | null,
): DecompressedInterleaved {
const invScaleX = 1 / (comp.resize?.scaleX ?? target.scale);
const invScaleY = 1 / (comp.resize?.scaleY ?? target.scale);
const ret = getOutputBuffer(data, target);
const outputBuffer = ret.pixels;
const needsRounding = !data.depth.startsWith('float');

const fullArea = invScaleX * invScaleY;

for (let y = 0; y < target.height; y++) {
const startY = y * invScaleY + source.y;
const endY = (y + 1) * invScaleY + source.y;

for (let x = 0; x < target.width; x++) {
const startX = x * invScaleX + source.x;
const endX = (x + 1) * invScaleX + source.x;

for (let i = 0; i < ret.channels; i++) {
const outPx = (y * target.width + x) * data.channels + i;

let totalValue = 0;
let totalWeight = 0;

const minSY = Math.max(0, Math.floor(startY));
const maxSY = Math.min(data.height - 1, Math.ceil(endY) - 1);
const minSX = Math.max(0, Math.floor(startX));
const maxSX = Math.min(data.width - 1, Math.ceil(endX) - 1);

for (let sy = minSY; sy <= maxSY; sy++) {
const yWeight = Math.min(sy + 1, endY) - Math.max(sy, startY);
for (let sx = minSX; sx <= maxSX; sx++) {
const xWeight = Math.min(sx + 1, endX) - Math.max(sx, startX);
const weight = xWeight * yWeight;

const val = data.pixels[(sy * data.width + sx) * data.channels + i];
if (val !== noData) {
totalValue += val * weight;
totalWeight += weight;
}
}
}

if (totalWeight === 0) {
outputBuffer[outPx] = noData ?? 0;
continue;
}

// Determine if we are at an image edge (not just a tile edge)
// If we are at an image edge, we want to fade to transparent (normalize by fullArea)
// If we are at a tile edge but still within the image, we want to extrapolate (normalize by totalWeight)
// const isAtImageX = comp.source.x + startX < 0 || comp.source.x + endX > comp.source.width;
// const isAtImageY = comp.source.y + startY < 0 || comp.source.y + endY > comp.source.height;

const pixel = totalValue / fullArea; // (isAtImageX || isAtImageY ? fullArea : totalWeight);
outputBuffer[outPx] = needsRounding ? Math.round(pixel) : pixel;
}
}
Expand Down
Loading