From 54c834238205fe1219814a70f3b410df5d411886 Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 26 Nov 2021 14:09:37 +0000 Subject: [PATCH 01/21] Support ROI table on Dataset, for table with ROI ID column --- src/omero_metadata/populate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index b6b68a39..b4e30ca8 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -253,6 +253,7 @@ def create_columns_image(self): return self._create_columns("image") def _create_columns(self, klass): + target_class = self.target_object.__class__ if self.types is not None and len(self.types) != len(self.headers): message = "Number of columns and column types not equal." raise MetadataError(message) @@ -308,7 +309,7 @@ def _create_columns(self, klass): self.DEFAULT_COLUMN_SIZE, list())) # Ensure ImageColumn is named "Image" column.name = "Image" - if column.__class__ is RoiColumn: + if column.__class__ is RoiColumn and target_class != DatasetI: append.append(StringColumn(ROI_NAME_COLUMN, '', self.DEFAULT_COLUMN_SIZE, list())) # Ensure RoiColumn is named 'Roi' @@ -759,6 +760,10 @@ def __init__(self, value_resolver): self.images_by_name = dict() self._load() + def resolve_roi(self, column, row, value): + # Support Dataset table with known ROI IDs + return int(value) + def get_image_id_by_name(self, iname, dname=None): return self.images_by_name[iname].id.val From a54bab4291e26268bcbc0f8dcf61aeb56e6360cf Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 13:09:12 +0000 Subject: [PATCH 02/21] Add tests for current state of roi ID handling --- src/omero_metadata/populate.py | 2 +- test/integration/metadata/test_populate.py | 117 ++++++++++++++++++++- 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index b4e30ca8..074b1a78 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -447,7 +447,7 @@ def resolve(self, column, value, row): try: return images_by_id[int(value)].id.val except KeyError: - log.debug('Image Id: %i not found!' % (value)) + log.debug('Image Id: %s not found!' % (value)) return -1 return if WellColumn is column_class: diff --git a/test/integration/metadata/test_populate.py b/test/integration/metadata/test_populate.py index 62a366fd..c19d527d 100644 --- a/test/integration/metadata/test_populate.py +++ b/test/integration/metadata/test_populate.py @@ -37,6 +37,7 @@ import shutil from omero.api import RoiOptions +from omero.gateway import BlitzGateway from omero.grid import ImageColumn from omero.grid import RoiColumn from omero.grid import StringColumn @@ -765,7 +766,7 @@ def assert_columns(self, columns): def assert_row_count(self, rows): # Hard-coded in createCsv's arguments - assert rows == 2 + assert rows == len(self.names) def get_target(self): if not self.image: @@ -810,6 +811,120 @@ def assert_child_annotations(self, oas): assert len(oas) == 0 +class RoiIdsInImage(Image2Rois): + + def __init__(self): + self.count = 5 + self.ann_count = 0 + self.image = None + self.rois = None + self.names = ("nucleus", "ER", "nucleolus") + self.table_name = None + # csv is created on demand, after ROIs created so we know IDs + self.csv = None + + def get_csv(self): + if self.csv is None: + # need ROI IDs... + self.get_target() + row_data = ["%s,Cell,0.5,100" % roi.id.val for roi in self.rois] + # rows with invalid IDs will be Skipped + row_data.append("1,Invalid_ROI_ID,0.5,100") + self.csv = self.create_csv( + col_names="Roi,Feature,RoiArea,Count", + row_data=row_data, + header="# header roi,s,d,l" + ) + return self.csv + + def assert_columns(self, columns): + # Adds a new 'Roi Name' column + col_names = "Roi,Feature,RoiArea,Count,Roi Name" + assert col_names == ",".join([c.name for c in columns]) + + def assert_child_annotations(self, oas): + assert len(oas) == 0 + + +class RoiIdsInDataset(RoiIdsInImage): + """Tests roi column with ROI IDs in a Dataset""" + + def __init__(self): + self.count = 6 + self.ann_count = 0 + self.dataset = None + self.rois = None + self.roi_names = ("nucleus", "ER", "nucleolus") + self.table_name = None + # csv is created on demand, after ROIs created so we know IDs + self.csv = None + + def get_target(self): + if not self.dataset: + dataset = self.create_dataset(names=["ImageOne", "ImageTwo"]) + self.set_name(dataset, "DatasetWithROIs") + # reload dataset to avoid unloaded exceptions etc. + self.dataset = self.test.client.sf.getQueryService().get( + 'Dataset', dataset.id.val) + self.rois = self.create_rois() + return self.dataset + + def get_csv(self): + if self.csv is None: + # need ROI IDs... + self.get_target() + row_data = ["%s,%s,Cell,0.5,100" % (roi.id.val, roi.image.id.val) for roi in self.rois] + # rows with invalid IDs will be Skipped + row_data.append("1,1,Invalid_ROI_ID,0.5,100") + self.csv = self.create_csv( + col_names="Roi,Image,Feature,RoiArea,Count", + row_data=row_data, + header="# header roi,image,s,d,l" + ) + return self.csv + + def create_rois(self): + if not self.dataset: + return [] + rois = [] + conn = BlitzGateway(client_obj=self.test.client) + ds = conn.getObject("Dataset", self.dataset.id) + for image in ds.listChildren(): + for roi_name in self.roi_names: + roi = RoiI() + roi.name = rstring(roi_name) + roi.setImage(ImageI(image.id, False)) + point = PointI() + point.x = rdouble(1) + point.y = rdouble(2) + roi.addShape(point) + rois.append(roi) + us = self.test.client.sf.getUpdateService() + return us.saveAndReturnArray(rois) + + def assert_columns(self, columns): + # Adds a new 'Image Name' column as we have an 'image' ID column + # but NOT 'Roi Name' as above for Image + # see https://github.com/ome/omero-metadata/issues/65 + col_names = "Roi,Image,Feature,RoiArea,Count,Image Name" + assert col_names == ",".join([c.name for c in columns]) + + def assert_row_count(self, rows): + # we created csv row for all ROIs. + # Extra rows with invalid IDs are NOT Skipped + assert rows == len(self.rois) + 1 + + def get_annotations(self): + query = """select d from Dataset d + left outer join fetch d.annotationLinks links + left outer join fetch links.child + where d.id=%s""" % self.dataset.id.val + qs = self.test.client.sf.getQueryService() + ds = qs.findByQuery(query, None) + anns = ds.linkedAnnotationList() + return anns + + class Image2RoisNoNan(Image2Rois): """ Tests that creating LongColumn or DoubleColumn with empty value From 36c8815fb241bfce829760a84a652f7b690a076b Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 16:43:32 +0000 Subject: [PATCH 03/21] DatasetWrapper loads shapes and rois when needed --- src/omero_metadata/populate.py | 71 +++++++++++++++++++++- test/integration/metadata/test_populate.py | 25 +++++--- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index 074b1a78..f64927c0 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -459,6 +459,8 @@ def resolve(self, column, value, row): return self.wrapper.resolve_dataset(column, row, value) if RoiColumn is column_class: return self.wrapper.resolve_roi(column, row, value) + if column_as_lower == 'shape': + return self.wrapper.resolve_shape(value) if column_as_lower in ('row', 'column') \ and column_class is LongColumn: try: @@ -758,11 +760,37 @@ def __init__(self, value_resolver): super(DatasetWrapper, self).__init__(value_resolver) self.images_by_id = dict() self.images_by_name = dict() + self.rois_by_id = None + self.shapes_by_id = None self._load() def resolve_roi(self, column, row, value): # Support Dataset table with known ROI IDs - return int(value) + print("DatasetWrapper resolve_rois", value) + if self.rois_by_id is None: + self._load_rois() + try: + return self.rois_by_id[int(value)].id.val + except KeyError: + log.warn('Dataset is missing ROI: %s' % value) + return Skip() + except ValueError: + log.warn('Wrong input type for ROI ID: %s' % value) + return Skip() + + def resolve_shape(self, value): + # Support Dataset table with known Shape IDs + if self.rois_by_id is None: + self._load_rois() + try: + return self.shapes_by_id[int(value)].id.val + except KeyError: + log.warn('Dataset is missing Shape: %s' % value) + return Skip() + except ValueError: + log.warn('Wrong input type for Shape ID: %s' % value) + return Skip() + def get_image_id_by_name(self, iname, dname=None): return self.images_by_name[iname].id.val @@ -811,6 +839,42 @@ def _load(self): self.images_by_id[self.target_object.id.val] = images_by_id log.debug('Completed parsing dataset: %s' % self.target_name) + def _load_rois(self): + log.debug('Loading ROIs in Dataset:%d' % self.target_object.id.val) + self.rois_by_id = {} + self.shapes_by_id = {} + query_service = self.client.getSession().getQueryService() + parameters = omero.sys.ParametersI() + parameters.addId(self.target_object.id.val) + data = list() + while True: + parameters.page(len(data), 1000) + rv = unwrap(query_service.projection(( + 'select distinct i, r, s ' + 'from Shape s ' + 'join s.roi as r ' + 'join r.image as i ' + 'join i.datasetLinks as dil ' + 'join dil.parent as d ' + 'where d.id = :id order by r.id desc'), + parameters, {'omero.group': '-1'})) + if len(rv) == 0: + break + else: + data.extend(rv) + if not data: + print("No ROIs on images in target Dataset") + # raise MetadataError('Could not find target object!') + + for image, roi, shape in data: + # we only care about *IDs* of ROIs and Shapes in the Dataset + rid = roi.id.val + sid = shape.id.val + self.rois_by_id[rid] = roi + self.shapes_by_id[sid] = shape + + log.debug('Completed loading ROIs and Shapes in Dataset: %s' % self.target_object.id.val) + class ProjectWrapper(PDIWrapper): @@ -1153,8 +1217,8 @@ def preprocess_data(self, reader): if isinstance(value, basestring): column.size = max( column.size, len(value.encode('utf-8'))) - # The following are needed for - # getting post process column sizes + # The following IDs are needed for + # post_process() to get column sizes for names if column.__class__ is WellColumn: column.values.append(value) elif column.__class__ is ImageColumn: @@ -1169,6 +1233,7 @@ def preprocess_data(self, reader): log.error('Original value "%s" now "%s" of bad type!' % ( original_value, value)) raise + # we call post_process each single (mostly empty) row to get ids -> names self.post_process() for column in self.columns: column.values = [] diff --git a/test/integration/metadata/test_populate.py b/test/integration/metadata/test_populate.py index c19d527d..9a15dd42 100644 --- a/test/integration/metadata/test_populate.py +++ b/test/integration/metadata/test_populate.py @@ -850,7 +850,7 @@ class RoiIdsInDataset(RoiIdsInImage): """Tests roi column with ROI IDs in a Dataset""" def __init__(self): - self.count = 6 + self.count = 7 self.ann_count = 0 self.dataset = None self.rois = None @@ -873,13 +873,22 @@ def get_csv(self): if self.csv is None: # need ROI IDs... self.get_target() - row_data = ["%s,%s,Cell,0.5,100" % (roi.id.val, roi.image.id.val) for roi in self.rois] + row_data = [] + for roi in self.rois: + shape_id = roi.copyShapes()[0].id.val + row_data.append("%s,%s,%s,Cell,0.5,100" % ( + roi.id.val, shape_id, roi.image.id.val)) # rows with invalid IDs will be Skipped - row_data.append("1,1,Invalid_ROI_ID,0.5,100") + for count, roi in enumerate(self.rois): + shape_id = roi.copyShapes()[0].id.val + ids = [shape_id, roi.id.val, roi.image.id.val] + # set either shape, roi or image ID to be invalid + ids[count % 3] = 1 + row_data.append("%s,%s,%s,Cell,0.5,100" % tuple(ids)) self.csv = self.create_csv( - col_names="Roi,Image,Feature,RoiArea,Count", + col_names="Roi,shape,Image,Feature,RoiArea,Count", row_data=row_data, - header="# header roi,image,s,d,l" + header="# header roi,l,image,s,d,l" ) return self.csv @@ -906,13 +915,13 @@ def assert_columns(self, columns): # Adds a new 'Image Name' column as we have an 'image' ID column # but NOT 'Roi Name' as above for Image # see https://github.com/ome/omero-metadata/issues/65 - col_names = "Roi,Image,Feature,RoiArea,Count,Image Name" + col_names = "Roi,shape,Image,Feature,RoiArea,Count,Image Name" assert col_names == ",".join([c.name for c in columns]) def assert_row_count(self, rows): # we created csv row for all ROIs. - # Extra rows with invalid IDs are NOT Skipped - assert rows == len(self.rois) + 1 + # Extra rows with invalid IDs are Skipped + assert rows == len(self.rois) def get_annotations(self): query = """select d from Dataset d From ded51f5f55bd794dad12e05f68db517c07832db3 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 17:11:05 +0000 Subject: [PATCH 04/21] Test multiple shapes per ROI, and invalid shape IDs --- src/omero_metadata/populate.py | 2 +- test/integration/metadata/test_populate.py | 35 ++++++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index f64927c0..72c5594d 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -856,7 +856,7 @@ def _load_rois(self): 'join r.image as i ' 'join i.datasetLinks as dil ' 'join dil.parent as d ' - 'where d.id = :id order by r.id desc'), + 'where d.id = :id order by s.id desc'), parameters, {'omero.group': '-1'})) if len(rv) == 0: break diff --git a/test/integration/metadata/test_populate.py b/test/integration/metadata/test_populate.py index 9a15dd42..9afc4b51 100644 --- a/test/integration/metadata/test_populate.py +++ b/test/integration/metadata/test_populate.py @@ -851,6 +851,7 @@ class RoiIdsInDataset(RoiIdsInImage): def __init__(self): self.count = 7 + self.shapes_per_roi = 3 self.ann_count = 0 self.dataset = None self.rois = None @@ -874,17 +875,16 @@ def get_csv(self): # need ROI IDs... self.get_target() row_data = [] + row_idx = 0 for roi in self.rois: - shape_id = roi.copyShapes()[0].id.val - row_data.append("%s,%s,%s,Cell,0.5,100" % ( - roi.id.val, shape_id, roi.image.id.val)) - # rows with invalid IDs will be Skipped - for count, roi in enumerate(self.rois): - shape_id = roi.copyShapes()[0].id.val - ids = [shape_id, roi.id.val, roi.image.id.val] - # set either shape, roi or image ID to be invalid - ids[count % 3] = 1 - row_data.append("%s,%s,%s,Cell,0.5,100" % tuple(ids)) + for shape in roi.copyShapes(): + ids = [roi.id.val, shape.id.val, roi.image.id.val] + row_data.append("%s,%s,%s,Cell,0.5,100" % tuple(ids)) + # rows with invalid IDs will be Skipped + # set either shape, roi or image ID to be invalid + ids[row_idx % 3] = 1 + row_data.append("%s,%s,%s,Cell,0.5,100" % tuple(ids)) + row_idx += 1 self.csv = self.create_csv( col_names="Roi,shape,Image,Feature,RoiArea,Count", row_data=row_data, @@ -903,10 +903,11 @@ def create_rois(self): roi = RoiI() roi.name = rstring(roi_name) roi.setImage(ImageI(image.id, False)) - point = PointI() - point.x = rdouble(1) - point.y = rdouble(2) - roi.addShape(point) + for count in range(self.shapes_per_roi): + point = PointI() + point.x = rdouble(count * 10) + point.y = rdouble(10) + roi.addShape(point) rois.append(roi) us = self.test.client.sf.getUpdateService() return us.saveAndReturnArray(rois) @@ -920,8 +921,10 @@ def assert_columns(self, columns): def assert_row_count(self, rows): # we created csv row for all ROIs. - # Extra rows with invalid IDs are Skipped - assert rows == len(self.rois) + # Extra rows with invalid Shape/ROI IDs are Skipped + # but rows with invalid Image IDs are kept with ID: -1 + # + 1 invalid Image ID per roi + assert rows == len(self.rois) * (self.shapes_per_roi + 1) def get_annotations(self): query = """select d from Dataset d From b19cb01f307065a5939aabea627d7ee36da731b8 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 17:20:28 +0000 Subject: [PATCH 05/21] flake8 fixes --- src/omero_metadata/populate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index 72c5594d..b9dd64c5 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -791,7 +791,6 @@ def resolve_shape(self, value): log.warn('Wrong input type for Shape ID: %s' % value) return Skip() - def get_image_id_by_name(self, iname, dname=None): return self.images_by_name[iname].id.val @@ -873,7 +872,8 @@ def _load_rois(self): self.rois_by_id[rid] = roi self.shapes_by_id[sid] = shape - log.debug('Completed loading ROIs and Shapes in Dataset: %s' % self.target_object.id.val) + log.debug('Completed loading ROIs and Shapes in Dataset: %s' + % self.target_object.id.val) class ProjectWrapper(PDIWrapper): @@ -1233,7 +1233,8 @@ def preprocess_data(self, reader): log.error('Original value "%s" now "%s" of bad type!' % ( original_value, value)) raise - # we call post_process each single (mostly empty) row to get ids -> names + # we call post_process on each single (mostly empty) row + # to get ids -> names self.post_process() for column in self.columns: column.values = [] From fcc64412bc3ccd4d7fdedb52285e16358ba28056 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 17:29:00 +0000 Subject: [PATCH 06/21] Update README to describe Image IDs workflow --- README.rst | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 480540f4..f1317188 100644 --- a/README.rst +++ b/README.rst @@ -65,8 +65,8 @@ populate This command creates an ``OMERO.table`` (bulk annotation) from a ``CSV`` file and links the table as a ``File Annotation`` to a parent container such as Screen, Plate, Project -or Dataset. It also attempts to convert Image or Well names from the ``CSV`` into -Image or Well IDs in the ``OMERO.table``. +Dataset or Image. It also attempts to convert Image, Well or ROI names from the ``CSV`` into +object IDs in the ``OMERO.table``. The ``CSV`` file must be provided as local file with ``--file path/to/file.csv``. @@ -86,10 +86,10 @@ The ``# header`` row is optional. Default column type is ``String``. NB: Column names should not contain spaces if you want to be able to query by these columns. -Examples: +**Project / Dataset** To add a table to a Project, the ``CSV`` file needs to specify ``Dataset Name`` -and ``Image Name``:: +and ``Image Name`` or ``Image ID``:: $ omero metadata populate Project:1 --file path/to/project.csv @@ -102,7 +102,8 @@ project.csv:: img-03.png,dataset01,0.093,3,TRITC img-04.png,dataset01,0.429,4,Cy5 -This will create an OMERO.table linked to the Project like this: +This will create an OMERO.table linked to the Project like this with +a new ``Image`` column with IDs: ========== ============ ======== ============= ============ ===== Image Name Dataset Name ROI_Area Channel_Index Channel_Name Image @@ -115,6 +116,33 @@ img-04.png dataset01 0.429 4 Cy5 36641 If the target is a Dataset instead of a Project, the ``Dataset Name`` column is not needed. +Alternatively, if you already know the Image IDs, these can be specified +in an ``image`` column, and an ``Image Name`` column will be added. +This is only supported for a Dataset:: + +dataset.csv:: + + # header image,d,l,s + Image,ROI_Area,Channel_Index,Channel_Name + 1277,0.0469,1,DAPI + 1278,0.142,2,GFP + 1279,0.093,3,TRITC + +This will create an OMERO.table with new ``Image Name`` column:: + +===== ======== ============= ============ =========== +Image ROI_Area Channel_Index Channel_Name Image Name +===== ======== ============= ============ =========== +1277 0.0469 1 DAPI img-01.png +1278 0.142 2 GFP img-02.png +1279 0.093 3 TRITC img-03.png +===== ======== ============= ============ =========== + +NB: Invalid Image IDs (not found in the Dataset) will be changed +to ``-1`` in the table, with blank ``Image Name``. + +**Screen / Plate** + To add a table to a Screen, the ``CSV`` file needs to specify ``Plate`` name and ``Well``. If a ``# header`` is specified, column types must be ``well`` and ``plate``. From 7e7799b1b0a67340c01bc7c535f0b3e5591dbcd5 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 22:40:52 +0000 Subject: [PATCH 07/21] Update README with ROI shape handling example --- README.rst | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index f1317188..08854d9f 100644 --- a/README.rst +++ b/README.rst @@ -170,36 +170,44 @@ Well Plate Drug Concentration Cell_Count Percent_Mitotic Well Name Plat If the target is a Plate instead of a Screen, the ``Plate`` column is not needed. -If the target is an Image, a csv with ROI-level and object-level data can be used to create an +**ROIs** + +If the target is an Image or a Dataset, a csv with ROI-level and object-level data can be used to create an ``OMERO.table`` (bulk annotation) as a ``File Annotation`` on an Image. The ROI identifying column can be an ``roi`` type column containing ROI ID, and ``Roi Name`` -column will be appended automatically (see example below). Alternatively, the input column can be +column will be appended automatically (see example below). If a column named ``shape`` +of type ``l`` is included, the Shape IDs will be validated and if an ``image`` ID +column is included, an ``Image Name`` column will be added as above. + +Alternatively, the ROI input column can be ``Roi Name`` (with type ``s``), and an ``roi`` type column will be appended containing ROI IDs. In this case, it is required that ROIs on the Image in OMERO have the ``Name`` attribute set. image.csv:: - # header roi,l,d,l - Roi,object,probability,area - 501,1,0.8,250 - 502,1,0.9,500 - 503,1,0.2,25 - 503,2,0.8,400 - 503,3,0.5,200 + # header roi,l,l,d,l + Roi,shape,object,probability,area + 501,1066,1,0.8,250 + 502,1067,2,0.9,500 + 503,1068,3,0.2,25 + 503,1069,4,0.8,400 + 503,1070,5,0.5,200 This will create an OMERO.table linked to the Image like this: -=== ====== =========== ==== ======== -Roi object probability area Roi Name -=== ====== =========== ==== ======== -501 1 0.8 250 Sample1 -502 1 0.9 500 Sample2 -503 1 0.2 25 Sample3 -503 2 0.8 400 Sample3 -503 3 0.5 200 Sample3 -=== ====== =========== ==== ======== - -Note that the ROI-level ``OMERO.table`` is not visible in the OMERO.web UI right-hand panel, but can be visualized by clicking the "eye" on the bulk annotation attachment on the Image. +=== ===== ====== =========== ==== ======== +Roi shape object probability area Roi Name +=== ===== ====== =========== ==== ======== +501 1066 1 0.8 250 Sample1 +502 1067 2 0.9 500 Sample2 +503 1068 3 0.2 25 Sample3 +503 1069 4 0.8 400 Sample3 +503 1070 5 0.5 200 Sample3 +=== ===== ====== =========== ==== ======== + +Note that the ROI-level data from an ``OMERO.table`` is not visible +in the OMERO.web UI right-hand panel under the ``Tables`` tab, +but the table can be visualized by clicking the "eye" on the bulk annotation attachment on the Image. Developer install ================= From c05e76ed1ee2be9b0fe72cca4037b76596bddc60 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 11 Jan 2022 22:48:58 +0000 Subject: [PATCH 08/21] Fix README formatting --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 08854d9f..a0feb9a7 100644 --- a/README.rst +++ b/README.rst @@ -118,7 +118,7 @@ If the target is a Dataset instead of a Project, the ``Dataset Name`` column is Alternatively, if you already know the Image IDs, these can be specified in an ``image`` column, and an ``Image Name`` column will be added. -This is only supported for a Dataset:: +This is only supported for a Dataset. dataset.csv:: @@ -128,7 +128,7 @@ dataset.csv:: 1278,0.142,2,GFP 1279,0.093,3,TRITC -This will create an OMERO.table with new ``Image Name`` column:: +This will create an OMERO.table with new ``Image Name`` column: ===== ======== ============= ============ =========== Image ROI_Area Channel_Index Channel_Name Image Name From 3e47612f45ab5f8fb29094eb805304ca158d5926 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 12 Jan 2022 11:37:44 +0000 Subject: [PATCH 09/21] README tweaks --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a0feb9a7..903686cf 100644 --- a/README.rst +++ b/README.rst @@ -172,12 +172,13 @@ If the target is a Plate instead of a Screen, the ``Plate`` column is not needed **ROIs** -If the target is an Image or a Dataset, a csv with ROI-level and object-level data can be used to create an -``OMERO.table`` (bulk annotation) as a ``File Annotation`` on an Image. +If the target is an Image or a Dataset, a csv with ROI-level and Shape-level data can be used to create an +``OMERO.table`` (bulk annotation) as a ``File Annotation`` linked to the target object. The ROI identifying column can be an ``roi`` type column containing ROI ID, and ``Roi Name`` column will be appended automatically (see example below). If a column named ``shape`` of type ``l`` is included, the Shape IDs will be validated and if an ``image`` ID column is included, an ``Image Name`` column will be added as above. +NB: Columns of type ``shape`` aren't yet supported on the OMERO.server. Alternatively, the ROI input column can be ``Roi Name`` (with type ``s``), and an ``roi`` type column will be appended containing ROI IDs. From a7befc5caa83b5896c2cd7bf0162237076e38eee Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 12 Jan 2022 16:16:33 +0000 Subject: [PATCH 10/21] Remove README changes unrelated to this PR --- README.rst | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/README.rst b/README.rst index 903686cf..b608f52a 100644 --- a/README.rst +++ b/README.rst @@ -116,30 +116,6 @@ img-04.png dataset01 0.429 4 Cy5 36641 If the target is a Dataset instead of a Project, the ``Dataset Name`` column is not needed. -Alternatively, if you already know the Image IDs, these can be specified -in an ``image`` column, and an ``Image Name`` column will be added. -This is only supported for a Dataset. - -dataset.csv:: - - # header image,d,l,s - Image,ROI_Area,Channel_Index,Channel_Name - 1277,0.0469,1,DAPI - 1278,0.142,2,GFP - 1279,0.093,3,TRITC - -This will create an OMERO.table with new ``Image Name`` column: - -===== ======== ============= ============ =========== -Image ROI_Area Channel_Index Channel_Name Image Name -===== ======== ============= ============ =========== -1277 0.0469 1 DAPI img-01.png -1278 0.142 2 GFP img-02.png -1279 0.093 3 TRITC img-03.png -===== ======== ============= ============ =========== - -NB: Invalid Image IDs (not found in the Dataset) will be changed -to ``-1`` in the table, with blank ``Image Name``. **Screen / Plate** From 0063b2cd0c75e398fbadfdbadbeaa589c9757613 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 12 Jan 2022 16:33:30 +0000 Subject: [PATCH 11/21] Remove print statements --- src/omero_metadata/populate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index b9dd64c5..242b6bfd 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -766,7 +766,6 @@ def __init__(self, value_resolver): def resolve_roi(self, column, row, value): # Support Dataset table with known ROI IDs - print("DatasetWrapper resolve_rois", value) if self.rois_by_id is None: self._load_rois() try: @@ -862,8 +861,7 @@ def _load_rois(self): else: data.extend(rv) if not data: - print("No ROIs on images in target Dataset") - # raise MetadataError('Could not find target object!') + raise MetadataError("No ROIs on images in target Dataset") for image, roi, shape in data: # we only care about *IDs* of ROIs and Shapes in the Dataset From 43a03c7f4f4b65746772f1461d1a9b5cb16f74bb Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 12 Jan 2022 16:37:11 +0000 Subject: [PATCH 12/21] Consistent use of CSV in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b608f52a..ccdc30e4 100644 --- a/README.rst +++ b/README.rst @@ -148,7 +148,7 @@ If the target is a Plate instead of a Screen, the ``Plate`` column is not needed **ROIs** -If the target is an Image or a Dataset, a csv with ROI-level and Shape-level data can be used to create an +If the target is an Image or a Dataset, a ``CSV`` with ROI-level and Shape-level data can be used to create an ``OMERO.table`` (bulk annotation) as a ``File Annotation`` linked to the target object. The ROI identifying column can be an ``roi`` type column containing ROI ID, and ``Roi Name`` column will be appended automatically (see example below). If a column named ``shape`` From dad037ffe20a725359a3585fba86847c1d46fa44 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 12 Jan 2022 17:21:03 +0000 Subject: [PATCH 13/21] Support shape column for target Image --- src/omero_metadata/populate.py | 24 +++++++++++++--- test/integration/metadata/test_populate.py | 33 ++++++++++++++-------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index 242b6bfd..168ad738 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -961,6 +961,7 @@ class ImageWrapper(ValueWrapper): def __init__(self, value_resolver): super(ImageWrapper, self).__init__(value_resolver) self.rois_by_id = dict() + self.shapes_by_id = dict() self.rois_by_name = dict() self.ambiguous_naming = False self._load() @@ -971,6 +972,16 @@ def get_roi_id_by_name(self, rname): def get_roi_name_by_id(self, rid): return unwrap(self.rois_by_id[rid].name) + def resolve_shape(self, value): + try: + return self.shapes_by_id[int(value)].id.val + except KeyError: + log.warn('Image is missing Shape: %s' % value) + return Skip() + except ValueError: + log.warn('Wrong input type for Shape ID: %s' % value) + return Skip() + def resolve_roi(self, column, row, value): try: return self.rois_by_id[int(value)].id.val @@ -997,9 +1008,10 @@ def _load(self): while True: parameters.page(len(data), 1000) rv = query_service.findAllByQuery(( - 'select distinct r from Image as i ' - 'join i.rois as r ' - 'where i.id = :id order by r.id desc'), + 'select distinct s from Shape as s ' + 'join s.roi as r ' + 'join r.image as i ' + 'where i.id = :id order by s.id desc'), parameters, {'omero.group': '-1'}) if len(rv) == 0: break @@ -1010,15 +1022,19 @@ def _load(self): rois_by_id = dict() rois_by_name = dict() - for roi in data: + shapes_by_id = dict() + for shape in data: + roi = shape.roi rid = roi.id.val rois_by_id[rid] = roi + shapes_by_id[shape.id.val] = shape if unwrap(roi.name) in rois_by_name.keys(): log.warn('Conflicting ROI names.') self.ambiguous_naming = True rois_by_name[unwrap(roi.name)] = roi self.rois_by_id = rois_by_id self.rois_by_name = rois_by_name + self.shapes_by_id = shapes_by_id log.debug('Completed parsing image: %s' % self.target_name) diff --git a/test/integration/metadata/test_populate.py b/test/integration/metadata/test_populate.py index 9afc4b51..0e97c8a9 100644 --- a/test/integration/metadata/test_populate.py +++ b/test/integration/metadata/test_populate.py @@ -756,7 +756,7 @@ def __init__(self): ) self.image = None self.rois = None - self.names = ("roi1", "roi2") + self.roi_names = ("roi1", "roi2") self.table_name = None def assert_columns(self, columns): @@ -766,7 +766,7 @@ def assert_columns(self, columns): def assert_row_count(self, rows): # Hard-coded in createCsv's arguments - assert rows == len(self.names) + assert rows == len(self.roi_names) def get_target(self): if not self.image: @@ -781,7 +781,7 @@ def create_rois(self): if not self.image: return [] rois = [] - for roi_name in self.names: + for roi_name in self.roi_names: roi = RoiI() roi.name = rstring(roi_name) roi.setImage(ImageI(self.image.id.val, False)) @@ -814,11 +814,11 @@ def assert_child_annotations(self, oas): class RoiIdsInImage(Image2Rois): def __init__(self): - self.count = 5 + self.count = 6 self.ann_count = 0 self.image = None self.rois = None - self.names = ("nucleus", "ER", "nucleolus") + self.roi_names = ("nucleus", "ER", "nucleolus") self.table_name = None # csv is created on demand, after ROIs created so we know IDs self.csv = None @@ -827,19 +827,28 @@ def get_csv(self): if self.csv is None: # need ROI IDs... self.get_target() - row_data = ["%s,Cell,0.5,100" % roi.id.val for roi in self.rois] - # rows with invalid IDs will be Skipped - row_data.append("1,Invalid_ROI_ID,0.5,100") + row_data = [] + row_idx = 0 + for roi in self.rois: + for shape in roi.copyShapes(): + ids = [roi.id.val, shape.id.val] + row_data.append("%s,%s,Cell,0.5,100" % tuple(ids)) + # rows with invalid IDs will be Skipped + # set either shape or roi ID to be invalid + ids[row_idx % 2] = 1 + row_data.append("%s,%s,Cell,0.5,100" % tuple(ids)) + row_idx += 1 self.csv = self.create_csv( - col_names="Roi,Feature,RoiArea,Count", + # shape columns identified by name not type + col_names="Roi,shape,Feature,RoiArea,Count", row_data=row_data, - header="# header roi,s,d,l" + header="# header roi,l,s,d,l" ) return self.csv def assert_columns(self, columns): # Adds a new 'Roi Name' column - col_names = "Roi,Feature,RoiArea,Count,Roi Name" + col_names = "Roi,shape,Feature,RoiArea,Count,Roi Name" assert col_names == ",".join([c.name for c in columns]) def assert_child_annotations(self, oas): @@ -1285,6 +1294,8 @@ def teardown_method(self, method): class TestPopulateMetadata(TestPopulateMetadataHelper): METADATA_FIXTURES = ( + RoiIdsInDataset(), + RoiIdsInImage(), Screen2Plates(), Plate2Wells(), Dataset2Images(), From 55d29d6e5335b93bb29c3a32b1892788ed4fc2e6 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 18 Jan 2022 16:17:12 +0000 Subject: [PATCH 14/21] Don't skip rows for invalid ROI/shape IDs --- src/omero_metadata/populate.py | 16 ++++++++-------- test/integration/metadata/test_populate.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index 168ad738..2ca963cf 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -772,10 +772,10 @@ def resolve_roi(self, column, row, value): return self.rois_by_id[int(value)].id.val except KeyError: log.warn('Dataset is missing ROI: %s' % value) - return Skip() + return -1 except ValueError: log.warn('Wrong input type for ROI ID: %s' % value) - return Skip() + return -1 def resolve_shape(self, value): # Support Dataset table with known Shape IDs @@ -785,10 +785,10 @@ def resolve_shape(self, value): return self.shapes_by_id[int(value)].id.val except KeyError: log.warn('Dataset is missing Shape: %s' % value) - return Skip() + return -1 except ValueError: log.warn('Wrong input type for Shape ID: %s' % value) - return Skip() + return -1 def get_image_id_by_name(self, iname, dname=None): return self.images_by_name[iname].id.val @@ -977,20 +977,20 @@ def resolve_shape(self, value): return self.shapes_by_id[int(value)].id.val except KeyError: log.warn('Image is missing Shape: %s' % value) - return Skip() + return -1 except ValueError: log.warn('Wrong input type for Shape ID: %s' % value) - return Skip() + return -1 def resolve_roi(self, column, row, value): try: return self.rois_by_id[int(value)].id.val except KeyError: log.warn('Image is missing ROI: %s' % value) - return Skip() + return -1 except ValueError: log.warn('Wrong input type for ROI ID: %s' % value) - return Skip() + return -1 def _load(self): query_service = self.client.getSession().getQueryService() diff --git a/test/integration/metadata/test_populate.py b/test/integration/metadata/test_populate.py index 0e97c8a9..619bc968 100644 --- a/test/integration/metadata/test_populate.py +++ b/test/integration/metadata/test_populate.py @@ -765,7 +765,6 @@ def assert_columns(self, columns): assert col_names == ",".join([c.name for c in columns]) def assert_row_count(self, rows): - # Hard-coded in createCsv's arguments assert rows == len(self.roi_names) def get_target(self): @@ -833,7 +832,7 @@ def get_csv(self): for shape in roi.copyShapes(): ids = [roi.id.val, shape.id.val] row_data.append("%s,%s,Cell,0.5,100" % tuple(ids)) - # rows with invalid IDs will be Skipped + # test handling of invalid IDs # set either shape or roi ID to be invalid ids[row_idx % 2] = 1 row_data.append("%s,%s,Cell,0.5,100" % tuple(ids)) @@ -854,6 +853,10 @@ def assert_columns(self, columns): def assert_child_annotations(self, oas): assert len(oas) == 0 + def assert_row_count(self, rows): + # we have 2 csv rows per ROI (one row is invalid) + assert rows == len(self.roi_names) * 2 + class RoiIdsInDataset(RoiIdsInImage): """Tests roi column with ROI IDs in a Dataset""" @@ -889,7 +892,7 @@ def get_csv(self): for shape in roi.copyShapes(): ids = [roi.id.val, shape.id.val, roi.image.id.val] row_data.append("%s,%s,%s,Cell,0.5,100" % tuple(ids)) - # rows with invalid IDs will be Skipped + # test handling of invalid IDs # set either shape, roi or image ID to be invalid ids[row_idx % 3] = 1 row_data.append("%s,%s,%s,Cell,0.5,100" % tuple(ids)) @@ -929,11 +932,8 @@ def assert_columns(self, columns): assert col_names == ",".join([c.name for c in columns]) def assert_row_count(self, rows): - # we created csv row for all ROIs. - # Extra rows with invalid Shape/ROI IDs are Skipped - # but rows with invalid Image IDs are kept with ID: -1 - # + 1 invalid Image ID per roi - assert rows == len(self.rois) * (self.shapes_per_roi + 1) + # we have 2 csv rows per Shape (one row is invalid) + assert rows == len(self.rois) * self.shapes_per_roi * 2 def get_annotations(self): query = """select d from Dataset d From b50d2a5404b167fc884290d72ccb2da79eba47fd Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 18 Jan 2022 22:25:40 +0000 Subject: [PATCH 15/21] Try to improve clarity in README --- README.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index ccdc30e4..e53fde9f 100644 --- a/README.rst +++ b/README.rst @@ -148,12 +148,13 @@ If the target is a Plate instead of a Screen, the ``Plate`` column is not needed **ROIs** -If the target is an Image or a Dataset, a ``CSV`` with ROI-level and Shape-level data can be used to create an +If the target is an Image or a Dataset, a ``CSV`` with ROI-level or Shape-level data can be used to create an ``OMERO.table`` (bulk annotation) as a ``File Annotation`` linked to the target object. -The ROI identifying column can be an ``roi`` type column containing ROI ID, and ``Roi Name`` +The ROI-identifying column can be an ``roi`` type column containing ROI ID and ``Roi Name`` column will be appended automatically (see example below). If a column named ``shape`` -of type ``l`` is included, the Shape IDs will be validated and if an ``image`` ID -column is included, an ``Image Name`` column will be added as above. +of type ``l`` is included, the Shape IDs will be validated (and set to -1 if invalid). +Also if an ``image`` ID +column is included, an ``Image Name`` column will be added as in above examples. NB: Columns of type ``shape`` aren't yet supported on the OMERO.server. Alternatively, the ROI input column can be From 3ca3dc8360f25cc095be4e961ccea6286cc297cf Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 16 Feb 2022 11:13:30 +0000 Subject: [PATCH 16/21] README tweaks --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e53fde9f..85ca1274 100644 --- a/README.rst +++ b/README.rst @@ -150,14 +150,13 @@ If the target is a Plate instead of a Screen, the ``Plate`` column is not needed If the target is an Image or a Dataset, a ``CSV`` with ROI-level or Shape-level data can be used to create an ``OMERO.table`` (bulk annotation) as a ``File Annotation`` linked to the target object. -The ROI-identifying column can be an ``roi`` type column containing ROI ID and ``Roi Name`` +If there is an ``roi`` column (header type ``roi``) containing ROI IDs, an ``Roi Name`` column will be appended automatically (see example below). If a column named ``shape`` of type ``l`` is included, the Shape IDs will be validated (and set to -1 if invalid). -Also if an ``image`` ID -column is included, an ``Image Name`` column will be added as in above examples. +Also if an ``image`` column of Image IDs is included, an ``Image Name`` column will be added. NB: Columns of type ``shape`` aren't yet supported on the OMERO.server. -Alternatively, the ROI input column can be +Alternatively, if the target is an Image, the ROI input column can be ``Roi Name`` (with type ``s``), and an ``roi`` type column will be appended containing ROI IDs. In this case, it is required that ROIs on the Image in OMERO have the ``Name`` attribute set. From 784ef6a656d18bea64537aec1f2e2244d46155c7 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 16 Feb 2022 12:20:06 +0000 Subject: [PATCH 17/21] README: clarify 'shape' column has Shape IDs --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 85ca1274..1e568745 100644 --- a/README.rst +++ b/README.rst @@ -151,7 +151,7 @@ If the target is a Plate instead of a Screen, the ``Plate`` column is not needed If the target is an Image or a Dataset, a ``CSV`` with ROI-level or Shape-level data can be used to create an ``OMERO.table`` (bulk annotation) as a ``File Annotation`` linked to the target object. If there is an ``roi`` column (header type ``roi``) containing ROI IDs, an ``Roi Name`` -column will be appended automatically (see example below). If a column named ``shape`` +column will be appended automatically (see example below). If a column of Shape IDs named ``shape`` of type ``l`` is included, the Shape IDs will be validated (and set to -1 if invalid). Also if an ``image`` column of Image IDs is included, an ``Image Name`` column will be added. NB: Columns of type ``shape`` aren't yet supported on the OMERO.server. From 536a76bcab9ba1b2998f29ef514f573d2bd8039e Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 16 Feb 2022 13:47:53 +0000 Subject: [PATCH 18/21] Fix Exception string formatting. Fixes #68 --- src/omero_metadata/populate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/omero_metadata/populate.py b/src/omero_metadata/populate.py index 2ca963cf..b803895f 100644 --- a/src/omero_metadata/populate.py +++ b/src/omero_metadata/populate.py @@ -831,7 +831,7 @@ def _load(self): images_by_id[iid] = image if iname in self.images_by_name: raise Exception("Image named %s(id=%d) present. (id=%s)" % ( - iname, self.images_by_name[iname], iid + iname, self.images_by_name[iname].id.val, iid )) self.images_by_name[iname] = image self.images_by_id[self.target_object.id.val] = images_by_id From 53d7da1ab7c63ce3ee948dba9cba775b56a9e613 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 21 Mar 2022 13:58:37 +0000 Subject: [PATCH 19/21] Add CHANGELOG for 0.10.0 --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 73333acb..df2fc3bd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ CHANGES ======= +0.10.0 +------ + +* Populate metadata supports ROIs and Shapes when target is a Dataset + 0.9.0 ----- From 7c0cea3992f1cf55889dc661d1e9cd0d608e9930 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 21 Mar 2022 13:59:29 +0000 Subject: [PATCH 20/21] =?UTF-8?q?Bump=20version:=200.9.1.dev0=20=E2=86=92?= =?UTF-8?q?=200.10.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c38ecdaf..e2cc8b42 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.1.dev0 +current_version = 0.10.0 commit = True tag = True sign_tags = True diff --git a/setup.py b/setup.py index cdc649f8..cca3da86 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -version = '0.9.1.dev0' +version = '0.10.0' url = "https://github.com/ome/omero-metadata/" setup( From 2c1b2691dd61f42ac860ee12095829b1523fed95 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 21 Mar 2022 14:00:14 +0000 Subject: [PATCH 21/21] =?UTF-8?q?Bump=20version:=200.10.0=20=E2=86=92=200.?= =?UTF-8?q?10.1.dev0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e2cc8b42..ee1374ac 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0 +current_version = 0.10.1.dev0 commit = True tag = True sign_tags = True diff --git a/setup.py b/setup.py index cca3da86..745aee7d 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -version = '0.10.0' +version = '0.10.1.dev0' url = "https://github.com/ome/omero-metadata/" setup(