Skip to content
Open
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
112 changes: 82 additions & 30 deletions docs/components/Labels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ref } from "vue";

const imgSrc = ref("");
const labelSrc = ref("");
const colorBy = ref("colorpicker"); // or "auto" or "data"
const labelsColor = ref("#00ffff");
const colName = ref("Centroids_RAW_X");
const lutName = ref("green_fire_blue.lut");
const luts = ref([]);
Expand All @@ -23,17 +25,36 @@ const ROW_DATA = [[1442865, "8179A86D76", 1, 26.317552776815774, 33.152220936829


async function renderLabel() {
console.log("fillColor", fillColor.value);
// const colName = "Centroids_RAW_X";
const colIndex = COL_NAMES.indexOf(colName.value);
const keyIndex = COL_NAMES.indexOf("Cell_ID");
const minValue = ROW_DATA.reduce((min, row) => Math.min(min, row[colIndex]), Infinity);
const maxValue = ROW_DATA.reduce((max, row) => Math.max(max, row[colIndex]), -Infinity);
console.log("colorBy", colorBy.value);

// 256 rgb values
// always render the lut preview...
const lut = omezarr.getLutRgb(lutName.value);
let newLut = luts.value.find((lut) => lut.name === lutName.value);
lutImgSrc.value = newLut ? newLut.png : null;

let transparent = [0,0,0,0];
if (colorBy.value === "colorpicker") {
const r = parseInt(labelsColor.value.slice(1, 3), 16);
const g = parseInt(labelsColor.value.slice(3, 5), 16);
const b = parseInt(labelsColor.value.slice(5, 7), 16);
let cpLut = [transparent, [r, g, b]];
labelImage.setChannelLut(0, cpLut);
labelSrc.value = await labelImage.render({ targetSize: 300 });
return;
}

if (colorBy.value === "auto") {
let glasbeyRgb = omezarr.getLutRgb("glasbey.lut").slice(0, 50);
let lutWithBackground = [transparent, ...glasbeyRgb];
labelImage.setChannelLut(0, lutWithBackground);
labelSrc.value = await labelImage.render({ targetSize: 300 });
return;
}

const colIndex = COL_NAMES.indexOf(colName.value);
const keyIndex = COL_NAMES.indexOf("Cell_ID");
const minValue = ROW_DATA.reduce((min, row) => Math.min(min, row[colIndex]), Infinity);
const maxValue = ROW_DATA.reduce((max, row) => Math.max(max, row[colIndex]), -Infinity);

// crate a rendering Map of Cell_ID to RGBA color based on the Centroids_RAW_X value
const renderingMap = new Map();
Expand All @@ -56,6 +77,7 @@ async function renderLabel() {
displayMin.value = minValue;
displayMax.value = maxValue;

labelImage.setChannelLut(0, undefined); // reset any previous LUT
labelImage.setChannelColorMap(0, renderingMap);

// render...
Expand All @@ -74,7 +96,7 @@ onMounted(async () => {
imgSrc.value = await img.render({ targetSize: 300 });

let labelPaths = await img.getLabelsPaths();
labelImage = await omezarr.NgffImage.load(
labelImage = await omezarr.LabelsImage.load(
url + "labels/" + labelPaths[0]
);
labelImage.setChannelActive(0, true);
Expand All @@ -83,32 +105,59 @@ onMounted(async () => {
</script>

<template>
<select v-model="colName" @change="renderLabel">
<option v-for="label in COL_NAMES.slice(3)" :key="label" :value="label">
{{ label }}
</option>
</select>

<select v-model="lutName" @change="renderLabel">
<option v-for="lut in luts" :key="lut.name" :value="lut.name">
{{lut.name}}
</option>
</select>

<div style="display: flex; flex-direction: row; gap: 7px; align-items: center; margin: 0 10px">
<input type="checkbox" v-model="fillColorEnabled" @change="renderLabel" />
Fill color:
</input>
<input type="color" v-model="fillColor" @input="renderLabel" />
<!-- radio buttons for colorBy attribute -->
Color labels by:
<hr>
<div>
<input type="radio" v-model="colorBy" id="colorpicker" name="color_by" value="colorpicker" @change="renderLabel"/>
<label for="colorpicker">Single color:</label>
<input title="Color picker" type="color" v-model="labelsColor" @input="renderLabel" style="position: relative; top: 4px; margin-left: 10px;" />
<code :class="$style.code">img.setChannelLut(0, [transparent, cyan]);</code>
</div>
<hr>
<div>
<input type="radio" v-model="colorBy" id="auto" name="color_by" value="auto" @change="renderLabel"/>
<label for="auto">Glasbey LUT</label>
<code :class="$style.code">img.setChannelLut(0, [transparent, ...glasbeyRgb]);</code>
</div>
<hr>
<div>
<input type="radio" v-model="colorBy" id="data" name="color_by" value="data" @change="renderLabel"/>
<label for="data">ColorMap:</label>
<code :class="$style.code">img.setChannelColorMap(0, colorMap);</code>
</div>

<div style="display: flex; flex-direction: row; gap: 7px; align-items: center; margin: 0 10px">
<!-- Show a colorbar with min and max values -->
<div>{{ displayMin.toFixed(2) }}</div>
<img :class="$style.lutImg" :src="lutImgSrc"/>
<div>{{ displayMax.toFixed(2) }}</div>
<!-- show 50% opacity unless colorBy is 'data' -->
<div style="margin-left: 100px" :style="{ opacity: colorBy === 'data' ? 1 : 0.5 }">
<select v-model="colName" @change="renderLabel" :disabled="colorBy !== 'data'">
<option v-for="label in COL_NAMES.slice(3)" :key="label" :value="label">
{{ label }}
</option>
</select>

<select v-model="lutName" @change="renderLabel" :disabled="colorBy !== 'data'">
<option v-for="lut in luts" :key="lut.name" :value="lut.name">
{{lut.name}}
</option>
</select>

<div style="display: flex; flex-direction: row; gap: 7px; align-items: center; margin: 0 10px">
<input type="checkbox" v-model="fillColorEnabled" @change="renderLabel" :disabled="colorBy !== 'data'" />
Fill color:
</input>
<input type="color" v-model="fillColor" @input="renderLabel" :disabled="colorBy !== 'data'" />
</div>

<div style="display: flex; flex-direction: row; gap: 7px; align-items: center; margin: 0 10px">
<!-- Show a colorbar with min and max values -->
<div>{{ displayMin.toFixed(2) }}</div>
<img :class="$style.lutImg" :src="lutImgSrc"/>
<div>{{ displayMax.toFixed(2) }}</div>
</div>
</div>

<hr/>

<a
:href="
VURL +
Expand Down Expand Up @@ -137,6 +186,9 @@ select {
border-radius: 5px;
appearance: auto;
}
.code {
margin-left: 30px;
}
.lutImg {
width: 256px;
height: 20px;
Expand Down
49 changes: 44 additions & 5 deletions docs/labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

We can use `ngffImage.getLabelsPaths()` to list any labels groups below a multiscales image.

These can then be opened as images and rendered as normal.

Here we use `ngffImage.setChannelColorMap(0, colorMap)` to map label values to `r,g,b` colors.
These groups can then be opened as images and rendered as normal. Here we use a
`LabelsImage` which will map pixel values directly to LUT indices when rendering LUTs (instead
of scaling by start/end values as for a regular Image).

```js
// load and render the parent image
Expand All @@ -16,22 +16,61 @@ document.getElementById("img").src = labelSrc;

// find labels and open the first label image
let labelPaths = await img.getLabelsPaths();
let labelImage = await omezarr.NgffImage.load(url + "labels/" + labelPaths[0]);
let labelImage = await omezarr.LabelsImage.load(url + "labels/" + labelPaths[0]);
```

## LUTs

We can render by `LUTs` (lookup-tables) which are lists of colors; each color is
defined by `[red, green, blue]`, or `[red, green, blue, alpha]` values.

When a `LabelsImage` renders with a `LUT`, the first item of the `LUT` list will be used for background (where `pixel value = 0`).
The remaining colors will be used for other label values, repeating as necessary.

```js
// for the "Color Picker" option (in the demo below) we only need to specify the background
// and a single additional color for all the labels:
let transparent = [0, 0, 0, 0];
let cyan = [0, 255, 255];
let lut = [transparent, cyan];

// OR, for the "Auto" option, we can use the "glasbey" LUT...
const glasbeyRgb = omezarr.getLutRgb("glasbey.lut");
// ...and add the first value as transparent
let lut = [transparent, ...glasbeyRgb];

// In either case, we can apply the LUT to channel 0 and render:
labelImage.setChannelLut(0, lut);
let labelSrc = await labelImage.render({ targetSize: 300 });
document.getElementById("labelImg").src = labelSrc;
```

## Color Map

If we want to precisely map pixel values to colors, we can use a `colorMap`, which is
a `Map` of `integer: [r, g, b]` (or `[r, g, b, a]`).

If we have a table of data, with a row for each label, we can build a `colorMap` for
a chosen table column, e.g. `Centroids_RAW_X`.

```js
// Using a LUT (list of [r, g, b] values)...
const lut = omezarr.getLutRgb("green_fire_blue.lut");
// and a table of data for each label value, we can
// create a colorMap for rendering the labels...
const colorMap = new Map();
ROW_DATA.forEach((row) => {
TABLE_DATA.forEach((row) => {
const labelValue = row["Label_Value"];
const paramValue = row["Centroids_RAW_X"];
// minValue and maxValue are determined for the chosen column (not shown)
const fraction = (paramValue - minValue) / (maxValue - minValue);
const lutIndex = Math.round(fraction * (lut.length - 1));
const rgb = lut[lutIndex];
colorMap.set(labelValue, rgb);
});
// We can use omezarr.FILL_VALUE_KEY to specify fillColor (transparent by default)
// This will be used for background (since 0 is not in our colorMap) and for any
// other label values that are missing from our colorMap
if (enableFill) {
colorMap.set(omezarr.FILL_VALUE_KEY, fillRGBA);
}
Expand Down
35 changes: 20 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -166,32 +166,37 @@ <h2>Look-up tables available to choose</h2>
document.getElementById("labels").innerHTML += html;

let labelPaths = await img.getLabelsPaths();
let labelImage = await omezarr.NgffImage.load(imgUrl + "labels/" + labelPaths[0]);
labelImage.setChannelActive(0, true);
// we use `LabelsImage` since it won't scale values when rendering...
let labelImage = await omezarr.LabelsImage.load(imgUrl + "labels/" + labelPaths[0]);

let glasbeyInvertedMap = new Map();
// We only want to see labels 50-60
for (let i = 20; i < 90; i++) {
glasbeyInvertedMap.set(i, omezarr.luts.GLASBEY_INVERTED[i]);
// We only want to see first 100 labels
for (let i = 1; i < 101; i++) {
// label 1 is first color LUT[0], label 2 is LUT[1] etc. (label 0 is background)
glasbeyInvertedMap.set(i, omezarr.luts.GLASBEY_INVERTED[i - 1]);
}
// set label 0 (background) to grey with some transparency
glasbeyInvertedMap.set(0, [100, 100, 100, 255]);
glasbeyInvertedMap.set(0, [100, 100, 100, 100]);
// set missing values to Yellow
glasbeyInvertedMap.set(omezarr.FILL_VALUE_KEY, [255, 255, 0, 100]);
labelImage.setChannelColorMap(0, glasbeyInvertedMap);
let labelSrc = await labelImage.render({targetSize: 512});
html = `<a href="${imgUrl}labels/${labelPaths[0]}"><img class="checquers" src="${labelSrc}" /></a>`;
document.getElementById("labels").innerHTML += html;

// let labelSrc = await omezarr.createRgbDataUrl(labelRgba.data, labelRgba.width);
let labelSrc = await labelImage.render({targetSize: 300});
// transparent (background), then glasbey_inverted for labels
// We use just 100 colors from the LUT; values over 100 are moduloed back to the start of the LUT
// the FIRST 100 labels will be identical to the colorMap example above...
labelImage.setChannelLut(0, [[255, 0, 0, 0], ...omezarr.luts.GLASBEY_INVERTED.slice(0, 100)]);
labelSrc = await labelImage.render({targetSize: 512});
html = `<a href="${imgUrl}labels/${labelPaths[0]}"><img class="checquers" src="${labelSrc}" /></a>`;
document.getElementById("labels").innerHTML += html;

let overlayHtml = `<div style="position: relative; display: inline-block;">
<a href="${imgUrl}">
<img src="${imsSrc}" />
<img src="${labelSrc}" class="fadeInOut" style="position: absolute; top: 0; left: 0;" />
</a>
</div>`;
document.getElementById("labels").innerHTML += overlayHtml;
// Alternatively, we can set ALL labels to the same color:
labelImage.setChannelLut(0, [[255, 0, 0, 0], [0, 255, 255]]);
labelSrc = await labelImage.render({targetSize: 512});
html = `<a href="${imgUrl}labels/${labelPaths[0]}"><img class="checquers" src="${labelSrc}" /></a>`;
document.getElementById("labels").innerHTML += html;
}

import * as zarr from "https://cdn.jsdelivr.net/npm/zarrita@next/+esm";
Expand Down
Loading
Loading