Public
Edited
Jul 13, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
proj = getMapGeoProjection(d3.geoMiller, [sampleImage.naturalWidth, sampleImage.naturalHeight], mapGeoExtent)
Insert cell
Insert cell
sampleTopology = await buildTopology(sampleTraceResult); //, { projection: proj })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const start = performance.now();
const p = new GpuImageTracer({ maxTileSize: 2048, colorTable: sampleColorTable });
const result = await p.traceImage(sampleImage);
return {
result,
time: performance.now() - start
};
}
Insert cell
{
const start = performance.now();
const p = new GpuImageTracer({ maxTileSize: 512, colorTable: sampleColorTable });
const result = await p.traceImageSerial(sampleImage);
return {
result,
time: performance.now() - start
};
}
Insert cell
class GpuImageTracer {
static {
GpuImageTracer.maxWorkerCount = Math.max(1, navigator.hardwareConcurrency - 1)
}
constructor(options) {
options = options ?? {};
this.colorTable = options.colorTable;
this.onTileProcessed = options.fnOnTileProcessed ?? (() => true);
this.maxTileSize = options.maxTileSize;

// combined coordClasses and pixelVectors
this.coordClasses = new MaskMap(CoordMask);
const pixelVectors = this.pixelVectors = new Map();
pixelVectors.getOrCreate = (key) => {
let maskSet = pixelVectors.get(key);
if (!maskSet) pixelVectors.set(key, maskSet = new MaskSet(XydMask));
return maskSet;
}
}

async traceImage(image) {
// image
this.image = image;
this.imageSize = [image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
// tileSlicer
const tsOptions = {};
if (this.maxTileSize) tsOptions.maxTileSize = this.maxTileSize;
const tileSlicer = new TileSlicer(this.image, tsOptions);
const tileRects = tileSlicer.getTileRects();

// worker pool
const workers = [];
{
const start = performance.now();
const workerPromises = [];
const workerCount = Math.min(tileRects.length, GpuImageTracer.maxWorkerCount);
for (let i = 0; i < workerCount; i++) {
const p = getWorker(tileSlicer.baseTileSize, this.colorTable).then(w => workers.push(w));
workerPromises.push(p);
}
await Promise.all(workerPromises);
console.log(workerCount + ' workers initialized', performance.now() - start);
}
const promises = new Set();
const busyWorkers = new Set();

// process tiles
for (let rect of tileSlicer.getTileRects()) {
while (busyWorkers.size >= workers.length) {
await Promise.race([...promises]);
}
const freeWorker = workers.find(w => !busyWorkers.has(w));
busyWorkers.add(freeWorker);
const bitmap = await tileSlicer.getTileBitmap(rect);
let promise = freeWorker.gpuReadTile(bitmap, rect).then(r => {
promises.delete(promise);
busyWorkers.delete(freeWorker);
this._ingestGpuTileResult(r);
new Promise(() => this.onTileProcessed(rect.unpadded));
bitmap.close();
});
promises.add(promise);
await pauseIfNeeded();
}
await Promise.all([...promises]);
tileSlicer.destroy();
await pauseIfNeeded();
workers.forEach(w => w.terminate());
const result = await this._buildRings();
await pauseIfNeeded();
return result;
}

async traceImageSerial(image) {
// image
this.image = image;
this.imageSize = [image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
// tileSlicer
const tsOptions = {};
if (this.maxTileSize) tsOptions.maxTileSize = this.maxTileSize;
const tileSlicer = new TileSlicer(this.image, tsOptions);

// reglProgram
const reglProgram = getReglProgram(tileSlicer.baseTileSize, this.colorTable);
// process tiles
for (let rect of tileSlicer.getTileRects()) {
const bitmap = await tileSlicer.getTileBitmap(rect);
const gpuOutput = await gpuProcessTile(bitmap, rect, reglProgram);
bitmap.close();

const tileResult = {};
tileResult.pixelVectors = await extractPixelVectors(gpuOutput);
await pauseIfNeeded();
tileResult.coordClasses = await extractCoordClasses(gpuOutput);
await pauseIfNeeded();

this._ingestGpuTileResult(tileResult);
await pauseIfNeeded();

new Promise(() => this.onTileProcessed(rect.unpadded));
}
tileSlicer.destroy();
const result = await this._buildRings();
await pauseIfNeeded();
return result;
}

_ingestGpuTileResult(tileResult) {
for (let [colorNum, newVectors] of tileResult.pixelVectors) {
this.pixelVectors.getOrCreate(colorNum).addValues(newVectors);
}
for (let [coord, coordClass] of tileResult.coordClasses) {
this.coordClasses.set(coord, coordClass);
}
}

async _buildRings() {
const polygonRings = await pixelVectorsToRings(this.pixelVectors, this.coordClasses);
const bbox = [0, 0, ...this.imageSize];
return { bbox, polygonRings, coordClasses: this.coordClasses };
}
}
Insert cell
getReglProgram(1024)
Insert cell
async function gpuProcessTile(bitmap, rect, reglProgram, options) {
const { regl, commands } = reglProgram;

const tileTexture = regl.texture({ data: bitmap });
const tileDimensions = [ bitmap.width, bitmap.height ];
const canvas = reglProgram.canvas;
if (canvas.width != bitmap.width) canvas.width = bitmap.width;
if (canvas.height != bitmap.height) canvas.height = bitmap.height;
const params = {};
for (let name of ["color", "neighbors", "coords"]) {
const cropRect = {...rect.crop};
if (name == "coords") {
if (rect.mapEdges.e) cropRect.width++;
if (rect.mapEdges.s) cropRect.height++;
}

const { width, height } = cropRect;
const exportByteLength = (4 * width * height);
const data = params[name] = new Uint8Array(exportByteLength);

params[name] = { cropRect, data };
}
params.color.buffer = regl.framebuffer({ depthStencil: false, shape: tileDimensions });
const result = {
offset: [rect.x, rect.y],
mapEdges: rect.mapEdges
};
async function createPixelIO(name) {
let { buffer, cropRect, data } = params[name];
if (buffer) {
regl.read({ framebuffer: buffer, ...cropRect, data });
const result = new PixelIO(cropRect.width, cropRect.height, data);
await pauseIfNeeded();
return result;
}
else {
const result = PixelIO.fromImageSource(canvas, cropRect);
return result;
}
}

// execute replaceColors into the buffer
commands.replaceColors({ uSampler: tileTexture, framebuffer: params.color.buffer });
// execute replaceColors onto the canvas, save the output to result
commands.replaceColors({ uSampler: tileTexture });
result.colorIO = await createPixelIO("color");

// execute findNeighbors onto the canvas, save the output to result
commands.findNeighbors({ uSampler: params.color.buffer });
result.neighborsIO = await createPixelIO("neighbors");

// execute findNeighbors onto the canvas, save the output to result
commands.classifyCoords({ uSampler: params.color.buffer });
result.coordsIO = await createPixelIO("coords");
[tileTexture, ...Object.values(params).map(p => p.buffer).filter(p => p)].forEach(r => r.destroy());
closeMapEdges(result);
return result;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
workerUrl = {
const workerScript = `${getWorkerHeader()}
/* Worker ---------------------------------------------------------------- */
self.reglProgram = undefined;

self.functions = {
createReglProgram: async function(baseTileSize, colorTable) {
self.reglProgram = getReglProgram(baseTileSize, colorTable);
},
gpuProcessTile: async function(bitmap, rect, options) {
const value = await gpuProcessTile(bitmap, rect, self.reglProgram, options);
return {
value,
transferables: [value.colorIO.dataView.buffer, value.neighborsIO.dataView.buffer, value.coordsIO.dataView.buffer]
}
},
gpuReadTile: async function(bitmap, rect, options) {
const gpuOutput = await gpuProcessTile(bitmap, rect, self.reglProgram, options);
bitmap.close();

const coordClasses = (await extractCoordClasses(gpuOutput)).numericMap;
const pixelVectors = await extractPixelVectors(gpuOutput);
for (let [key, maskSet] of pixelVectors) {
const numericSet = maskSet.numericSet;
pixelVectors.set(key, numericSet);
}

return {
value: {
coordClassesNumeric: coordClasses,
pixelVectorsNumeric: pixelVectors
}
};
},
testSuccess: async function(num) {
await new Promise((resolve) => setTimeout(resolve, 3000));
return { value: "testSuccess tested successfully " + num };
},
testFailure: async function(num) {
throw "testFailure failed successfully " + num;
}
};

self.onmessage = function(event) {
const { fn, params } = event.data ?? {};
params ??= [];

const resultPromise = self.functions[fn](...params);
resultPromise.then(r => {
const { value, transferables } = (r ?? {})
self.postMessage(value, transferables);
});
}
self.onunhandledrejection = function (event) {
throw event.reason;
};`;

var workerBlob = new Blob(
[workerScript],
{ type:'text/javascript' }
);
var workerBlobUrl = URL.createObjectURL(workerBlob);
invalidation.then(() => URL.revokeObjectURL(workerBlobUrl));

return workerBlobUrl;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
li = undefined; // new LoadIndicator(sampleImage);
Insert cell
li.canvas
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more