Skip to content
Draft
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
99 changes: 99 additions & 0 deletions sample_figures/zarr_collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// we want to convert exisiting figure.json to ome-zarr collection format
// the figure json will have "panels" and "figureName" keys
{
"ome": {
"version": "0.x",
"type": "collection",
// the "name" of the collection is taken from the figure's "figureName" key
"name": "My figure",
// each panel in the figure will be represented as a node in this collection
"nodes": [
{
// the panel["zarr"] json contains info we use to create the multiscale nodes...
// if panel["zarr"]["version"] is NOT "0.5" then we IGNORE this panel (only zarr v3 supported)
"type": "multiscale",
// the panel 'imageId' is the zarr URL. We get the name from the ...url/image_name.zarr/ path segment
// BUT the name MUST be unique within the collection, even if the same image is used in multiple panels
// So we add a unique identifier to the name, e.g., "my_image_name_1"
"name": "my_image_name_1",
// panel["zarr"]["multiscales"][0]["datasets"] list maps to "nodes"
"nodes": [
{
// each node has name "s0", "s1", etc. corresponding to the dataset it represents
"name": "s0",
"type": "singlescale",
"path": {
"type": "zarr",
// Path is the URL, joined to the dataset "path". e.g. if "path": "0", the full path is "https://example.com/image.ome.zarr/0"
"path": "https://example.com/image.ome.zarr/0"
},
"attributes": {
// each dataset["coordinateTransformations"] maps to "coordinateTransformations" in the node's attributes
// with addition of "input" with "id" of the dataset name and "output" with "id" of the coordinate system
"coordinateTransformations": [
{
"type": "scale",
"scale": [1, 1, 1],
"input": {"id": "s0"},
"output": {"id": "my_image_name_1_physical"}
}
]
}
},
{
"name": "s1",
"type": "singlescale",
"path": {
"type": "zarr",
"path": "https://example.com/image.ome.zarr/s1"
},
"attributes": {
"coordinateTransformations": [
{
"type": "scale",
"scale": [1, 2, 2],
"input": {"id": "s1"},
"output": {"id": "my_image_name_1_physical"}
}
]
}
}
],
"attributes": {
"coordinateSystems": [
{
// id MUST be unique within the collection. Let's use the "node_name_physical" convention
"id": "my_image_name_1_physical",
// the "axes" are defined in panel["zarr"]["multiscales"][0]["axes"]
"axes": [
{
"name": "x",
"unit": "micrometer"
},
{
"name": "y",
"unit": "micrometer"
},
{
"name": "z",
"unit": "micrometer"
}
]
}
],
"figure:panel": {
// here we dump all the other panel attributes apart from "zarr"

}
}
}
],
// ALL top-level attributes for the collection can go here. Everything except the "panels" key should be included here.
"attributes": {
"figure:figure": {
"figureName": "my_figure_name"
// etc...
}
}
}
}
65 changes: 65 additions & 0 deletions sample_figures/zarr_scene.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

{
"ome": {
"version": "0.x",
"type": "collection",
"name": "My figure",

"attributes": {
"scene": {
"coordinateSystems": [
{
"id": "world",
"axes": [...]
}
],
"coordinateTransformations": [
{
"type": "translation",
"translation": [0, 0, 100],
"input": {
"path": {
"type": "zarr",
"path": "./tile_0.zarr"
},
"id": "physical"
}, // references coordinate system "physical" defined in tile_0
"output": {"id": "world"} // references coordinate system "world" defined in same node
},
{
"type": "translation",
"translation": [100, 0, 0],
"input": {
"path": {
"type": "zarr",
"path": "./tile_1.zarr"
},
"id": "physical"
}, // references coordinate system "physical" defined in tile_1
"output": {"id": "world"} // references coordinate system "world" defined in same node
}
]
}
},
"nodes": [
{
"type": "multiscale",
"id": "tile_0",
"name": "Tile 0",
"path": {
"type": "zarr",
"path": "./tile_0.zarr"
}
},
{
"type": "multiscale",
"id": "tile_1",
"name": "Tile 1",
"path": {
"type": "zarr",
"path": "./tile_1.zarr"
}
}
]
}
}
45 changes: 45 additions & 0 deletions src/html/dialogs/saveAsOmeZarrModal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div
class="modal"
id="saveAsOmeZarrModal"
tabindex="-1"
role="dialog"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Save as OME-Zarr?</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body" style="padding: 25px 25px 35px 25px">
<p>
Would you like to save this figure as an OME-Zarr Collection? <br>
Experimental RFC-8 spec (subject to change). <br>
NB: Only images in OME-Zarr v0.5 (zarr v3) are compatible.
Other images will not be included.
</p>
</div>
<div class="modal-footer" style="margin-top: 0">
<button
type="button"
class="btn btn-default save_as_ome_zarr_no"
data-bs-dismiss="modal"
>
Save as Normal
</button>
<button
type="button"
class="btn btn-primary save_as_ome_zarr_yes"
data-bs-dismiss="modal"
>
Save as OME-Zarr
</button>
</div>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,7 @@ <h5>Example Label</h5>
</div>

<load src="html/dialogs/scriptManagerDialog.html" />
<load src="html/dialogs/saveAsOmeZarrModal.html" />

<!-- -- END OF MODAL DIALOGS -- -->

Expand Down
19 changes: 15 additions & 4 deletions src/js/models/figure_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
getJsonWithCredentials,
saveFigureToStorage,
normalizeZProjectionBounds} from "../views/util";
import { loadZarrForPanel } from "./zarr_utils";
import { loadZarrForPanel, figure_to_ome_zarr_collection } from "./zarr_utils";

// Version of the json file we're saving.
// This only needs to increment when we make breaking changes (not linked to release versions.)
Expand Down Expand Up @@ -341,6 +341,11 @@
return json;
},

to_ome_zarr_collection: function() {
var figureJSON = this.figure_toJSON();
return figure_to_ome_zarr_collection(figureJSON);
},

figure_toJSON: function() {
// Turn panels into json
var p_json = [],
Expand Down Expand Up @@ -415,9 +420,15 @@
if (options.figureName) {
figureJSON.figureName = options.figureName;
}
let fileName = figureJSON.figureName || "figure";
let jsonText = JSON.stringify(this.figure_toJSON(), null, 2);
downloadAsFile(jsonText, "application/json", fileName + ".json");
let fileName = (figureJSON.figureName || "figure") + ".json";
let jsonText;
if (options.saveAsOmeZarr) {
fileName = "zarr.json";
jsonText = JSON.stringify(this.to_ome_zarr_collection(), null, 2);
} else {
jsonText = JSON.stringify(this.figure_toJSON(), null, 2);
}
downloadAsFile(jsonText, "application/json", fileName);
this.set({unsaved: false});
},

Expand Down
108 changes: 108 additions & 0 deletions src/js/models/zarr_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,114 @@ function dtypeToPixelsType(dtype) {
return dt;
};

// last path segment of a zarr URL, with any trailing ".zarr" extension removed
function getZarrUrlName(url) {
let u = url.endsWith("/") ? url.slice(0, -1) : url;
let name = u.split("/").pop();
if (name.endsWith(".zarr")) {
name = name.slice(0, -".zarr".length);
}
return name;
}

// Converts a figure.json (as returned by FigureModel.figure_toJSON()) into
// an OME-Zarr "collection" JSON document. See sample_figures/zarr_collection.json
// for the target format and mapping rules.
export function figure_to_ome_zarr_collection(figureJson) {
let nodes = [];
let nodeIndex = 0;

(figureJson.panels || []).forEach((panel) => {
let zarrAttrs = panel.zarr;
// Only OME-Zarr v0.5 (zarr v3) multiscale metadata is supported
if (!zarrAttrs || zarrAttrs.version !== "0.5") {
return;
}
let multiscale = zarrAttrs.multiscales?.[0];
if (!multiscale) {
return;
}

// append an incrementing index to guarantee a unique node name,
// even if the same image URL is used in multiple panels
nodeIndex++;
let baseUrl = panel.imageId;
let nodeName = `${getZarrUrlName(baseUrl)}_${nodeIndex}`;
let coordSystemId = `${nodeName}_physical`;

let datasetNodes = (multiscale.datasets || []).map((dataset, idx) => {
let datasetName = `s${idx}`;
let transforms = (dataset.coordinateTransformations || []).map((ct) => ({
...ct,
input: {id: datasetName},
output: {id: coordSystemId},
}));
return {
name: datasetName,
type: "singlescale",
path: {
type: "zarr",
path: `${baseUrl}/${dataset.path}`,
},
attributes: {
coordinateTransformations: transforms,
},
};
});

let axes = (multiscale.axes || []).map((axis) => {
let a = {name: axis.name};
if (axis.unit) {
a.unit = axis.unit;
}
return a;
});

// dump all other panel attributes apart from 'zarr'
let panelAttrs = {};
Object.keys(panel).forEach((key) => {
if (key !== "zarr") {
panelAttrs[key] = panel[key];
}
});

nodes.push({
type: "multiscale",
name: nodeName,
nodes: datasetNodes,
attributes: {
coordinateSystems: [
{
id: coordSystemId,
axes: axes,
},
],
"figure:panel": panelAttrs,
},
});
});

// all top-level figure attributes, apart from 'panels', go under figure:figure
let figureAttrs = {};
Object.keys(figureJson).forEach((key) => {
if (key !== "panels") {
figureAttrs[key] = figureJson[key];
}
});

return {
ome: {
version: "0.x",
type: "collection",
name: figureJson.figureName || "Untitled Figure",
nodes: nodes,
attributes: {
"figure:figure": figureAttrs,
},
},
};
}

export async function renderZarrToSrc(source, attrs, theZ, theT, channels, rect, targetSize=500) {
let paths = attrs.multiscales[0].datasets.map((d) => d.path);
// for v0.3 each axes is a string, for v0.4+ it is an object
Expand Down
10 changes: 8 additions & 2 deletions src/js/views/figure_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
showModal,
hideModals,
hideModal,
showSaveAsOmeZarrDialog,
updateRoiIds} from "./util";
const RELEASE_VERSION = import.meta.env.VITE_VERSION;

Expand Down Expand Up @@ -596,9 +597,14 @@
options.fileId = fileId;
this.model.save_to_OMERO(options);
} else {
this.save_as(options);
var self = this;
showSaveAsOmeZarrDialog(function(saveAsOmeZarr) {
if (saveAsOmeZarr) {
options.saveAsOmeZarr = true;
}
self.save_as(options);
});
}

},

save_as_event: function(event) {
Expand Down
Loading