Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display collection using Plate layout #124

Closed
wants to merge 3 commits into from
Closed
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
17 changes: 3 additions & 14 deletions src/io.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DTYPE_VALUES, ImageLayer, MultiscaleImageLayer, ZarrPixelSource } from '@hms-dbmi/viv';
import { Group as ZarrGroup, openGroup, ZarrArray } from 'zarr';
import GridLayer from './gridLayer';
import { loadOmeroMultiscales, loadPlate, loadWell } from './ome';
import { loadCollection, loadOmeroMultiscales, loadWell } from './ome';
import type {
ImageLayerConfig,
LayerState,
Expand Down Expand Up @@ -111,8 +111,8 @@ export async function createSourceData(config: ImageLayerConfig): Promise<Source
if (node instanceof ZarrGroup) {
const attrs = (await node.attrs.asObject()) as Ome.Attrs;

if ('plate' in attrs) {
return loadPlate(config, node, attrs.plate);
if ('collection' in attrs || 'plate' in attrs) {
return loadCollection(config, node, attrs);
}

if ('well' in attrs) {
Expand All @@ -123,17 +123,6 @@ export async function createSourceData(config: ImageLayerConfig): Promise<Source
return loadOmeroMultiscales(config, node, attrs);
}

if (Object.keys(attrs).length === 0 && node.path) {
// No rootAttrs in this group.
// if url is to a plate/acquisition/ check parent dir for 'plate' zattrs
const parentPath = node.path.slice(0, node.path.lastIndexOf('/'));
const parent = await openGroup(node.store, parentPath);
const parentAttrs = (await parent.attrs.asObject()) as Ome.Attrs;
if ('plate' in parentAttrs) {
return loadPlate(config, parent, parentAttrs.plate);
}
}

if (!('multiscales' in attrs)) {
throw Error('Group is missing multiscales specification.');
}
Expand Down
103 changes: 75 additions & 28 deletions src/ome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,42 +107,89 @@ export async function loadWell(config: ImageLayerConfig, grp: ZarrGroup, wellAtt
return sourceData;
}

export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateAttrs: Ome.Plate): Promise<SourceData> {
if (!('columns' in plateAttrs) || !('rows' in plateAttrs)) {
throw Error(`Plate .zattrs missing columns or rows`);
async function getImagePaths(grp: ZarrGroup, omeAttrs: Ome.Attrs): Promise<string[]> {
if ('collection' in omeAttrs) {
return Object.keys(omeAttrs.collection.images);
} else if ('plate' in omeAttrs) {
// Load each Well to get a path/to/image/
const wellPaths = omeAttrs.plate.wells.map((well) => well.path);
async function getImgPath(wellPath: string) {
const wellAttrs = await getAttrsOnly<{ well: Ome.Well }>(grp, wellPath);
// Fields are by index and we assume at least 1 per Well
return join(wellPath, wellAttrs.well.images[0].path);
}
const imgPaths = await Promise.all(wellPaths.map(getImgPath));
return imgPaths;
} else {
return [];
}
}

const rows = plateAttrs.rows.map((row) => row.name);
const columns = plateAttrs.columns.map((row) => row.name);
export async function loadCollection(
config: ImageLayerConfig,
grp: ZarrGroup,
omeAttrs: Ome.Attrs
): Promise<SourceData> {
const imagePaths = await getImagePaths(grp, omeAttrs);

let displayName = 'Collection';
let rows: string[];
let columns: string[];
let colCount: number;
let rowCount: number;
if ('plate' in omeAttrs) {
const plateAttrs = omeAttrs.plate;
if (!('columns' in plateAttrs) || !('rows' in plateAttrs)) {
throw Error(`Plate .zattrs missing columns or rows`);
}
rows = plateAttrs.rows.map((row) => row.name);
columns = plateAttrs.columns.map((row) => row.name);
displayName = plateAttrs.name || 'Collection';
colCount = columns.length;
rowCount = rows.length;
} else {
const imgCount = imagePaths.length;
colCount = Math.ceil(Math.sqrt(imgCount));
rowCount = Math.ceil(imgCount / colCount);
}

// Fields are by index and we assume at least 1 per Well
const wellPaths = plateAttrs.wells.map((well) => well.path);
function getImgSource(source: string, row: number, column: number) {
if (rows && columns) {
return join(source, rows[row], columns[column]);
} else {
return join(source, imagePaths[row * colCount + column]);
}
}

// Use first image as proxy for others.
const wellAttrs = await getAttrsOnly<{ well: Ome.Well }>(grp, wellPaths[0]);
if (!('well' in wellAttrs)) {
throw Error('Path for image is not valid, not a well.');
function getGridCoord(imgPath: string) {
let row, col, name;
if (rows && columns) {
const [rowName, colName] = imgPath.split('/');
row = rows.indexOf(rowName);
col = columns.indexOf(colName);
name = `${rowName}${colName}`;
} else {
const imgIndex = imagePaths?.indexOf(imgPath);
row = Math.floor(imgIndex / colCount);
col = imgIndex - row * colCount;
name = imgPath;
}
return { row, col, name };
}

const imgPath = wellAttrs.well.images[0].path;
const imgAttrs = (await grp.getItem(join(wellPaths[0], imgPath)).then((g) => g.attrs.asObject())) as Ome.Attrs;
const imgPath = imagePaths[0];
const imgAttrs = (await grp.getItem(imgPath).then((g) => g.attrs.asObject())) as Ome.Attrs;
if (!('omero' in imgAttrs)) {
throw Error('Path for image is not valid.');
}
// Lowest resolution is the 'path' of the last 'dataset' from the first multiscales
const { datasets } = imgAttrs.multiscales[0];
const resolution = datasets[datasets.length - 1].path;

async function getImgPath(wellPath: string) {
const wellAttrs = await getAttrsOnly<{ well: Ome.Well }>(grp, wellPath);
return join(wellPath, wellAttrs.well.images[0].path);
}
const wellImagePaths = await Promise.all(wellPaths.map(getImgPath));

// Create loader for every Well. Some loaders may be undefined if Wells are missing.
const mapper = ([key, path]: string[]) => grp.getItem(path).then((arr) => [key, arr]) as Promise<[string, ZarrArray]>;
const promises = await pMap(
wellImagePaths.map((p) => [p, join(p, resolution)]),
imagePaths.map((p) => [p, join(p, resolution)]),
mapper,
{ concurrency: 10 }
);
Expand All @@ -151,11 +198,11 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
const meta = parseOmeroMeta(imgAttrs.omero, axis_labels);
const tileSize = guessTileSize(data[0][1]);
const loaders = data.map((d) => {
const [row, col] = d[0].split('/');
const coord = getGridCoord(d[0]);
return {
name: `${row}${col}`,
row: rows.indexOf(row),
col: columns.indexOf(col),
name: coord.name,
row: coord.row,
col: coord.col,
loader: new ZarrPixelSource(d[1], axis_labels, tileSize),
};
});
Expand All @@ -172,9 +219,9 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
colormap: config.colormap ?? '',
opacity: config.opacity ?? 1,
},
name: plateAttrs.name || 'Plate',
rows: rows.length,
columns: columns.length,
name: displayName,
rows: rowCount,
columns: colCount,
};
// Us onClick from image config or Open Well in new window
sourceData.onClick = (info: any) => {
Expand All @@ -185,7 +232,7 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
const { row, column } = gridCoord;
let imgSource = undefined;
if (typeof config.source === 'string' && grp.path && !isNaN(row) && !isNaN(column)) {
imgSource = join(config.source, rows[row], columns[column]);
imgSource = getImgSource(config.source, row, column);
}
if (config.onClick) {
delete info.layer;
Expand Down
5 changes: 5 additions & 0 deletions types/ome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,17 @@ declare module Ome {
wells: { path: string }[];
}

interface Collection {
images: {};
}

interface Well {
images: { path: string; acquisition?: number }[];
version: Version;
}

type Attrs =
| { collection: Collection }
| { multiscales: Multiscale[] }
| { omero: Omero; multiscales: Multiscale[] }
| { plate: Plate }
Expand Down