Skip to content

Commit

Permalink
Zarr rendering in panel_view.js
Browse files Browse the repository at this point in the history
  • Loading branch information
will-moore committed Dec 4, 2024
1 parent df9cd78 commit e55c396
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 31 deletions.
48 changes: 24 additions & 24 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@
"raphael": "^2.3.0",
"sortablejs": "^1.15.2",
"underscore": "^1.13.4",
"zarrita": "^0.4.0-next.17"
"zarrita": "^0.4.0-next.19"
}
}
7 changes: 4 additions & 3 deletions src/js/models/figure_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -549,11 +549,12 @@
let axesNames = axes.map(axis => axis.name);

let datasets = multiscales[0].datasets;
let zarrays = [];
let zarrays = {};
// 'consolidate' the metadata for all arrays
for (let ds of datasets) {
let zarray = await fetch(`${zarrUrl}/${ds.path}/.zarray`).then(rsp => rsp.json());
zarrays.push(zarray);
let path = ds.path;
let zarray = await fetch(`${zarrUrl}/${path}/.zarray`).then(rsp => rsp.json());
zarrays[path] = zarray;
}
// store under 'arrays' key
zattrs['arrays'] = zarrays;
Expand Down
10 changes: 9 additions & 1 deletion src/js/models/panel_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Backbone from "backbone";
import _ from "underscore";
import $ from "jquery";
import {renderZarrToSrc} from "./zarr_utils";

// Corresponds to css - allows us to calculate size of labels
var LINE_HEIGHT = 1.43;
Expand Down Expand Up @@ -870,7 +871,14 @@
return this.get('orig_width') * this.get('orig_height') > MAX_PLANE_SIZE;
},

get_img_src: function(force_no_padding) {
get_zarr_img_src: async function() {
return renderZarrToSrc(this.get('imageId'), this.get('zarr'), this.get('theZ'), this.get('theT'), this.get('channels'));
},

get_img_src: async function(force_no_padding) {
if (this.get("zarr")) {
return this.get_zarr_img_src();
}
var chs = this.get('channels');
var cStrings = chs.map(function(c, i){
return (c.active ? '' : '-') + (1+i) + "|" + c.window.start + ":" + c.window.end + "$" + c.color;
Expand Down
205 changes: 205 additions & 0 deletions src/js/models/zarr_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@

import * as zarr from "zarrita";
import { slice } from "@zarrita/indexing";


export async function renderZarrToSrc(source, attrs, theZ, theT, channels) {
let paths = attrs.multiscales[0].datasets.map((d) => d.path);
let axes = attrs.multiscales[0].axes.map((a) => a.name);
let zarrays = attrs.arrays;

// Pick first resolution that is below a max size...
const MAX_SIZE = 2000;
console.log("Zarr pick size to render...");
let path;
for (let p of paths) {
let arrayAttrs = zarrays[p];
console.log(path, arrayAttrs);
let shape = arrayAttrs.shape;
if (shape.at(-1) * shape.at(-2) < MAX_SIZE * MAX_SIZE) {
path = p;
break;
}
}

if (!path) {
console.error(`Lowest resolution too large for rendering: > ${MAX_SIZE} x ${MAX_SIZE}`);
return;
}

console.log("Init zarr.FetchStore:", source + "/" + path);
const store = new zarr.FetchStore(source + "/" + path);

const arr = await zarr.open(store, { kind: "array" });

let chDim = axes.indexOf("c");
let shape = zarrays[path].shape;
let dims = shape.length;

let activeChIndicies = [];
let colors = [];
let minMaxValues = [];
channels.forEach((ch, index) => {
if (ch.active) {
activeChIndicies.push(index);
colors.push(hexToRGB(ch.color));
minMaxValues.push([ch.window.start, ch.window.end]);
}
});
console.log("activeChIndicies", activeChIndicies);
console.log("colors", colors);
console.log("minMaxValues", minMaxValues);

let promises = activeChIndicies.map((chIndex) => {
let slices = shape.map((dimSize, index) => {
// channel
if (index == chDim) return chIndex;
// x and y
if (index >= dims - 2) {
return slice(0, dimSize);
}
// z
if (axes[index] == "z") {
return parseInt(dimSize / 2 + "");
}
if (axes[index] == "t") {
return parseInt(dimSize / 2 + "");
}
return 0;
});
console.log("Zarr chIndex slices:", chIndex, slices);
console.log("Zarr chIndex shape:", chIndex, shape);
// TODO: add controller: { opts: { signal: controller.signal } }
return zarr.get(arr, slices);
});

let ndChunks = await Promise.all(promises);
// let minMaxValues = ndChunks.map((ch) => getMinMaxValues(ch));
let rbgData = renderTo8bitArray(ndChunks, minMaxValues, colors);

let width = shape.at(-1);
let height = shape.at(-2);

const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
context.putImageData(new ImageData(rbgData, width, height), 0, 0);
let dataUrl = canvas.toDataURL("image/png");
console.log("Zarr dataUrl", dataUrl);
return dataUrl;
}


export function renderTo8bitArray(ndChunks, minMaxValues, colors) {
// Render chunks (array) into 2D 8-bit data for new ImageData(arr)
// ndChunks is list of zarr arrays

// assume all chunks are same shape
const shape = ndChunks[0].shape;
const height = shape[0];
const width = shape[1];
const pixels = height * width;

if (!minMaxValues) {
minMaxValues = ndChunks.map(getMinMaxValues);
}

// let rgb = [255, 255, 255];

let rgba = new Uint8ClampedArray(4 * height * width).fill(0);
let offset = 0;
for (let y = 0; y < pixels; y++) {
for (let p = 0; p < ndChunks.length; p++) {
let rgb = colors[p];
let data = ndChunks[p].data;
let range = minMaxValues[p];
let rawValue = data[y];
let fraction = (rawValue - range[0]) / (range[1] - range[0]);
// for red, green, blue,
for (let i = 0; i < 3; i++) {
// rgb[i] is 0-255...
let v = (fraction * rgb[i]) << 0;
// increase pixel intensity if value is higher
rgba[offset * 4 + i] = Math.max(rgba[offset * 4 + i], v);
}
}
rgba[offset * 4 + 3] = 255; // alpha
offset += 1;
}

return rgba;
}

export function getMinMaxValues(chunk2d) {
const data = chunk2d.data;
let maxV = 0;
let minV = Infinity;
let length = chunk2d.data.length;
for (let y = 0; y < length; y++) {
let rawValue = data[y];
maxV = Math.max(maxV, rawValue);
minV = Math.min(minV, rawValue);
}
return [minV, maxV];
}

export const MAX_CHANNELS = 4;
export const COLORS = {
cyan: "#00FFFF",
yellow: "#FFFF00",
magenta: "#FF00FF",
red: "#FF0000",
green: "#00FF00",
blue: "#0000FF",
white: "#FFFFFF",
};
export const MAGENTA_GREEN = [COLORS.magenta, COLORS.green];
export const RGB = [COLORS.red, COLORS.green, COLORS.blue];
export const CYMRGB = Object.values(COLORS).slice(0, -2);

export function getDefaultVisibilities(n) {
let visibilities;
if (n <= MAX_CHANNELS) {
// Default to all on if visibilities not specified and less than 6 channels.
visibilities = Array(n).fill(true);
} else {
// If more than MAX_CHANNELS, only make first set on by default.
visibilities = [
...Array(MAX_CHANNELS).fill(true),
...Array(n - MAX_CHANNELS).fill(false),
];
}
return visibilities;
}

export function getDefaultColors(n, visibilities) {
let colors = [];
if (n == 1) {
colors = [COLORS.white];
} else if (n == 2) {
colors = MAGENTA_GREEN;
} else if (n === 3) {
colors = RGB;
} else if (n <= MAX_CHANNELS) {
colors = CYMRGB.slice(0, n);
} else {
// Default color for non-visible is white
colors = Array(n).fill(COLORS.white);
// Get visible indices
const visibleIndices = visibilities.flatMap((bool, i) => (bool ? i : []));
// Set visible indices to CYMRGB colors. visibleIndices.length === MAX_CHANNELS from above.
for (const [i, visibleIndex] of visibleIndices.entries()) {
colors[visibleIndex] = CYMRGB[i];
}
}
return colors.map(hexToRGB);
}

export function hexToRGB(hex) {
if (hex.startsWith("#")) hex = hex.slice(1);
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return [r, g, b];
}
4 changes: 2 additions & 2 deletions src/js/views/panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@
this.$img_panel.show();
}.bind(this));

var src = this.model.get_img_src();
this.$img_panel.attr('src', src);
this.model.get_img_src()
.then(src => this.$img_panel.attr('src', src));

// if a 'reasonable' dpi is set, we don't pixelate
if (this.model.get('min_export_dpi') > 100) {
Expand Down

0 comments on commit e55c396

Please sign in to comment.