Skip to content

Commit

Permalink
Merge pull request #11 from NFDI4BIOIMAGE/dev
Browse files Browse the repository at this point in the history
Support OMERO ROIs as cell segmentation.
  • Loading branch information
MicheleBortol authored Oct 23, 2024
2 parents bf67a3f + 91ae0f1 commit 1f6cb54
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 25 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ Clicking the link will open a new tab with the viewer displaying the selected co

The form allows the user to automatically generate a config file with the selected parameters. After clicking the `Generate Config` button
a config file is generated and attached to the dataset/image. The viewer displaying the autogenerated configuration will open in a new tab.
![Form-image](https://github.com/user-attachments/assets/94727eb5-486c-4d9f-beae-b7559224ddfd)
![Form-image](https://github.com/user-attachments/assets/0c6513e4-1f8f-40b0-b919-e1c9d57c2190)


#### Open-with:
Right click on a `dataset` or an `images` in the left-panel and select `open-with` -> `Vitessce`.
This will open the vitessce viewer in a new tab using the first configuration file attachement.
Expand Down Expand Up @@ -95,6 +94,7 @@ The following fields are available:
- `Config file name` (required, "VitessceConfig-YYYY.MM.DD_HHMMSS.json"): Name of the config file to attach, a ".json" extension is added if missing.
- `Images` (required): OMERO Image(s) to view, assumes the same pixel size for all images.
- `Segmentation` (optional, `None`): Label image to overlay on the image, pixel values correspond to cell identities.
- `ROIs` (optional, `False`): Use the ROIs from OMERO as a cell segmentation. Assumes 1 polygon shape per ROI, whose text value is the cell identity.
- `Cell identities` (optional, `None`): `.csv` file with at least 2 columns: `Cell id column` and `Label column` defined in the 2 fields below.
- `Cell id column` (optional, "cell_id"): Name of the `Cell id column` used in `Cell identities`, `Expression`, `Embeddings`.
- `Label column` (optional, "label"): Name of the `Label` used in `Cell identities`.
Expand All @@ -112,6 +112,7 @@ The following fields are available:
- `Status` (required , `False`): Adds a status panel to display info on the selected cell.
- `Description` (required , `False`): Adds a description panel to display info on the dataset/image (taken from the description metadata field from OMERO).

The `ROIs` and `Segmentation` fields are mutually exclusive, when the ROI field is selected the segmentation image is not considered.
The `Expression` and `Cell identities` files are required to show the histograms.
The `Embeddings` file is necessary to show the cells in a scatterplot.
The `Molecules` file is used to overlay molecules on the image. All molecules are displayed and selecting by gene is not yet possible.
Expand All @@ -125,19 +126,24 @@ For how to create a custom config file see:
- [VItessce Docs](http://vitessce.io/docs/view-config-json/)
- [Vitessce Examples](http://vitessce.io/examples/)

#### Serving the images / data /metadata
#### Serving the images / rois / data / metadata
Images and data can be served through:
- [omero-web-zarr](https://pypi.org/project/omero-web-zarr/): OME-NGFF images only.
- [omero-openlink](https://github.com/sukunis/OMERO.openlink): all images and file attachements.
- `webclient/annotation/` endpoint: only for Annotations (useful for file attachments).
- `omero_vitessce/vitessce_json_rois/ID1,ID2,...` endpoint where `ID1,ID2,...` is a comma-separated list of image ids.

Example showing ROIs from OMERO displayed in the omero-vitessce viewer window:
![image](https://github.com/user-attachments/assets/223072d6-9aa7-4b96-a199-65bbab863c1b)


Development
=======================

## Sources

The main sources this project relies on are:
- omero-vitessce from Will Moore: https://github.com/will-moore/omero-vitessce
- omero-vitessce from Will Moore for handling ROIs: https://github.com/will-moore/omero-vitessce
- cookiecutter-omero-webapp: https://github.com/ome/cookiecutter-omero-webapp
- `react_webapp` from omero-web-apps-examples: https://github.com/ome/omero-web-apps-examples/tree/master/react-webapp
- Vitessce python package used for generating config files http://python-docs.vitessce.io/
Expand Down
9 changes: 9 additions & 0 deletions omero_vitessce/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ConfigForm(forms.Form):
images_help = "OMERO Image(s) to view, assumes identical pixel size."
segmentation_help = "Label image to overlay on the image, \
pixel values should correspond to cell identities."
rois_help = "Use image ROIs as segmentation."
cell_identities_help = ".csv file with at least 2 columns: \
Cell id column and Label column defined in the 2 fields below."
cell_id_help = "Name of the Cell id column used in \
Expand Down Expand Up @@ -78,6 +79,14 @@ def __init__(self, file_names, file_urls,
self.fields["segmentation"] = forms.ChoiceField(
choices=self.image_choices, required=False,
help_text=ConfigForm.segmentation_help)
# Segmentation and rois are mutually exclusive,
# Setting rois to true disables the segmentation
self.fields["rois"] = forms.BooleanField(
initial=False, required=False,
help_text=ConfigForm.rois_help)
self.fields["rois"].widget.attrs = \
{"onclick":
"javascript:toggleDisabled('id_rois', 'id_segmentation', true);"}

self.fields["cell_identities"] = forms.ChoiceField(
choices=self.text_choices, required=False,
Expand Down
24 changes: 24 additions & 0 deletions omero_vitessce/templates/omero_vitessce/vitessce_open_with.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
</div></th>
<th style="text-align:left;">{{ form.segmentation }}</th>
</tr>
<tr style="height:2vh">
<th><div class="tooltip">{{ form.rois.label_tag }}
<span class="tooltiptext">{{ form.rois.help_text }}</span>
</div></th>
<th style="text-align:left;">{{ form.rois }}</th>
</tr>
<tr style="height:2vh">
<th><div class="tooltip">{{ form.cell_identities.label_tag }}
<span class="tooltiptext">{{ form.cell_identities.help_text }}</span>
Expand Down Expand Up @@ -183,6 +189,24 @@
</table>
<input type = "submit" value = "Generate Config">
</form>
<script>
function toggleDisabled(divId1, divId2, value) {
const field_to_check = document.getElementById(divId1);
const field_to_disable = document.getElementById(divId2);
if (field_to_check.value === value || field_to_check.checked == value) {
field_to_disable.disabled = true;
}
else {
field_to_disable.disabled = false;
}
}
</script>
<script>
// Ensure checkbox is unchecked when the page loads
window.onload = function() {
document.getElementById('id_rois').checked = false;
};
</script>
{% else %}
<p style="font-size:medium%"> Autogenerating config files requires the
<a href=https://github.com/ome/omero-web-zarr target="_blank">omero-web-zarr</a>
Expand Down
18 changes: 18 additions & 0 deletions omero_vitessce/templates/omero_vitessce/vitessce_panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
</div></th>
<th style="text-align:left;">{{ form.segmentation }}</th>
</tr>
<tr>
<th><div class="tooltip">{{ form.rois.label_tag }}
<span class="tooltiptext">{{ form.rois.help_text }}</span>
</div></th>
<th style="text-align:left;">{{ form.rois }}</th>
</tr>
<tr>
<th><div class="tooltip">{{ form.cell_identities.label_tag }}
<span class="tooltiptext">{{ form.cell_identities.help_text }}</span>
Expand Down Expand Up @@ -183,6 +189,18 @@
</table>
<input type = "submit" value = "Generate Config">
</form>
<script>
function toggleDisabled(divId1, divId2, value) {
const field_to_check = document.getElementById(divId1);
const field_to_disable = document.getElementById(divId2);
if (field_to_check.value === value || field_to_check.checked == value) {
field_to_disable.disabled = true;
}
else {
field_to_disable.disabled = false;
}
}
</script>
{% else %}
<p> Autogenerating config files requires the
<a href=https://github.com/ome/omero-web-zarr target="_blank">omero-web-zarr</a>
Expand Down
4 changes: 4 additions & 0 deletions omero_vitessce/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
re_path(r"^generate_config$", views.vitessce_index,
name="generate_config"),

# json response with ROIs
re_path(r"vitessce_json_rois/(?P<img_ids>[0-9,]+)",
views.vitessce_json_rois, name="vitessce_rois"),

# Right panel plugin
re_path(r"(?P<obj_type>[a-z]+)/(?P<obj_id>[0-9]+)",
views.vitessce_panel, name='vitessce_tab'),
Expand Down
109 changes: 100 additions & 9 deletions omero_vitessce/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
from pathlib import Path
from shapely.geometry import Polygon
from omero_marshal import get_encoder

from omero.util.temp_files import create_path

Expand All @@ -18,7 +20,7 @@ def get_files_images(obj_type, obj_id, conn):
""" Gets all the non config files attached to an object,
and images if the object is a dataset,
and returns a list of file names and a list of urls
for the files and eventually the images.
for the files and eventually the images, plus the image ids.
"""
obj = conn.getObject(obj_type, obj_id)
file_names = [
Expand All @@ -29,17 +31,19 @@ def get_files_images(obj_type, obj_id, conn):
if i.getFileName().endswith(".csv")]
file_urls = [i.getId() for i in file_names]
file_names = [i.getFileName() for i in file_names]
file_urls = [SERVER + "/webclient/annotation/" + str(i) for i in file_urls]
file_urls = [build_attachement_url(i) for i in file_urls]

if obj_type == "dataset":
imgs = list(obj.listChildren())
img_urls = [build_zarr_image_url(i.getId()) for i in imgs]
img_ids = [i.getId() for i in imgs]
img_urls = [build_zarr_image_url(i) for i in img_ids]
img_names = [i.getName() for i in imgs]
else:
img_urls = [build_zarr_image_url(obj_id)]
img_names = [obj.getName()]
img_ids = [obj_id]

return file_names, file_urls, img_names, img_urls
return file_names, file_urls, img_names, img_urls, img_ids


def build_viewer_url(config_id):
Expand All @@ -57,6 +61,13 @@ def build_zarr_image_url(image_id):
return SERVER + "/zarr/v0.4/image/" + str(image_id) + ".zarr"


def build_attachement_url(obj_id):
""" Generates urls like:
http://localhost:4080/webclient/annotation/99999
"""
return SERVER + "/webclient/annotation/" + str(obj_id)


def get_attached_configs(obj_type, obj_id, conn):
""" Gets all the ".json" files attached to an object
and returns a list of file names and a list of urls
Expand Down Expand Up @@ -144,6 +155,71 @@ def add_cell_identities(config_args, vc_dataset):
return vc_dataset


class VitessceShape():
"""
Converts an OMERO ROI shape to a vitessce compatibel represetnation
https://github.com/will-moore/omero-vitessce/blob/master/omero_vitessce/views.py
"""
def __init__(self, shape):
self.shape = self.to_shapely(shape)
self.name = shape.getTextValue().getValue()

def to_shapely(self, omero_shape):
encoder = get_encoder(omero_shape.__class__)
shape_json = encoder.encode(omero_shape)

if "Points" in shape_json:
xy = shape_json["Points"].split(" ")
coords = []
for coord in xy:
c = coord.split(",")
coords.append((float(c[0]), float(c[1])))
return Polygon(coords)

def poly(self):
# Use 2 to get e.g. 8 points from 56.
return list(self.shape.simplify(2).exterior.coords)


def process_rois(img_ids, conn):
"""
Generates shapely polygons from OMERO rois,
extracted from a list of images
"""
rois = []
for img_id in img_ids:
img = conn.getObject("Image", img_id)
if not img:
continue
rois += img.getROIs()
shapes = [VitessceShape(r.getShape(0)) for r in rois]
return shapes


def make_cell_json(shapes):
"""
Turns a list of polygons into a dictionary:
{cell_id: [[x, y], ...]}
"""
cell_dict = {}
for s in shapes:
cell_dict[s.name] = s.poly()
return cell_dict


def add_rois(img_ids, vc_dataset, conn):
"""
Adds a url to the rois in json format to the vitessce config
"""
cell_json_url = SERVER + "/omero_vitessce/vitessce_json_rois/" + \
",".join([str(i) for i in img_ids])
vc_dataset = vc_dataset.add_file(
url=cell_json_url,
file_type=Ft.OBS_SEGMENTATIONS_JSON,
coordination_values={"obsType": "cell"})
return vc_dataset


def create_config(config_args, obj_type, obj_id, conn):
"""
Generates a Vitessce config and returns it,
Expand All @@ -154,7 +230,7 @@ def create_config(config_args, obj_type, obj_id, conn):
# plugin is loaded and the form is submitted then it will not be found
# and will not be present in the cleaned_data -> None

file_names, file_urls, img_files, img_urls = get_files_images(
file_names, file_urls, img_files, img_urls, img_ids = get_files_images(
obj_type, obj_id, conn)
config_args = ConfigForm(data=config_args, file_names=file_names,
file_urls=file_urls, img_names=img_files,
Expand Down Expand Up @@ -212,6 +288,11 @@ def create_config(config_args, obj_type, obj_id, conn):
img_url=config_args.get("segmentation"),
name="Segmentation", is_bitmask=True)
images.append(segmentation)
if config_args.get("rois"):
vc_dataset = add_rois(img_ids, vc_dataset, conn)
vc.link_views([sp, lc], ["spatialSegmentationLayer"],
[{"opacity": 1, "radius": 0,
"visible": True, "stroked": False}])
if config_args.get("molecules"):
vc_dataset = add_molecules(config_args, vc_dataset)
vc.link_views([sp, lc], c_types=[Ct.SPATIAL_POINT_LAYER],
Expand All @@ -228,7 +309,7 @@ def create_config(config_args, obj_type, obj_id, conn):
vc.add_coordination_by_dict({
Ct.SPATIAL_ZOOM: 2,
Ct.SPATIAL_TARGET_X: 0,
Ct.SPATIAL_TARGET_Y: 0
Ct.SPATIAL_TARGET_Y: 0,
})

displays = hconcat(*displays)
Expand All @@ -241,10 +322,20 @@ def create_config(config_args, obj_type, obj_id, conn):
controllers = hconcat(controllers, hists)
vc.layout(vconcat(displays, controllers))

return vc
vc_dict = vc.to_dict()

# OBS_SEGMENTATIONS_JSON does not work with raster.json
# https://github.com/vitessce/vitessce/discussions/1962
if config_args.get("rois"):
for d in vc_dict["datasets"]:
for f in d["files"]:
if f["fileType"] == "raster.json":
f["fileType"] = "image.raster.json"

return vc_dict


def attach_config(vc, obj_type, obj_id, filename, conn):
def attach_config(vc_dict, obj_type, obj_id, filename, conn):
"""
Generates a Vitessce config for an OMERO image and returns it.
Assumes the images is an OME NGFF v0.4 file
Expand All @@ -257,7 +348,7 @@ def attach_config(vc, obj_type, obj_id, filename, conn):

config_path = Path(config_path).joinpath(filename)
with open(config_path, "w") as outfile:
json.dump(vc.to_dict(), outfile, indent=4, sort_keys=False)
json.dump(vc_dict, outfile, indent=4, sort_keys=False)

file_ann = conn.createFileAnnfromLocalFile(
config_path, mimetype="text/plain")
Expand Down
18 changes: 15 additions & 3 deletions omero_vitessce/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, JsonResponse

from omeroweb.webclient.decorators import login_required

from .forms import ConfigForm
from .utils import get_attached_configs, create_config, attach_config
from .utils import get_files_images, build_viewer_url
from .utils import process_rois, make_cell_json

from omeroweb.settings import ADDITIONAL_APPS

Expand Down Expand Up @@ -36,7 +37,7 @@ def vitessce_panel(request, obj_type, obj_id, conn=None, **kwargs):
"obj_type": obj_type, "obj_id": obj_id}

if OMERO_WEB_ZARR:
files, urls, img_files, img_urls = get_files_images(
files, urls, img_files, img_urls, _ = get_files_images(
obj_type, obj_id, conn)
form = ConfigForm(file_names=files, file_urls=urls,
img_names=img_files, img_urls=img_urls)
Expand Down Expand Up @@ -64,6 +65,17 @@ def generate_config(request, obj_type, obj_id, conn=None, **kwargs):
return HttpResponseRedirect(viewer_url)


@login_required()
def vitessce_json_rois(request, img_ids, conn=None, **kwargs):
"""Generate a json response with the polygon coordinates of the
vertices of the ROIs on the given images.
"""
img_ids = [int(img_id) for img_id in img_ids.split(",")]
shapes = process_rois(img_ids, conn)
cell_dict = make_cell_json(shapes)
return JsonResponse(cell_dict)


@login_required()
def vitessce_open(request, conn=None, **kwargs):
"""Get the first .json attachement and generate a link for it
Expand All @@ -85,7 +97,7 @@ def vitessce_open(request, conn=None, **kwargs):
context = {"json_configs": dict(),
"obj_type": obj_type, "obj_id": obj_id}
if OMERO_WEB_ZARR:
files, urls, img_files, img_urls = get_files_images(
files, urls, img_files, img_urls, _ = get_files_images(
obj_type, obj_id, conn)
form = ConfigForm(file_names=files, file_urls=urls,
img_names=img_files, img_urls=img_urls)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
vitessce==3.3.0
shapely==2.0.6
Loading

0 comments on commit 1f6cb54

Please sign in to comment.