Skip to content

Commit

Permalink
Merge pull request #635 from girder/tile-frames-client
Browse files Browse the repository at this point in the history
Use tile-frames for better frame browsing.
  • Loading branch information
manthey authored Aug 12, 2021
2 parents bdf0461 + a34cbed commit 5e7246a
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
### Features
- Provide band information on all tile sources (#622, #623)
- Add a tileFrames method to tile sources and appropriate endpoints to composite regions from multiple frames to a single output image (#629)
- The test tile source now support frames (#631)
- The test tile source now support frames (#631, #632, #634, #634)

### Improvements
- Better handle TIFFs with missing levels and tiles (#624, #627)
- Better report inefficient TIFFs (#626)
- Smoother cross-frame navigation (#635)

## Version 1.6.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import d3 from 'd3';
import { restRequest } from '@girder/core/rest';

import ImageViewerWidget from './base';
import setFrameQuad from './setFrameQuad.js';

window.hammerjs = Hammer;
window.d3 = d3;
Expand Down Expand Up @@ -77,6 +78,16 @@ var GeojsImageViewerWidget = ImageViewerWidget.extend({
this.viewer = geo.map(params.map);
params.layer.autoshareRenderer = false;
this._layer = this.viewer.createLayer('osm', params.layer);
if (this.metadata.frames && this.metadata.frames.length > 1) {
setFrameQuad(this.metadata, this._layer, {
// allow more and larger textures is slower, balancing
// performance and appearance
// maxTextures: 16,
// maxTotalTexturePixels: 256 * 1024 * 1024,
baseUrl: this._getTileUrl('{z}', '{x}', '{y}').split('/tiles/')[0] + '/tiles'
});
this._layer.setFrameQuad(0);
}
} else {
params = {
keepLower: false,
Expand Down Expand Up @@ -134,19 +145,40 @@ var GeojsImageViewerWidget = ImageViewerWidget.extend({
if (frame === 0) {
return;
}
// use two layers to get smooth transitions
this._layer2 = this.viewer.createLayer('osm', this._layer._options);
this._layer2.moveDown();
this._baseurl = this._layer.url();
this._frame = 0;
this._baseurl = this._layer.url();
let quadLoaded = ((this._layer.setFrameQuad || {}).status || {}).loaded;
if (!quadLoaded) {
// use two layers to get smooth transitions until we load
// background quads.
this._layer2 = this.viewer.createLayer('osm', this._layer._options);
this._layer2.moveDown();
setFrameQuad((this._layer.setFrameQuad.status || {}).tileinfo, this._layer2, (this._layer.setFrameQuad.status || {}).options);
this._layer2.setFrameQuad(0);
}
}
this._nextframe = frame;
if (frame !== this._frame && !this._updating) {
this._updating = true;
this._frame = frame;
this.trigger('g:imageFrameChanging', this, frame);
let quadLoaded = ((this._layer.setFrameQuad || {}).status || {}).loaded;
if (quadLoaded) {
if (this._layer2) {
this.viewer.deleteLayer(this._layer2);
delete this._layer2;
}
this._layer.url(this.getFrameAndUrl().url);
this._layer.setFrameQuad(frame);
this._layer.frame = frame;
this.trigger('g:imageFrameChanged', this, frame);
return;
}

this._updating = true;
this.viewer.onIdle(() => {
this._layer2.url(this.getFrameAndUrl().url);
this._layer2.setFrameQuad(frame);
this._layer2.frame = frame;
this.viewer.onIdle(() => {
this._layer.moveDown();
var ltemp = this._layer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* Given metadata on a tile source, a GeoJS tileLayer, and a set of options,
* add a function to the layer `setFrameQuad(<frame>)` that will, if possible,
* set the baseQuad to a cropped section of an image that contains excerpts of
* all frames.
*
* @param {object} tileinfo The metadata of the source image. This expects
* ``sizeX`` and ``sizeY`` to be the width and height of the image and
* ``frames`` to contain a list of the frames of the image or be undefined if
* there is only one frame.
* @param {geo.tileLayer} layer The GeoJS layer to add the function to. This
* is also used to get a maximal texture size if the layer is a webGL
* layer.
* @param {object} options Additional options for the function. This must
* minimally include ``baseUrl``.
* @param {string} options.baseUrl The reference to the tile endpoint, e.g.,
* <url>/api/v1/item/<item id>/tiles.
* @param {string} [options.format='encoding=JPEG&jpegQuality=85&jpegSubsampling=1']
* The compression and format for the texture.
* @param {string} [options.query] Additional query options to add to the
* tile_frames endpoint, e.g. 'style={"min":"min","max":"max"}'. Do not
* include framesAcross or frameList. You must specify 'cache=true' if
* that is desired.
* @param {number} [options.frameBase=0] Starting frame number used.
* @param {number} [options.frameStride=1] Only use ever ``frameStride`` frame
* of the image.
* @param {number} [options.maxTextureSize] Limit the maximum texture size to a
* square of this size. The size is also limited by the WebGL maximum
* size for webgl-based layers or 16384 for canvas-based layers.
* @param {number} [options.maxTextures=1] If more than one, allow multiple
* textures to increase the size of the individual frames. The number of
* textures will be capped by ``maxTotalTexturePixels`` as well as this
* number.
* @param {number} [options.maxTotalTexturePixels=1073741824] Limit the
* maximum texture size and maximum number of textures so that the combined
* set does not exceed this number of pixels.
* @param {number} [options.alignment=16] Individual frames are buffer to an
* alignment of this maxy pixels. If JPEG compression is used, this should
* be 8 for monochrome images or jpegs without subsampling, or 16 for jpegs
* with moderate subsampling to avoid compression artifacts from leaking
* between frames.
* @param {number} [options.adjustMinLevel=true] If truthy, adjust the tile
* layer's minLevel after the quads are loaded.
* @param {number} [options.maxFrameSize] If set, limit the maximum width and
* height of an individual frame to this value.
* @param {string} [options.crossOrigin] If specified, use this as the
* crossOrigin policy for images.
*/
function setFrameQuad(tileinfo, layer, options) {
layer.setFrameQuad = function () { };
if (!tileinfo || !tileinfo.sizeX || !tileinfo.sizeY || !options || !options.baseUrl) {
return;
}
let maxTextureSize;
try {
maxTextureSize = layer.renderer()._maxTextureSize || layer.renderer().constructor._maxTextureSize;
} catch (err) { }
let w = tileinfo.sizeX,
h = tileinfo.sizeY,
numFrames = (tileinfo.frames || []).length || 1,
texSize = maxTextureSize || 16384,
textures = options.maxTextures || 1,
maxTotalPixels = options.maxTotalTexturePixels || 1073741824,
alignment = options.alignment || 16;
let frames = [];
for (let fidx = options.frameBase || 0; fidx < numFrames; fidx += options.frameStride || 1) {
frames.push(fidx);
}
numFrames = frames.length;
if (numFrames === 0 || !Object.getOwnPropertyDescriptor(layer, 'baseQuad')) {
return;
}
texSize = Math.min(texSize, options.maxTextureSize || texSize);
while (texSize ** 2 > maxTotalPixels) {
texSize /= 2;
}
while (textures && texSize ** 2 * textures > maxTotalPixels) {
textures -= 1;
}
let fw, fh, fhorz, fvert;
/* Iterate in case we can reduce the number of textures or the texture
* size */
while (true) {
let f = Math.ceil(numFrames / textures); // frames per texture
let texScale2 = texSize ** 2 / f / w / h;
// frames across the texture
fhorz = Math.ceil(texSize / (Math.floor(w * texScale2 ** 0.5 / alignment) * alignment));
fvert = Math.ceil(texSize / (Math.floor(h * texScale2 ** 0.5 / alignment) * alignment));
// tile sizes
fw = Math.floor(texSize / fhorz / alignment) * alignment;
fh = Math.floor(texSize / fvert / alignment) * alignment;
if (options.maxFrameSize) {
let maxFrameSize = Math.floor(options.maxFrameSize / alignment) * alignment;
fw = Math.min(fw, maxFrameSize);
fh = Math.min(fh, maxFrameSize);
}
if (fw > w) {
fw = Math.ceil(w / alignment) * alignment;
}
if (fh > h) {
fh = Math.ceil(h / alignment) * alignment;
}
// shrink one dimension so account for aspect ratio
fw = Math.min(Math.ceil(fw * w / h / alignment) * alignment, fw);
fh = Math.min(Math.ceil(fw * h / w / alignment) * alignment, fh);
// recompute frames across the texture
fhorz = Math.floor(texSize / fw);
fvert = Math.min(Math.floor(texSize / fh), Math.ceil(numFrames / fhorz));
// check if we are not using all textires or are using less than a
// quarter of one texture. If not, stop, if so, reduce and recalculate
if (textures > 1 && numFrames <= fhorz * fvert * (textures - 1)) {
textures -= 1;
continue;
}
if (fhorz >= 2 && Math.ceil(f / Math.floor(fhorz / 2)) * fh <= texSize / 2) {
texSize /= 2;
continue;
}
break;
}
// used area of each tile
let usedw = Math.floor(w / Math.max(w / fw, h / fh)),
usedh = Math.floor(h / Math.max(w / fw, h / fh));
// get the set of texture images
let status = {
tileinfo: tileinfo,
options: options,
images: [],
src: [],
quads: [],
frames: frames,
framesToIdx: {}
};
if (tileinfo.tileWidth && tileinfo.tileHeight) {
// report that tiles below this level are not needed
status.minLevel = Math.ceil(Math.log(Math.min(usedw / tileinfo.tileWidth, usedh / tileinfo.tileHeight)) / Math.log(2));
}
frames.forEach((frame, idx) => { status.framesToIdx[frame] = idx; });
for (let idx = 0; idx < textures; idx += 1) {
let img = new Image();
if (options.baseUrl.indexOf(':') >= 0 && options.baseUrl.indexOf('/') === options.baseUrl.indexOf(':') + 1) {
img.crossOrigin = options.crossOrigin || 'anonymous';
}
let frameList = frames.slice(idx * fhorz * fvert, (idx + 1) * fhorz * fvert);
let src = `${options.baseUrl}/tile_frames?framesAcross=${fhorz}&width=${fw}&height=${fh}&fill=corner:black&exact=false`;
if (frameList.length !== (tileinfo.frames || []).length) {
src += `&frameList=${frameList.join(',')}`;
}
src += '&' + (options.format || 'encoding=JPEG&jpegQuality=85&jpegSubsampling=1').replace(/(^&|^\?|\?$|&$)/g, '');
if (options.query) {
src += '&' + options.query.replace(/(^&|^\?|\?$|&$)/g, '');
}
status.src.push(src);
if (idx === textures - 1) {
img.onload = function () {
status.loaded = true;
if (layer._options && layer._options.minLevel !== undefined && (options.adjustMinLevel === undefined || options.adjustMinLevel) && status.minLevel && status.minLevel > layer._options.minLevel) {
layer._options.minLevel = Math.min(layer._options.maxLevel, status.minLevel);
}
};
} else {
((idx) => {
img.onload = function () {
status.images[idx + 1].src = status.src[idx + 1];
};
})(idx);
}
status.images.push(img);
// the last image can have fewer frames than the other images
let f = frameList.length;
let ivert = Math.ceil(f / fhorz),
ihorz = Math.min(f, fhorz);
frameList.forEach((frame, fidx) => {
let quad = {
// z = -1 to place under other tile layers
ul: {x: 0, y: 0, z: -1},
// y coordinate is inverted
lr: {x: w, y: -h, z: -1},
crop: {
x: w,
y: h,
left: (fidx % ihorz) * fw,
top: (ivert - Math.floor(fidx / ihorz)) * fh - usedh,
right: (fidx % ihorz) * fw + usedw,
bottom: (ivert - Math.floor(fidx / ihorz)) * fh
},
image: img
};
status.quads.push(quad);
});
}
status.images[0].src = status.src[0];

layer.setFrameQuad = function (frame) {
if (status.framesToIdx[frame] !== undefined) {
layer.baseQuad = Object.assign({}, status.quads[status.framesToIdx[frame]]);
status.frame = frame;
}
};
layer.setFrameQuad.status = status;
}

export default setFrameQuad;

0 comments on commit 5e7246a

Please sign in to comment.