UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

235 lines 9.64 kB
const FIXED_FRAC_BITS = 14; /** * Filter input value given a filter window. * @param x - input * @param a - filter window * @returns - filtered value */ function filterValue(x, a) { if (x <= -a || x >= a) return 0; if (x === 0) return 0; // appears to do nothing? // if ( x > -1.19209290e-07 && x < 1.19209290e-07 ) return 1 const xPi = x * Math.PI; return ((Math.sin(xPi) / xPi) * Math.sin(xPi / a)) / (xPi / a); } /** * Convert value to fixed point * @param value - input * @returns - fixed point */ function toFixedPoint(value) { return Math.round(value * ((1 << FIXED_FRAC_BITS) - 1)); } /** * Create a Lanczos filter * @param srcSize - source image size * @param destSize - destination image size * @param scale - scale factor * @param offset - offset to apply * @param use2 - use 2nd lanczos filter instead of 3rd * @returns - filter */ function filters(srcSize, destSize, scale, offset, use2) { const a = use2 ? 2 : 3; const scaleInverted = 1 / scale; const scaleClamped = Math.min(1, scale); // For upscale // Filter window (averaging interval), scaled to src image const srcWindow = a / scaleClamped; const maxFilterElementSize = Math.floor((srcWindow + 1) * 2); const packedFilter = new Int16Array((maxFilterElementSize + 2) * destSize); let packedFilterPtr = 0; // For each destination pixel calculate source range and built filter values for (let destPixel = 0; destPixel < destSize; destPixel++) { // Scaling should be done relative to central pixel point const sourcePixel = (destPixel + 0.5) * scaleInverted + offset; const sourceFirst = Math.max(0, Math.floor(sourcePixel - srcWindow)); const sourceLast = Math.min(srcSize - 1, Math.ceil(sourcePixel + srcWindow)); const filterElementSize = sourceLast - sourceFirst + 1; const floatFilter = new Float32Array(filterElementSize); const fxpFilter = new Int16Array(filterElementSize); let total = 0; // Fill filter values for calculated range let index = 0; for (let pixel = sourceFirst; pixel <= sourceLast; pixel++) { const floatValue = filterValue((pixel + 0.5 - sourcePixel) * scaleClamped, a); total += floatValue; floatFilter[index] = floatValue; index++; } // Normalize filter, convert to fixed point and accumulate conversion error let filterTotal = 0; for (let index = 0; index < floatFilter.length; index++) { const filterValue = floatFilter[index] / total; filterTotal += filterValue; fxpFilter[index] = toFixedPoint(filterValue); } // Compensate normalization error, to minimize brightness drift fxpFilter[destSize >> 1] += toFixedPoint(1 - filterTotal); // // Now pack filter to useable form // // 1. Trim heading and tailing zero values, and compensate shitf/length // 2. Put all to single array in this format: // // [ pos shift, data length, value1, value2, value3, ... ] // let leftNotEmpty = 0; while (leftNotEmpty < fxpFilter.length && fxpFilter[leftNotEmpty] === 0) { leftNotEmpty++; } let rightNotEmpty = fxpFilter.length - 1; while (rightNotEmpty > 0 && fxpFilter[rightNotEmpty] === 0) { rightNotEmpty--; } const filterShift = sourceFirst + leftNotEmpty; const filterSize = rightNotEmpty - leftNotEmpty + 1; packedFilter[packedFilterPtr++] = filterShift; // shift packedFilter[packedFilterPtr++] = filterSize; // size packedFilter.set(fxpFilter.subarray(leftNotEmpty, rightNotEmpty + 1), packedFilterPtr); packedFilterPtr += filterSize; } return packedFilter; } /** * Copy the contents of the source image to the destination image * @param source - the source image * @param dest - the destination image * @param sx - source starting x point [Default: 0] * @param sy - source starting y point [Default: 0] * @param sw - source width to use [Default: source width - sx] * @param sh - source height to use [Default: source height - sy] * @param dx - destination starting x point [Default: 0] * @param dy - destination starting y point [Default: 0] */ export function copyImage(source, dest, sx = 0, sy = 0, sw = source.width - sx, sh = source.height - sy, dx = 0, dy = 0) { sx = sx | 0; sy = sy | 0; sw = sw | 0; sh = sh | 0; dx = dx | 0; dy = dy | 0; if (sw <= 0 || sh <= 0) return; const sourceData = new Uint32Array(source.data.buffer); const destData = new Uint32Array(dest.data.buffer); for (let y = 0; y < sh; y++) { const sourceY = sy + y; if (sourceY < 0 || sourceY >= source.height) continue; const destY = dy + y; if (destY < 0 || destY >= dest.height) continue; for (let x = 0; x < sw; x++) { const sourceX = sx + x; if (sourceX < 0 || sourceX >= source.width) continue; const destX = dx + x; if (destX < 0 || destX >= dest.width) continue; const sourceIndex = sourceY * source.width + sourceX; const destIndex = destY * dest.width + destX; destData[destIndex] = sourceData[sourceIndex]; } } } /** * Create an image given the size, fill color and number of channels * @param width - the image width * @param height - the image height * @param data - the image data [Default: creates a new array] * @param fill - the fill color [Default: [0, 0, 0, 0]] * @param channels - the number of channels [Default: 4] * @returns - the created image */ export function createImage(width, height, data, fill = [0, 0, 0, 0], channels = 4) { width = Math.floor(width); height = Math.floor(height); if (width < 1 || height < 1) { throw TypeError('Index or size is negative or greater than the allowed amount'); } const length = width * height * channels; if (data === undefined) { data = new Uint8ClampedArray(length); } if (data.length !== length) { throw TypeError('Index or size is negative or greater than the allowed amount'); } for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * channels; for (let c = 0; c < channels; c++) { data[index + c] = fill[c]; } } } return { data, width, height }; } /** * Lanczos resize function * @param source - the source image * @param dest - the destination image * @param use2 - use 2nd lanczos filter instead of 3rd */ export function resizeImage(source, dest, use2 = false) { const xRatio = dest.width / source.width; const yRatio = dest.height / source.height; const filtersX = filters(source.width, dest.width, xRatio, 0, use2); const filtersY = filters(source.height, dest.height, yRatio, 0, use2); const tmp = new Uint8ClampedArray(dest.width * source.height * 4); convolveImage(source.data, tmp, source.width, source.height, dest.width, filtersX); convolveImage(tmp, dest.data, source.height, dest.width, dest.height, filtersY); } /** * Convolve an image with a filter * @param source - the source image * @param dest - the destination image * @param sw - source width * @param sh - source height * @param dw - destination width * @param filters - image filter */ export function convolveImage(source, dest, sw, sh, dw, filters) { let srcOffset = 0; let destOffset = 0; // For each row for (let sourceY = 0; sourceY < sh; sourceY++) { let filterPtr = 0; // Apply precomputed filters to each destination row point for (let destX = 0; destX < dw; destX++) { // Get the filter that determines the current output pixel. const filterShift = filters[filterPtr++]; let srcPtr = (srcOffset + filterShift * 4) | 0; let r = 0; let g = 0; let b = 0; let a = 0; // Apply the filter to the row to get the destination pixel r, g, b, a for (let filterSize = filters[filterPtr++]; filterSize > 0; filterSize--) { const filterValue = filters[filterPtr++]; r = (r + filterValue * source[srcPtr]) | 0; g = (g + filterValue * source[srcPtr + 1]) | 0; b = (b + filterValue * source[srcPtr + 2]) | 0; a = (a + filterValue * source[srcPtr + 3]) | 0; srcPtr = (srcPtr + 4) | 0; } // Bring this value back in range. All of the filter scaling factors // are in fixed point with FIXED_FRAC_BITS bits of fractional part. // // (!) Add 1/2 of value before clamping to get proper rounding. In other // case brightness loss will be noticeable if you resize image with white // border and place it on white background. // dest[destOffset] = (r + (1 << 13)) >> FIXED_FRAC_BITS; dest[destOffset + 1] = (g + (1 << 13)) >> FIXED_FRAC_BITS; dest[destOffset + 2] = (b + (1 << 13)) >> FIXED_FRAC_BITS; dest[destOffset + 3] = (a + (1 << 13)) >> FIXED_FRAC_BITS; destOffset = (destOffset + sh * 4) | 0; } destOffset = ((sourceY + 1) * 4) | 0; srcOffset = ((sourceY + 1) * sw * 4) | 0; } } //# sourceMappingURL=util.js.map