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
69 changes: 68 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(-45deg, transparent 75%, #ccc 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.fadeInOut {
animation: fadeInOut 3s ease-in-out infinite;
Expand Down Expand Up @@ -83,6 +83,8 @@ <h2>render()</h2>
<div id="stack"></div>
<h2>Look-up tables available to choose</h2>
<div id="luts"></div>
<h2>Projections for 3D images</h2>
<div id="projections"></div>
<script type="module">

async function renderTiles() {
Expand Down Expand Up @@ -367,6 +369,71 @@ <h2>Look-up tables available to choose</h2>
document.getElementById("luts").innerHTML += html;
});


// --- Stag Beetle: max/mean/sum and orthogonal projections ---
const stagUrl = "https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/stag_beetle.ome.zarr";
const stagImg = await omezarr.NgffImage.load(stagUrl);

let stagHtml = "<h3>Stag Beetle: max / mean / sum</h3><div style='display: flex; flex-wrap: wrap; gap: 20px;'>";
for (const mode of ["max", "mean", "sum"]) {
stagHtml += `<div style="text-align: center;">
<img src="${await stagImg.render({projection: mode, autoBoost: true, maxVoxels: 8_000_000})}" title="${mode}" /><br/>
<span>${mode}</span>
</div>`;
}
stagHtml += "</div>";

stagHtml += "<h3>Stag Beetle: Orthogonal projections (z / y / x)</h3><div style='display: flex; flex-wrap: wrap; gap: 20px;'>";
for (const axis of ["z", "y", "x"]) {
stagHtml += `<div style="text-align: center;">
<img src="${await stagImg.render({projection: "max", projectionAxis: axis, autoBoost: true, maxVoxels: 8_000_000})}" title="axis ${axis}" /><br/>
<span>axis ${axis}</span>
</div>`;
}
stagHtml += "</div>";
document.getElementById("projections").innerHTML += stagHtml;

// --- IDR0062: 3D multi-channel with LUTs ---
const idr62Url = "https://livingobjects.ebi.ac.uk/idr/zarr/v0.4/idr0062A/6001240_ngff-zarr.ome.zarr";
const idr62Img = await omezarr.NgffImage.load(idr62Url);

let idr62Html = "<h3>IDR0062: Projection with LUTs</h3><div style='display: flex; flex-wrap: wrap; gap: 20px;'>";

for (const mode of ["max", "mean", "sum"]) {
idr62Html += `<div style="text-align: center;">
<img src="${await idr62Img.render({projection: mode, autoBoost: true, maxVoxels: 4_000_000})}" title="${mode}" /><br/>
<span>${mode}</span>
</div>`;
}
idr62Html += "</div>";

idr62Img.setChannelActive(0, true);
idr62Img.setChannelActive(1, false);

idr62Html += "<h3>IDR0062: Channel 0 with LUTs</h3><div style='display: flex; flex-wrap: wrap; gap: 20px;'>";

idr62Img.setChannelLut(0, omezarr.luts.FIRE);
idr62Html += `<div style="text-align: center;">
<img src="${await idr62Img.render({projection: "max", autoBoost: true, maxVoxels: 4_000_000})}" title="FIRE" /><br/>
<span>FIRE</span>
</div>`;

idr62Img.setChannelLut(0, omezarr.luts.SIXTEEN_COLORS);
idr62Html += `<div style="text-align: center;">
<img src="${await idr62Img.render({projection: "max", autoBoost: true, maxVoxels: 4_000_000})}" title="16_colors" /><br/>
<span>16_colors</span>
</div>`;

idr62Img.setChannelLut(0, undefined);
idr62Img.setChannelInverted(0, true);
idr62Html += `<div style="text-align: center;">
<img src="${await idr62Img.render({projection: "max", autoBoost: true, maxVoxels: 4_000_000})}" title="inverted" /><br/>
<span>inverted</span>
</div>`;

idr62Html += "</div>";
document.getElementById("projections").innerHTML += idr62Html;

</script>
</body>
</html>
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 80 additions & 12 deletions src/image.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@

import * as zarr from "zarrita";
import { ImageAttrs, ImageAttrsV5, OmeAttrs, Multiscale, Omero, Axis, Channel, Color } from "./types/ome";
import { createRgbDataUrl, openArray, openGroup, createOmero } from "./utils";
import { renderArray } from "./render";
import { renderArray, Projection } from "./render";
import { generateNeuroglancerStateForOmeZarr, LayerType } from "./helper";

export class NgffImage {
/**
* This is the main Image class for the API. It handles both v0.4 and v0.5 formats,
* This is the main Image class for the API. It handles both v0.4 and v0.5 formats,
* and provides a consistent interface for accessing the multiscale datasets,
* omero metadata, and zarr version.
*
Expand Down Expand Up @@ -96,7 +95,7 @@ export class NgffImage {
}
attrs = group.attrs as OmeAttrs;
}

// create an instance of the static class...
const img = new this(attrs, store);

Expand Down Expand Up @@ -372,23 +371,68 @@ export class NgffImage {
return path;
}

/**
* Count the voxels a projection would fetch for a given shape: nz * ny * nx.
*/
private countVoxels(shape: number[]): number {
const names = this.getAxesNames();
const zi = names.indexOf("z");
const yi = names.indexOf("y");
const xi = names.indexOf("x");
// If any of the axes are missing, treat them as size 1 (e.g. a 2D image has no z axis).
const sz = zi === -1 ? 1 : shape[zi];
const sy = yi === -1 ? 1 : shape[yi];
const sx = xi === -1 ? 1 : shape[xi];
return sz * sy * sx;
}


/**
* This is the projection counterpart of getPathForTargetSize()
* Default is limited to 64M voxels (~64MB if uint8)
*/
async getPathForTargetVolume(maxVoxels: number = 64_000_000): Promise<string> {
const names = this.getAxesNames();
if (!names.includes("z")) {
throw new Error("getPathForTargetVolume requires a 'z' axis; image has none");
}
// shapes can be empty for v0.1-v0.3 (no coordinateTransformations)
const shapes = await this.calcShapes();
if (!shapes.length) {
// no scales to reason about - use the smallest level
return this.paths[this.paths.length - 1];
}
for (let i = 0; i < shapes.length; i++) {
if (this.countVoxels(shapes[i]) <= maxVoxels) {
return this.paths[i];
}
}
return this.paths[this.paths.length - 1];
}

async renderArray(options: {
// Array can be provided directly, or we will load based on targetSize or arrayPathOrIndex
arr?: zarr.Array<any> | string,
targetSize?: number,
arrayPathOrIndex?: string | number,
arrayPathOrIndex?: string | number,
slices?: { [k: string]: number | [number, number] | undefined },
autoBoost?: boolean,
channels?: Channel[],
maxSize?: number,
signal?: AbortSignal,
calcMinMaxForRange?: boolean,
projection?: Projection, // "max" | "mean" | "sum"
projectionAxis?: "z" | "y" | "x", // default "z"
maxVoxels?: number, // budget for the nz*ny*nx fetch
} = {}
): Promise<{
data: Uint8ClampedArray;
width: number,
height: number
}> {
const projection = options.projection;
const maxVoxels = options.maxVoxels ?? 64_000_000;

let arr;
if (options.arr) {
if (typeof options.arr === "string") {
Expand All @@ -403,13 +447,16 @@ export class NgffImage {
path = options.arrayPathOrIndex;
} else if (options.targetSize !== undefined) {
path = await this.getPathForTargetSize(options.targetSize);
} else if (projection) {
// No level given: for a projection, pick by VOLUME rather than XY size.
path = await this.getPathForTargetVolume(maxVoxels);
} else {
throw new Error("Need to specify arr OR targetSize OR arrayPathOrIndex ")
throw new Error("Need to specify arr OR targetSize OR arrayPathOrIndex OR projection");
}
arr = await this.openArray(path);
}

// TODO: decide when to ignore maxSize?
// TODO: decide when to ignore maxSize?
// E.g. if targetSize is specified, use maxSize = 2 x targetSize?
let maxSize = options.maxSize ?? 1000;
let shape = arr.shape;
Expand All @@ -424,10 +471,22 @@ export class NgffImage {
);
}

// A projection fetches nz * ny * nx voxels,
if (projection) {
const voxels = this.countVoxels(shape);
if (voxels > maxVoxels) {
throw new Error(
`Projection would fetch ${voxels.toLocaleString()} voxels, over the ` +
`'maxVoxels' limit of ${maxVoxels.toLocaleString()}. Use a coarser ` +
`level (arrayPathOrIndex / getPathForTargetVolume), or raise maxVoxels.`
);
}
}

// let omero = options.omero || this.omero;
let slices = options.slices || {};
// Get slices for each channel
if (slices["z"] == undefined) {
// Get slices for each channel.
if (slices["z"] == undefined && !projection) {
slices["z"] = this.omero?.rdefs?.defaultZ;
}

Expand Down Expand Up @@ -458,7 +517,12 @@ export class NgffImage {
channels,
slices,
Boolean(options.autoBoost),
{ signal: options.signal, calcMinMaxForRange: Boolean(calcMinMaxForRange) }
{
signal: options.signal,
calcMinMaxForRange: Boolean(calcMinMaxForRange),
projection: options.projection,
projectionAxis: options.projectionAxis,
}
);

return { data, width, height };
Expand All @@ -468,13 +532,17 @@ export class NgffImage {
// Array can be provided directly, or we will load based on targetSize or arrayPathOrIndex
arr?: zarr.Array<any> | string,
targetSize?: number,
arrayPathOrIndex?: string | number,
arrayPathOrIndex?: string | number,
slices?: { [k: string]: number | [number, number] | undefined },
autoBoost?: boolean,
channels?: Channel[],
maxSize?: number,
signal?: AbortSignal,
calcMinMaxForRange?: boolean
calcMinMaxForRange?: boolean,
// Projection: collapse an axis instead of picking a single plane.
projection?: Projection, // "max" | "mean" | "sum"
projectionAxis?: "z" | "y" | "x", // default "z"
maxVoxels?: number,
} = {}
): Promise<string> {
let { data, width } = await this.renderArray(options);
Expand Down
Loading
Loading