Skip to content

Commit

Permalink
Don't use viv/Deck.gl for thumbnails
Browse files Browse the repository at this point in the history
  • Loading branch information
will-moore committed Feb 14, 2023
1 parent 230ccd8 commit 53fface
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 23 deletions.
57 changes: 34 additions & 23 deletions src/ImageItem.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from "react";

import Thumbnail from "./Thumbnail";
import Viewer from "./Viewer";
import OpenWith from "./OpenWith";
import CopyButton from "./CopyButton";
import { loadOmeroMultiscales, open, getNgffAxes } from "./util";
import { openArray } from "zarr";

// DeckGL react component
export default function ImageItem({ source }) {
let config = { source };

const [layers, setLayers] = React.useState([]);
// const [layers, setLayers] = React.useState([]);

const [imgInfo, setImageInfo] = React.useState({});

Expand Down Expand Up @@ -45,30 +47,37 @@ export default function ImageItem({ source }) {

const axes = getNgffAxes(attrs.multiscales);

let layerData = await loadOmeroMultiscales(config, node, attrs);

let shape = layerData.loader[0]._data.meta.shape;
let chunks = layerData.loader[0]._data.meta.chunks;
console.log("layerData.loader[0]._data.meta", layerData.loader[0]._data.meta, chunks, chunks.join(","))

let selections = [];
layerData.channelsVisible.forEach((visible, chIndex) => {
if (visible) {
selections.push(
axes.map((axis, dim) => {
if (axis.type == "time") return 0;
if (axis.name == "z") return parseInt(shape[dim] / 2);
if (axis.name == "c") return chIndex;
return 0;
})
);
}
});
let path = attrs.multiscales[0].datasets[0].path;
const store = await openArray({ store: source + "/" + path, mode: "r" });

let shape = store.meta.shape;
let chunks = store.meta.chunks;

// let layerData = await loadOmeroMultiscales(config, node, attrs);
// let shape = ["TBD"] // layerData.loader[0]._data.meta.shape;
// let chunks = ["TBD"] // layerData.loader[0]._data.meta.chunks;
// console.log("layerData.loader[0]._data.meta", layerData.loader[0]._data.meta, chunks, chunks.join(","))

layerData.selections = selections;
// let selections = [];
// layerData.channelsVisible.forEach((visible, chIndex) => {
// if (visible) {
// selections.push(
// axes.map((axis, dim) => {
// if (axis.type == "time") return 0;
// if (axis.name == "z") return parseInt(shape[dim] / 2);
// if (axis.name == "c") return chIndex;
// return 0;
// })
// );
// }
// });

// layerData.selections = selections;

// setLayers([layerData]);

setLayers([layerData]);
setImageInfo({
attrs,
axes: axes.map((axis) => axis.name).join(""),
version: attrs.multiscales?.[0]?.version,
keywords,
Expand Down Expand Up @@ -115,7 +124,9 @@ export default function ImageItem({ source }) {
<td>{imgInfo?.keywords?.join(", ")}</td>
<td>
<div style={wrapperStyle}>
<Viewer layersData={layers} />
{imgInfo.attrs &&
<Thumbnail source={source} axes={imgInfo.axes} attrs={imgInfo.attrs} />
}
</div>
</td>
</tr>
Expand Down
99 changes: 99 additions & 0 deletions src/Thumbnail.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";

import { openArray, slice } from "zarr";
import {getNgffAxes, renderTo8bitArray, getMinMaxValues, getDefaultVisibilities, hexToRGB} from "./util";

export default function Thumbnail({ source, attrs }) {
// const [r, setChunk] = React.useState();

const [canvasSize, setCanvasSize] = React.useState({width: 100, height: 100});

const canvas = React.useRef();


React.useEffect(() => {
const fn = async function () {
let paths = attrs.multiscales[0].datasets.map(d => d.path);
let axes = getNgffAxes(attrs.multiscales).map(a => a.name);
console.log("paths", paths, "axes", axes);

let path = paths.at(-1);
const store = await openArray({ store: source + "/" + path, mode: "r" });

let chDim = axes.indexOf('c');

let shape = store.meta.shape;
let dims = shape.length;
let ch = store.meta.chunks;
console.log("shape", shape, ch);

let channel_count = shape[chDim];
let visibilities;
let colors;
if (attrs?.omero?.channels) {
console.log("omero channels", attrs?.omero?.channels)
visibilities = attrs.omero.channels.map(ch => ch.active);
colors = attrs.omero.channels.map(ch => hexToRGB(ch.color));
} else {
visibilities = getDefaultVisibilities(channel_count);
colors = getDefaultColors(channel_count, visibilities);
}
// filter for active channels
colors = colors.filter((col, idx) => visibilities[idx]);


let activeChannels = visibilities.reduce((prev, active, index) => {
if (active) prev.push(index);
return prev;
}, []);

console.log({visibilities, activeChannels, colors});

let promises = activeChannels.map(chIndex => {
let indecies = 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);
}
return 0;
});
console.log('ch indecies', chIndex, indecies);
return store.get(indecies);
});


let ndChunks = await Promise.all(promises);
console.log('ndChunks', ndChunks);




let minMaxValues = ndChunks.map(ch => getMinMaxValues(ch));

console.log("minMaxValues", minMaxValues);

let rbgData = renderTo8bitArray(ndChunks, minMaxValues, colors);

console.log("rbgData", rbgData);
// setChunk(data);
let width = shape.at(-1);
let height = shape.at(-2);
setCanvasSize({width, height });

const ctx = canvas.current.getContext('2d');
ctx.putImageData(new ImageData(rbgData, width, height), 0, 0);
};

fn();
}, []);

return <div>
<canvas style={{maxWidth: 100, maxHeight: 100}} ref={canvas} height={canvasSize.height} width={canvasSize.width} />
</div>;
}
59 changes: 59 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,62 @@ export async function loadOmeroMultiscales(config, zarrGroup, attrs) {
name: meta.name ?? name,
};
}


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];

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

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

const rgba = new Uint8ClampedArray(4 * height * width).fill(0);
let offset = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
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][x];
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 shape = chunk2d.shape;
const height = shape[0];
const width = shape[1];
const data = chunk2d.data;
let maxV = 0;
let minV = Infinity;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let rawValue = data[y][x];
maxV = Math.max(maxV, rawValue);
minV = Math.min(minV, rawValue);
}
}
return [minV, maxV];
}

0 comments on commit 53fface

Please sign in to comment.