diff --git a/src/ImageItem.jsx b/src/ImageItem.jsx index f34b14f..b76d50f 100644 --- a/src/ImageItem.jsx +++ b/src/ImageItem.jsx @@ -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({}); @@ -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, @@ -115,7 +124,9 @@ export default function ImageItem({ source }) { {imgInfo?.keywords?.join(", ")}
- + {imgInfo.attrs && + + }
diff --git a/src/Thumbnail.jsx b/src/Thumbnail.jsx new file mode 100644 index 0000000..9aca410 --- /dev/null +++ b/src/Thumbnail.jsx @@ -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
+ +
; +} diff --git a/src/util.js b/src/util.js index 8ac39f9..1e39901 100644 --- a/src/util.js +++ b/src/util.js @@ -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]; +} +