fusion-tools 2.4.2__tar.gz → 2.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/PKG-INFO +1 -1
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/setup.py +1 -1
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/maps.py +2 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/segmentation.py +232 -84
- fusion_tools-2.4.4/src/fusion_tools/utils/images.py +133 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/shapes.py +88 -6
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/PKG-INFO +1 -1
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/SOURCES.txt +1 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/LICENSE +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/README.md +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/pyproject.toml +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/setup.cfg +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/__init__.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/__init__.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/tools.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/dataset.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/feature_extraction.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/handler.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/tileserver.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/__init__.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/omics.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/stats.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/visualization/__init__.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/visualization/components.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/visualization/vis_utils.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/dependency_links.txt +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/requires.txt +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/top_level.txt +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_classification_model.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_custom_component.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_dataset_classes.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_extract_nested_prop.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_extract_recursive.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_feature_annotation.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_feature_extraction.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_generate_property_dict.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_get_gene_info.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_hra_viewer.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_image_overlay.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_model_train.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_multiframe_rgb.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_multiframeslidemap.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_process.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_property_plotter.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_read_anndata.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_remote_slide_annotations.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_remote_tileserver.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_segmentation.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_slide_annotations.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_spatial_aggregation.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_tile_server.py +0 -0
- {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_visualization_component.py +0 -0
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="fusion-tools",
|
|
8
|
-
version="2.4.
|
|
8
|
+
version="2.4.4",
|
|
9
9
|
author="Sam Border",
|
|
10
10
|
author_email="sam.border2256@gmail.com",
|
|
11
11
|
description="Modular visualization and analysis dashboard creation for high-resolution microscopy images",
|
|
@@ -366,6 +366,7 @@ class SlideMap(MapComponent):
|
|
|
366
366
|
var overlayVal = Number.Nan;
|
|
367
367
|
if (overlayProp) {
|
|
368
368
|
if (overlayProp.name) {
|
|
369
|
+
//TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
369
370
|
var overlaySubProps = overlayProp.name.split(" --> ");
|
|
370
371
|
var prop_dict = feature.properties;
|
|
371
372
|
for (let i = 0; i < overlaySubProps.length; i++) {
|
|
@@ -423,6 +424,7 @@ class SlideMap(MapComponent):
|
|
|
423
424
|
// Iterating through filterVals list
|
|
424
425
|
var filter = filterVals[i];
|
|
425
426
|
if (filter.name) {
|
|
427
|
+
//TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
426
428
|
var filterSubProps = filter.name.split(" --> ");
|
|
427
429
|
var prop_dict = feature.properties;
|
|
428
430
|
for (let j = 0; j < filterSubProps.length; j++) {
|
|
@@ -1240,6 +1240,10 @@ class BulkLabels(Tool):
|
|
|
1240
1240
|
n_clicks = 0,
|
|
1241
1241
|
id = {'type': 'bulk-labels-apply-labels','index': 0},
|
|
1242
1242
|
disabled = True
|
|
1243
|
+
),
|
|
1244
|
+
dbc.Tooltip(
|
|
1245
|
+
'Saves current label to labeling session but does not add label to structure-level data',
|
|
1246
|
+
target = {'type': 'bulk-labels-apply-labels','index': 0}
|
|
1243
1247
|
)
|
|
1244
1248
|
])
|
|
1245
1249
|
]),
|
|
@@ -1252,6 +1256,10 @@ class BulkLabels(Tool):
|
|
|
1252
1256
|
id = {'type': 'bulk-labels-download-labels','index': 0},
|
|
1253
1257
|
disabled = True
|
|
1254
1258
|
),
|
|
1259
|
+
dbc.Tooltip(
|
|
1260
|
+
'Downloads both individual labels as well as associated labeling metadata containing rationales for labels.',
|
|
1261
|
+
target = {'type': 'bulk-labels-download-labels','index': 0}
|
|
1262
|
+
),
|
|
1255
1263
|
dcc.Download(
|
|
1256
1264
|
id = {'type':'bulk-labels-download-data','index': 0}
|
|
1257
1265
|
),
|
|
@@ -1259,6 +1267,29 @@ class BulkLabels(Tool):
|
|
|
1259
1267
|
id = {'type': 'bulk-labels-download-metadata','index': 0}
|
|
1260
1268
|
)
|
|
1261
1269
|
])
|
|
1270
|
+
],style = {'marginTop': '10px'}),
|
|
1271
|
+
dbc.Row([
|
|
1272
|
+
dbc.Col([
|
|
1273
|
+
dbc.Button(
|
|
1274
|
+
'Add Labels to Structures',
|
|
1275
|
+
id = {'type': 'bulk-labels-add-labels-to-structures','index': 0},
|
|
1276
|
+
className = 'd-grid col-12 mx-auto',
|
|
1277
|
+
n_clicks = 0,
|
|
1278
|
+
disabled = True
|
|
1279
|
+
),
|
|
1280
|
+
dbc.Tooltip(
|
|
1281
|
+
'Adds current labels to structures for use in plotting, filtering, etc.',
|
|
1282
|
+
target = {'type': 'bulk-labels-add-labels-to-structures','index': 0}
|
|
1283
|
+
)
|
|
1284
|
+
])
|
|
1285
|
+
],style = {'marginTop': '10px'}),
|
|
1286
|
+
dbc.Row([
|
|
1287
|
+
dbc.Col([
|
|
1288
|
+
html.Div(
|
|
1289
|
+
id = {'type': 'bulk-labels-label-stats-div','index': 0},
|
|
1290
|
+
children = []
|
|
1291
|
+
)
|
|
1292
|
+
])
|
|
1262
1293
|
],style = {'marginTop': '10px'})
|
|
1263
1294
|
])
|
|
1264
1295
|
])
|
|
@@ -1307,7 +1338,8 @@ class BulkLabels(Tool):
|
|
|
1307
1338
|
Output({'type': 'bulk-labels-label-text','index': ALL},'disabled'),
|
|
1308
1339
|
Output({'type': 'bulk-labels-label-rationale','index': ALL},'disabled'),
|
|
1309
1340
|
Output({'type': 'bulk-labels-apply-labels','index': ALL},'disabled'),
|
|
1310
|
-
Output({'type': 'bulk-labels-download-labels','index': ALL},'disabled')
|
|
1341
|
+
Output({'type': 'bulk-labels-download-labels','index': ALL},'disabled'),
|
|
1342
|
+
Output({'type': 'bulk-labels-add-labels-to-structures','index': ALL},'disabled')
|
|
1311
1343
|
],
|
|
1312
1344
|
[
|
|
1313
1345
|
State({'type': 'bulk-labels-include-structures','index': ALL},'value'),
|
|
@@ -1381,6 +1413,8 @@ class BulkLabels(Tool):
|
|
|
1381
1413
|
],
|
|
1382
1414
|
[
|
|
1383
1415
|
Output({'type': 'bulk-labels-labels-store','index': ALL},'data'),
|
|
1416
|
+
Output({'type': 'bulk-labels-apply-labels','index': ALL},'color'),
|
|
1417
|
+
Output({'type': 'bulk-labels-label-stats-div','index': ALL},'children')
|
|
1384
1418
|
],
|
|
1385
1419
|
[
|
|
1386
1420
|
State({'type': 'bulk-labels-markers','index': ALL},'data'),
|
|
@@ -1413,7 +1447,9 @@ class BulkLabels(Tool):
|
|
|
1413
1447
|
Output({'type': 'bulk-labels-label-source','index': ALL},'children'),
|
|
1414
1448
|
Output({'type': 'bulk-labels-apply-labels','index': ALL},'disabled'),
|
|
1415
1449
|
Output({'type': 'bulk-labels-include-structures','index':ALL},'options'),
|
|
1416
|
-
Output({'type': 'bulk-labels-spatial-query-structures','index':ALL},'options')
|
|
1450
|
+
Output({'type': 'bulk-labels-spatial-query-structures','index':ALL},'options'),
|
|
1451
|
+
Output({'type': 'bulk-labels-apply-labels','index': ALL},'color'),
|
|
1452
|
+
Output({'type': 'map-marker-div','index': ALL},'children')
|
|
1417
1453
|
],
|
|
1418
1454
|
[
|
|
1419
1455
|
State({'type': 'feature-overlay','index':ALL},'name')
|
|
@@ -1457,6 +1493,33 @@ class BulkLabels(Tool):
|
|
|
1457
1493
|
]
|
|
1458
1494
|
)(self.update_structure_count)
|
|
1459
1495
|
|
|
1496
|
+
# Adding current labels to structure data
|
|
1497
|
+
self.blueprint.callback(
|
|
1498
|
+
[
|
|
1499
|
+
Input({'type': 'bulk-labels-add-labels-to-structures','index': ALL},'n_clicks')
|
|
1500
|
+
],
|
|
1501
|
+
[
|
|
1502
|
+
Output({'type': 'map-annotations-store','index': ALL},'data')
|
|
1503
|
+
],
|
|
1504
|
+
[
|
|
1505
|
+
State({'type': 'map-annotations-store','index': ALL},'data'),
|
|
1506
|
+
State({'type': 'bulk-labels-labels-store','index': ALL},'data')
|
|
1507
|
+
]
|
|
1508
|
+
)(self.add_labels_to_structures)
|
|
1509
|
+
|
|
1510
|
+
# Editing current labels by editing the label table
|
|
1511
|
+
self.blueprint.callback(
|
|
1512
|
+
[
|
|
1513
|
+
Input({'type': 'bulk-labels-label-table','index': ALL},'data'),
|
|
1514
|
+
],
|
|
1515
|
+
[
|
|
1516
|
+
Output({'type': 'bulk-labels-labels-store','index': ALL},'data')
|
|
1517
|
+
],
|
|
1518
|
+
[
|
|
1519
|
+
State({'type': 'bulk-labels-labels-store','index': ALL},'data'),
|
|
1520
|
+
]
|
|
1521
|
+
)(self.update_label_table)
|
|
1522
|
+
|
|
1460
1523
|
|
|
1461
1524
|
def update_slide(self, new_annotations: list):
|
|
1462
1525
|
|
|
@@ -1511,6 +1574,8 @@ class BulkLabels(Tool):
|
|
|
1511
1574
|
|
|
1512
1575
|
if query_type is None:
|
|
1513
1576
|
raise exceptions.PreventUpdate
|
|
1577
|
+
if not type(query_type)==str:
|
|
1578
|
+
raise exceptions.PreventUpdate
|
|
1514
1579
|
|
|
1515
1580
|
query_types = {
|
|
1516
1581
|
'within': '''
|
|
@@ -1770,7 +1835,7 @@ class BulkLabels(Tool):
|
|
|
1770
1835
|
(f['bbox'][1]+f['bbox'][3])/2
|
|
1771
1836
|
]
|
|
1772
1837
|
},
|
|
1773
|
-
'properties': f_data
|
|
1838
|
+
'properties': f_data | {'_id': f['properties']['_id']}
|
|
1774
1839
|
}
|
|
1775
1840
|
for f,f_data in zip(filtered_geojson['features'],filtered_ref_list)
|
|
1776
1841
|
]
|
|
@@ -1792,15 +1857,17 @@ class BulkLabels(Tool):
|
|
|
1792
1857
|
labels_rationale_disabled = False
|
|
1793
1858
|
labels_apply_button_disabled = False
|
|
1794
1859
|
labels_download_button_disabled = False
|
|
1860
|
+
labels_add_to_structures_button_disabled = False
|
|
1795
1861
|
else:
|
|
1796
1862
|
labels_type_disabled = True
|
|
1797
1863
|
labels_text_disabled = True
|
|
1798
1864
|
labels_rationale_disabled = True
|
|
1799
1865
|
labels_apply_button_disabled = True
|
|
1800
1866
|
labels_download_button_disabled = True
|
|
1867
|
+
labels_add_to_structures_button_disabled = True
|
|
1801
1868
|
|
|
1802
1869
|
|
|
1803
|
-
return [new_structures_div], [new_markers_div], [new_labels_source], [labels_type_disabled], [labels_text_disabled], [labels_rationale_disabled], [labels_apply_button_disabled], [labels_download_button_disabled]
|
|
1870
|
+
return [new_structures_div], [new_markers_div], [new_labels_source], [labels_type_disabled], [labels_text_disabled], [labels_rationale_disabled], [labels_apply_button_disabled], [labels_download_button_disabled], [labels_add_to_structures_button_disabled]
|
|
1804
1871
|
|
|
1805
1872
|
def update_label_properties(self, add_click:list, remove_click:list, property_info:list):
|
|
1806
1873
|
"""Adding/removing property filter dropdown
|
|
@@ -1913,36 +1980,13 @@ class BulkLabels(Tool):
|
|
|
1913
1980
|
)
|
|
1914
1981
|
)
|
|
1915
1982
|
|
|
1916
|
-
return values_selector
|
|
1917
|
-
|
|
1918
|
-
def parse_marker_div(self, parent_marker_div,slide_information):
|
|
1919
|
-
"""Parse div containing markers, extracting position and data from each
|
|
1920
|
-
|
|
1921
|
-
:param parent_marker_div: Div containing all current Markers
|
|
1922
|
-
:type parent_marker_div: list
|
|
1923
|
-
:return: Coordinates for all markers and associated data (source structure and index)
|
|
1924
|
-
:rtype: tuple
|
|
1925
|
-
"""
|
|
1926
|
-
|
|
1927
|
-
marker_coords = []
|
|
1928
|
-
marker_data = []
|
|
1929
|
-
for p in parent_marker_div:
|
|
1930
|
-
coords = p['props']['position']
|
|
1931
|
-
data = json.loads(p['props']['children'][-1]['props']['children'])
|
|
1932
|
-
|
|
1933
|
-
marker_coords.append(
|
|
1934
|
-
[coords[1]/slide_information['x_scale'],coords[0]/slide_information['y_scale']]
|
|
1935
|
-
)
|
|
1936
|
-
marker_data.append(data)
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
return marker_coords, marker_data
|
|
1983
|
+
return values_selector
|
|
1940
1984
|
|
|
1941
|
-
def search_labels(self,
|
|
1985
|
+
def search_labels(self, marker_features, current_labels, label_type, label_text, label_method):
|
|
1942
1986
|
"""Checking current labels and adding the new label based on label_method (inclusive, exclusive, or unlabeled).
|
|
1943
1987
|
|
|
1944
|
-
:param
|
|
1945
|
-
:type
|
|
1988
|
+
:param marker_features: GeoJSON Features for new markers
|
|
1989
|
+
:type marker_features: list
|
|
1946
1990
|
:param current_labels: List of all current labels (dicts with keys: centroid, labels)
|
|
1947
1991
|
:type current_labels: list
|
|
1948
1992
|
:param label_type: String corresponding to the type for the new label
|
|
@@ -1959,8 +2003,10 @@ class BulkLabels(Tool):
|
|
|
1959
2003
|
else:
|
|
1960
2004
|
current_labels['labels'] = []
|
|
1961
2005
|
current_centroids = []
|
|
1962
|
-
for m_idx, m in enumerate(
|
|
1963
|
-
|
|
2006
|
+
for m_idx, m in enumerate(marker_features):
|
|
2007
|
+
m_centroid = m['geometry']['coordinates']
|
|
2008
|
+
m_id = m['properties']['_id']
|
|
2009
|
+
if m_centroid in current_centroids:
|
|
1964
2010
|
cent_idx = current_centroids.index(m)
|
|
1965
2011
|
if label_method=='in':
|
|
1966
2012
|
# Adding inclusive label to current set of labels
|
|
@@ -1996,7 +2042,8 @@ class BulkLabels(Tool):
|
|
|
1996
2042
|
else:
|
|
1997
2043
|
current_labels['labels'].append(
|
|
1998
2044
|
{
|
|
1999
|
-
'centroid':
|
|
2045
|
+
'centroid': m_centroid,
|
|
2046
|
+
'_id': m_id,
|
|
2000
2047
|
'labels': [{
|
|
2001
2048
|
'type': label_type,
|
|
2002
2049
|
'value': label_text
|
|
@@ -2049,57 +2096,14 @@ class BulkLabels(Tool):
|
|
|
2049
2096
|
|
|
2050
2097
|
scaled_markers = geojson.utils.map_geometries(lambda g: geojson.utils.map_tuples(lambda c: (c[0]/slide_information['x_scale'],c[1]/slide_information['y_scale']),g),current_markers)
|
|
2051
2098
|
|
|
2052
|
-
|
|
2053
|
-
labeled_items = self.search_labels(marker_positions, current_data, label_type, label_text, label_method)
|
|
2054
|
-
|
|
2055
|
-
#TODO: Applying property to structures
|
|
2056
|
-
"""
|
|
2057
|
-
ann_names_order = [i['properties']['name'] for i in current_annotations]
|
|
2058
|
-
apply_label_dict = {'type': label_type, 'value': label_text}
|
|
2059
|
-
for m_d in marker_data:
|
|
2060
|
-
ann_idx = ann_names_order.index(m_d['name'])
|
|
2061
|
-
if label_method=='in':
|
|
2062
|
-
if 'fusion_labels' in current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']:
|
|
2063
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'].append(
|
|
2064
|
-
apply_label_dict
|
|
2065
|
-
)
|
|
2066
|
-
else:
|
|
2067
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'] = [
|
|
2068
|
-
apply_label_dict
|
|
2069
|
-
]
|
|
2070
|
-
elif label_method=='over':
|
|
2071
|
-
if 'fusion_labels' in current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']:
|
|
2072
|
-
if any([label_type==i['type'] for i in current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels']]):
|
|
2073
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'] = [
|
|
2074
|
-
i if not i['type']==label_type else apply_label_dict
|
|
2075
|
-
for i in current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels']
|
|
2076
|
-
]
|
|
2077
|
-
else:
|
|
2078
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'].append(
|
|
2079
|
-
apply_label_dict
|
|
2080
|
-
)
|
|
2081
|
-
else:
|
|
2082
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'] = [
|
|
2083
|
-
apply_label_dict
|
|
2084
|
-
]
|
|
2085
|
-
elif label_method=='un':
|
|
2086
|
-
if 'fusion_labels' in current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels']:
|
|
2087
|
-
if not any([label_type==i['type'] for i in current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels']]):
|
|
2088
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'].append(
|
|
2089
|
-
apply_label_dict
|
|
2090
|
-
)
|
|
2091
|
-
else:
|
|
2092
|
-
current_annotations[ann_idx]['features'][m_d['feature_index']]['properties']['fusion_labels'] = [
|
|
2093
|
-
apply_label_dict
|
|
2094
|
-
]
|
|
2095
|
-
|
|
2096
|
-
"""
|
|
2099
|
+
labeled_items = self.search_labels(scaled_markers['features'], current_data, label_type, label_text, label_method)
|
|
2097
2100
|
|
|
2098
|
-
if 'labels' in current_data:
|
|
2101
|
+
if 'labels' in current_data and 'labels_metadata' in current_data:
|
|
2099
2102
|
current_data['labels'] = labeled_items['labels']
|
|
2100
2103
|
current_data['labels_metadata'].append(
|
|
2101
2104
|
{
|
|
2102
2105
|
'type': label_type,
|
|
2106
|
+
'label': label_text,
|
|
2103
2107
|
'rationale': label_rationale,
|
|
2104
2108
|
'source': label_source
|
|
2105
2109
|
}
|
|
@@ -2109,15 +2113,56 @@ class BulkLabels(Tool):
|
|
|
2109
2113
|
current_data['labels_metadata'] = [
|
|
2110
2114
|
{
|
|
2111
2115
|
'type': label_type,
|
|
2116
|
+
'label': label_text,
|
|
2112
2117
|
'rationale': label_rationale,
|
|
2113
2118
|
'source': label_source
|
|
2114
2119
|
}
|
|
2115
2120
|
]
|
|
2116
2121
|
|
|
2117
2122
|
new_data = json.dumps(current_data)
|
|
2118
|
-
#new_annotations = json.dumps(current_annotations)
|
|
2119
2123
|
|
|
2120
|
-
|
|
2124
|
+
# Creating a table for count of unique labels:
|
|
2125
|
+
labels = []
|
|
2126
|
+
for l in labeled_items['labels']:
|
|
2127
|
+
for m in l['labels']:
|
|
2128
|
+
# Accounting for multiple values for the same "type"
|
|
2129
|
+
l_dict = {
|
|
2130
|
+
'Label Type': m['type'],
|
|
2131
|
+
'Value': m['value']
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
labels.append(l_dict)
|
|
2135
|
+
|
|
2136
|
+
label_count_df = pd.DataFrame.from_records(labels).groupby(by='Label Type')
|
|
2137
|
+
label_counts = label_count_df.value_counts(['Value']).to_frame()
|
|
2138
|
+
label_counts.reset_index(inplace=True)
|
|
2139
|
+
label_counts.columns = ['Label Type','Label Value','Count']
|
|
2140
|
+
|
|
2141
|
+
label_count_table = dash_table.DataTable(
|
|
2142
|
+
columns = [{'name':i,'id':i,'selectable':True} for i in label_counts],
|
|
2143
|
+
data = label_counts.to_dict('records'),
|
|
2144
|
+
editable=True,
|
|
2145
|
+
row_deletable=True,
|
|
2146
|
+
sort_mode='multi',
|
|
2147
|
+
sort_action = 'native',
|
|
2148
|
+
page_current=0,
|
|
2149
|
+
page_size=5,
|
|
2150
|
+
style_cell = {
|
|
2151
|
+
'overflow':'hidden',
|
|
2152
|
+
'textOverflow':'ellipsis',
|
|
2153
|
+
'maxWidth':0
|
|
2154
|
+
},
|
|
2155
|
+
tooltip_data = [
|
|
2156
|
+
{
|
|
2157
|
+
column: {'value':str(value),'type':'markdown'}
|
|
2158
|
+
for column, value in row.items()
|
|
2159
|
+
} for row in label_counts.to_dict('records')
|
|
2160
|
+
],
|
|
2161
|
+
tooltip_duration = None,
|
|
2162
|
+
id = {'type': f'{self.component_prefix}-bulk-labels-label-table','index':0}
|
|
2163
|
+
)
|
|
2164
|
+
|
|
2165
|
+
return [new_data], ['success'], [label_count_table]
|
|
2121
2166
|
|
|
2122
2167
|
def refresh_labels(self, refresh_click, structure_options):
|
|
2123
2168
|
"""Clear current label components and start over
|
|
@@ -2143,6 +2188,8 @@ class BulkLabels(Tool):
|
|
|
2143
2188
|
rationale_disable = True
|
|
2144
2189
|
label_source = '`Label Source`'
|
|
2145
2190
|
apply_disable = True
|
|
2191
|
+
apply_style = 'primary'
|
|
2192
|
+
markers = []
|
|
2146
2193
|
|
|
2147
2194
|
include_options = [{'label': i, 'value': i} for i in structure_options]
|
|
2148
2195
|
if len(ctx.outputs_list[13])>0:
|
|
@@ -2150,7 +2197,7 @@ class BulkLabels(Tool):
|
|
|
2150
2197
|
else:
|
|
2151
2198
|
spatial_queries_structures = []
|
|
2152
2199
|
|
|
2153
|
-
return [include_structures], [spatial_queries], [add_property], [current_structures],[label_type],[type_disable],[label_text], [text_disable], [label_rationale], [rationale_disable], [label_source], [apply_disable], [include_options], spatial_queries_structures
|
|
2200
|
+
return [include_structures], [spatial_queries], [add_property], [current_structures],[label_type],[type_disable],[label_text], [text_disable], [label_rationale], [rationale_disable], [label_source], [apply_disable], [include_options], spatial_queries_structures, [apply_style], [markers]
|
|
2154
2201
|
else:
|
|
2155
2202
|
raise exceptions.PreventUpdate
|
|
2156
2203
|
|
|
@@ -2184,4 +2231,105 @@ class BulkLabels(Tool):
|
|
|
2184
2231
|
)
|
|
2185
2232
|
]
|
|
2186
2233
|
|
|
2187
|
-
return new_structure_count_children
|
|
2234
|
+
return new_structure_count_children
|
|
2235
|
+
|
|
2236
|
+
def add_labels_to_structures(self, button_click, current_annotations, current_labels):
|
|
2237
|
+
|
|
2238
|
+
if not any([i['value'] for i in ctx.triggered]):
|
|
2239
|
+
raise exceptions.PreventUpdate
|
|
2240
|
+
|
|
2241
|
+
current_annotations = json.loads(get_pattern_matching_value(current_annotations))
|
|
2242
|
+
current_labels = json.loads(get_pattern_matching_value(current_labels))
|
|
2243
|
+
|
|
2244
|
+
label_ids = [i['_id'] for i in current_labels['labels']]
|
|
2245
|
+
for c in current_annotations:
|
|
2246
|
+
feature_ids = [i['properties']['_id'] for i in c['features']]
|
|
2247
|
+
id_intersect = list(set(label_ids) & set(feature_ids))
|
|
2248
|
+
if len(id_intersect)>0:
|
|
2249
|
+
for id in id_intersect:
|
|
2250
|
+
feature_label = current_labels['labels'][label_ids.index(id)]
|
|
2251
|
+
print(feature_label)
|
|
2252
|
+
c['features'][feature_ids.index(id)]['properties'] = c['features'][feature_ids.index(id)]['properties'] | current_labels['labels'][label_ids.index(id)]
|
|
2253
|
+
|
|
2254
|
+
updated_annotations = json.dumps(current_annotations)
|
|
2255
|
+
|
|
2256
|
+
return [updated_annotations]
|
|
2257
|
+
|
|
2258
|
+
def update_label_table(self, table_data, current_labels):
|
|
2259
|
+
|
|
2260
|
+
table_data = get_pattern_matching_value(table_data)
|
|
2261
|
+
current_labels = json.loads(get_pattern_matching_value(current_labels))
|
|
2262
|
+
if not table_data is None:
|
|
2263
|
+
included_label_types = sorted(list(set([i['Label Type'] for i in table_data])))
|
|
2264
|
+
current_label_types = sorted(list(set([i['type'] for i in current_labels['labels_metadata']])))
|
|
2265
|
+
|
|
2266
|
+
included_label_values = []
|
|
2267
|
+
current_label_values = []
|
|
2268
|
+
for t in included_label_types:
|
|
2269
|
+
included_label_values.extend(sorted([i['Label Value'] for i in table_data if i['Label Type']==t]))
|
|
2270
|
+
current_label_values.extend(sorted([i['label'] for i in current_labels['labels_metadata'] if i['type']==t]))
|
|
2271
|
+
|
|
2272
|
+
# Removing not-included label from labels and label metadata
|
|
2273
|
+
if not included_label_types==current_label_types or not included_label_values==current_label_values:
|
|
2274
|
+
# If the len of included_label_types does not equal the len of current_label_types then there has been a type deletion
|
|
2275
|
+
deletion = False
|
|
2276
|
+
replation = False
|
|
2277
|
+
if not len(included_label_types)==len(current_label_types):
|
|
2278
|
+
# This would be a deletion of one label type/value
|
|
2279
|
+
#print(f'type deletion: {included_label_types}, {current_label_types}')
|
|
2280
|
+
deletion = True
|
|
2281
|
+
replation = False
|
|
2282
|
+
else:
|
|
2283
|
+
# If the len of the types are the same length then check if they contain the same values:
|
|
2284
|
+
if not included_label_types==current_label_types:
|
|
2285
|
+
# This is a type replation
|
|
2286
|
+
#print(f'type replation: {included_label_types}, {current_label_types}')
|
|
2287
|
+
deletion = False
|
|
2288
|
+
replation = {'type': [i for i in included_label_types if not i in current_label_types][0]}
|
|
2289
|
+
|
|
2290
|
+
else:
|
|
2291
|
+
# In this case there is most likely a value replation
|
|
2292
|
+
if not included_label_values==current_label_values:
|
|
2293
|
+
#print(f'value replation: {included_label_values}, {current_label_values}')
|
|
2294
|
+
deletion = False
|
|
2295
|
+
replation = {'value': [i for i in included_label_values if not i in current_label_values][0]}
|
|
2296
|
+
|
|
2297
|
+
for l_idx,l in enumerate(current_labels['labels']):
|
|
2298
|
+
for l_m_idx,l_m in enumerate(l['labels']):
|
|
2299
|
+
if not l_m['type'] in included_label_types:
|
|
2300
|
+
if deletion:
|
|
2301
|
+
del current_labels['labels'][l_idx]['labels'][l_m_idx]
|
|
2302
|
+
elif replation:
|
|
2303
|
+
current_labels['labels'][l_idx]['labels'][l_m_idx]['type'] = replation['type']
|
|
2304
|
+
else:
|
|
2305
|
+
if not l_m['value'] in included_label_values:
|
|
2306
|
+
if deletion:
|
|
2307
|
+
del current_labels['labels'][l_idx]['labels'][l_m_idx]
|
|
2308
|
+
elif replation:
|
|
2309
|
+
current_labels['labels'][l_idx]['labels'][l_m_idx]['value'] = replation['value']
|
|
2310
|
+
|
|
2311
|
+
if len(l['labels'])==0:
|
|
2312
|
+
del current_labels['labels'][l_idx]
|
|
2313
|
+
|
|
2314
|
+
if deletion:
|
|
2315
|
+
current_labels['labels_metadata'] = [i for i in current_labels['labels_metadata'] if i['label'] in included_label_values and i['type'] in included_label_types]
|
|
2316
|
+
elif replation:
|
|
2317
|
+
if 'type' in replation:
|
|
2318
|
+
current_labels['labels_metadata'] = [i if i['type'] in included_label_types else i | replation for i in current_labels['labels_metadata']]
|
|
2319
|
+
elif 'value' in replation:
|
|
2320
|
+
current_labels['labels_metadata'] = [i if i['label'] in included_label_values else i | {'label': replation['value']} for i in current_labels['labels_metadata']]
|
|
2321
|
+
|
|
2322
|
+
updated_labels = json.dumps(current_labels)
|
|
2323
|
+
else:
|
|
2324
|
+
raise exceptions.PreventUpdate
|
|
2325
|
+
else:
|
|
2326
|
+
updated_labels = json.dumps({})
|
|
2327
|
+
|
|
2328
|
+
return [updated_labels]
|
|
2329
|
+
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions relating to images
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing_extensions import Union
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from shapely.geometry import shape
|
|
10
|
+
from skimage.draw import polygon2mask
|
|
11
|
+
|
|
12
|
+
import large_image
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_style_dict(channel_colors:list, tile_source):
|
|
16
|
+
"""Generate style dictionary that can be used in association with large_image.open("/path/to/file.ext",style={}) to view different channels as different colors
|
|
17
|
+
|
|
18
|
+
:param channel_colors: List of dictionaries where each key is a channel name found in "tile_source" and each value is an rgba() CSS color string
|
|
19
|
+
:type channel_colors: list
|
|
20
|
+
:param tile_source: Large-image tile source object used to open image
|
|
21
|
+
:type tile_source: None
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
assert hasattr(tile_source,"getMetadata")
|
|
25
|
+
|
|
26
|
+
tile_metadata = tile_source.getMetadata()
|
|
27
|
+
|
|
28
|
+
if 'frames' in tile_metadata:
|
|
29
|
+
frame_names = [i['Channel'] if 'Channel' in i else idx for idx,i in enumerate(tile_metadata['frames'])]
|
|
30
|
+
style_dict = {"bands": []}
|
|
31
|
+
for c in channel_colors:
|
|
32
|
+
style_dict["bands"].append(
|
|
33
|
+
{
|
|
34
|
+
'palette': ['rgba(0,0,0,255)',list(c.values())[0]],
|
|
35
|
+
'framedelta': frame_names.index(list(c.keys())[0])
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return style_dict
|
|
40
|
+
else:
|
|
41
|
+
raise TypeError('tile_source is not a multi-frame image')
|
|
42
|
+
|
|
43
|
+
def get_feature_image(feature:dict, tile_source:None, return_mask: bool=False):
|
|
44
|
+
"""Extract image region associated with a given feature from tile_source (a large-image object)
|
|
45
|
+
|
|
46
|
+
:param feature: GeoJSON Feature with "geometry" field containing coordinates
|
|
47
|
+
:type feature: dict
|
|
48
|
+
:param tile_source: A large-image tile source object (or custom object with "getRegion" method)
|
|
49
|
+
:type tile_source: None
|
|
50
|
+
:param return_mask: Whether or not to return both the image region (bounding box) as well as a binary mask of the boundaries of that feature, defaults to False
|
|
51
|
+
:type return_mask: bool, optional
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Getting bounding box of feature:
|
|
55
|
+
if not 'geometry' in feature:
|
|
56
|
+
raise ValueError(f"Feature does not contain 'geometry' key: {list(feature.keys())}")
|
|
57
|
+
|
|
58
|
+
feature_shape = shape(feature['geometry'])
|
|
59
|
+
# This will only work with geometries that have an "exterior", so not Point
|
|
60
|
+
if not feature_shape.geom_type=='Point':
|
|
61
|
+
feature_bounds = list(feature_shape.exterior.bounds)
|
|
62
|
+
else:
|
|
63
|
+
raise TypeError("Feature geometries of type: 'Point' are not implemented, try again using .buffer(1) or other to extract a valid bounding box")
|
|
64
|
+
|
|
65
|
+
feature_image, mime_type = tile_source.getRegion(
|
|
66
|
+
region = {
|
|
67
|
+
'left': feature_bounds[0],
|
|
68
|
+
'top': feature_bounds[1],
|
|
69
|
+
'right': feature_bounds[2],
|
|
70
|
+
'bottom': feature_bounds[3]
|
|
71
|
+
},
|
|
72
|
+
format = large_image.constants.TILE_FORMAT_NUMPY
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if return_mask:
|
|
76
|
+
|
|
77
|
+
height = feature_image.shape[0]
|
|
78
|
+
width = feature_image.shape[1]
|
|
79
|
+
|
|
80
|
+
# Scaling exterior coordinates to fit within bounding box
|
|
81
|
+
top_left = feature_bounds[0:2]
|
|
82
|
+
scaled_coords = np.squeeze(feature_shape.exterior.coords) - np.array([top_left[0], top_left[1]])
|
|
83
|
+
|
|
84
|
+
# polygon2mask expects coordinates to be y,x and scaled_coords is x,y
|
|
85
|
+
feature_mask = polygon2mask(
|
|
86
|
+
image_shape = (height,width),
|
|
87
|
+
polygon = np.flip(scaled_coords,axis=1)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return feature_image, feature_mask
|
|
91
|
+
else:
|
|
92
|
+
return feature_image
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
@@ -21,6 +21,7 @@ from scipy import ndimage
|
|
|
21
21
|
|
|
22
22
|
import pandas as pd
|
|
23
23
|
import anndata as ad
|
|
24
|
+
import large_image
|
|
24
25
|
|
|
25
26
|
import uuid
|
|
26
27
|
|
|
@@ -740,6 +741,7 @@ def spatially_aggregate(child_geo:dict, parent_geos: list, separate: bool = True
|
|
|
740
741
|
'Sum': nested_sum
|
|
741
742
|
}
|
|
742
743
|
|
|
744
|
+
#TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
743
745
|
sub_keys = reversed(p.split(' --> '))
|
|
744
746
|
for p_idx,part in enumerate(sub_keys):
|
|
745
747
|
nested_dict = {part: nested_dict}
|
|
@@ -756,6 +758,7 @@ def spatially_aggregate(child_geo:dict, parent_geos: list, separate: bool = True
|
|
|
756
758
|
props = numeric_df.columns.tolist()
|
|
757
759
|
for p in props:
|
|
758
760
|
mean_dict = mean_props[p]
|
|
761
|
+
#TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
759
762
|
for part in reversed(p.split(' --> ')):
|
|
760
763
|
mean_dict = {part: mean_dict}
|
|
761
764
|
|
|
@@ -801,6 +804,7 @@ def spatially_aggregate(child_geo:dict, parent_geos: list, separate: bool = True
|
|
|
801
804
|
'Sum': nested_sum
|
|
802
805
|
}
|
|
803
806
|
|
|
807
|
+
#TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
804
808
|
sub_keys = reversed(p.split(' --> '))
|
|
805
809
|
for part in sub_keys:
|
|
806
810
|
nested_dict = {part: nested_dict}
|
|
@@ -819,6 +823,7 @@ def spatially_aggregate(child_geo:dict, parent_geos: list, separate: bool = True
|
|
|
819
823
|
props = numeric_df.columns.tolist()
|
|
820
824
|
for p in props:
|
|
821
825
|
mean_dict = mean_props[p]
|
|
826
|
+
#TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
822
827
|
for part in reversed(p.split(' --> ')):
|
|
823
828
|
mean_dict = {part: mean_dict}
|
|
824
829
|
|
|
@@ -860,18 +865,44 @@ def extract_nested_prop(main_prop_dict: dict, depth: int, path: tuple = (), valu
|
|
|
860
865
|
values_list.append({
|
|
861
866
|
' --> '.join(list(path+(keys,))): values
|
|
862
867
|
})
|
|
868
|
+
|
|
869
|
+
elif type(values)==list:
|
|
870
|
+
values_list.extend(extract_listed_prop,path+(keys,),[])
|
|
871
|
+
|
|
863
872
|
else:
|
|
864
|
-
# Skipping properties that are still nested
|
|
873
|
+
# Skipping properties that are still nested
|
|
865
874
|
continue
|
|
866
875
|
else:
|
|
867
876
|
if type(values)==dict:
|
|
868
877
|
extract_nested_prop(values, depth-1, path+ (keys,), values_list)
|
|
869
|
-
|
|
878
|
+
elif type(values)==list:
|
|
879
|
+
values_list.extend(extract_listed_prop,path+(keys,),[])
|
|
880
|
+
elif type(values) in [int,float,str]:
|
|
870
881
|
# Only adding properties to the list one time
|
|
871
882
|
if not any([' --> '.join(list(path+(keys,))) in list(i.keys()) for i in values_list]):
|
|
872
883
|
values_list.append({
|
|
873
884
|
' --> '.join(list(path+(keys,))): values
|
|
874
885
|
})
|
|
886
|
+
else:
|
|
887
|
+
# Skipping properties of some mysterious fifth type
|
|
888
|
+
continue
|
|
889
|
+
|
|
890
|
+
return values_list
|
|
891
|
+
|
|
892
|
+
def extract_listed_prop(main_list: list, path: tuple = (), values_list: list = []):
|
|
893
|
+
|
|
894
|
+
if len(main_list)>0:
|
|
895
|
+
for item_idx,list_item in enumerate(main_list):
|
|
896
|
+
item_key = f'Value {item_idx}'
|
|
897
|
+
if type(list_item) in [int,float,str]:
|
|
898
|
+
values_list.append({
|
|
899
|
+
' --+ '.join(list(path+(item_key,))): list_item
|
|
900
|
+
})
|
|
901
|
+
elif type(list_item)==dict:
|
|
902
|
+
nested_n = find_nested_levels({item_key:list_item})
|
|
903
|
+
values_list.extend(extract_nested_prop({item_key: list_item},nested_n,path+(item_key,),[]))
|
|
904
|
+
elif type(list_item)==list:
|
|
905
|
+
extract_listed_prop(list_item,path+(item_key,),[])
|
|
875
906
|
|
|
876
907
|
return values_list
|
|
877
908
|
|
|
@@ -916,8 +947,13 @@ def extract_geojson_properties(geo_list: list, reference_object: Union[str,None]
|
|
|
916
947
|
for p in f_props:
|
|
917
948
|
# Checking for sub-properties
|
|
918
949
|
sub_props = []
|
|
919
|
-
if type(f['properties'][p])
|
|
920
|
-
|
|
950
|
+
if type(f['properties'][p]) in [dict,list]:
|
|
951
|
+
# Pulling out nested properties (either dictionaries or lists or lists of dictionaries or dictionaries of lists, etc.)
|
|
952
|
+
if type(f['properties'][p]) ==dict:
|
|
953
|
+
nested_value = extract_nested_prop({p: f['properties'][p]}, nested_depth, (), [])
|
|
954
|
+
elif type(f['properties'][p])==list:
|
|
955
|
+
nested_value = extract_listed_prop(f['properties'][p],(p,),[])
|
|
956
|
+
|
|
921
957
|
if len(nested_value)>0:
|
|
922
958
|
for n in nested_value:
|
|
923
959
|
n_key = list(n.keys())[0]
|
|
@@ -950,7 +986,7 @@ def extract_geojson_properties(geo_list: list, reference_object: Union[str,None]
|
|
|
950
986
|
property_info[n_key]['unique'].append(n_value)
|
|
951
987
|
property_info[n_key]['distinct'] +=1
|
|
952
988
|
|
|
953
|
-
|
|
989
|
+
elif type(f['properties'][p]) in [int,float,str]:
|
|
954
990
|
f_sup_val = f['properties'][p]
|
|
955
991
|
|
|
956
992
|
if not p in property_info:
|
|
@@ -986,7 +1022,6 @@ def extract_geojson_properties(geo_list: list, reference_object: Union[str,None]
|
|
|
986
1022
|
|
|
987
1023
|
#TODO: After loading an experiment, reference the file here for additional properties
|
|
988
1024
|
|
|
989
|
-
|
|
990
1025
|
|
|
991
1026
|
geojson_properties = sorted(geojson_properties)
|
|
992
1027
|
|
|
@@ -1090,6 +1125,7 @@ def process_filters_queries(filter_list:list, spatial_list:list, structures:list
|
|
|
1090
1125
|
|
|
1091
1126
|
for f in filter_list:
|
|
1092
1127
|
if include:
|
|
1128
|
+
#TODO: Update this for different types of nested props (--+ = list, --# = external reference object)
|
|
1093
1129
|
filter_name_parts = f['name'].split(' --> ')
|
|
1094
1130
|
|
|
1095
1131
|
include = True
|
|
@@ -1165,4 +1201,50 @@ def path_to_mask(path, shape):
|
|
|
1165
1201
|
mask = ndimage.binary_fill_holes(mask)
|
|
1166
1202
|
return mask
|
|
1167
1203
|
|
|
1204
|
+
def get_feature_image(feature:dict, tile_source:None, return_mask: bool=False, style: Union[None,list,dict]=None):
|
|
1205
|
+
"""Extract image region associated with a given feature from tile_source (a large-image object)
|
|
1206
|
+
|
|
1207
|
+
:param feature: GeoJSON Feature with "geometry" field containing coordinates
|
|
1208
|
+
:type feature: dict
|
|
1209
|
+
:param tile_source: A large-image tile source object (or custom object with "getRegion" method)
|
|
1210
|
+
:type tile_source: None
|
|
1211
|
+
:param return_mask: Whether or not to return both the image region (bounding box) as well as a binary mask of the boundaries of that feature, defaults to False
|
|
1212
|
+
:type return_mask: bool, optional
|
|
1213
|
+
:param style: For multi-frame tile sources, this can either be official "bands": [] format as described by large-image or a list containing {channel_name: 'rgba()'} keys and values, defaults to None
|
|
1214
|
+
:type style: Union[None,list,dict], optional
|
|
1215
|
+
"""
|
|
1216
|
+
|
|
1217
|
+
# Getting bounding box of feature:
|
|
1218
|
+
if not 'geometry' in feature:
|
|
1219
|
+
raise ValueError(f"Feature does not contain 'geometry' key: {list(feature.keys())}")
|
|
1220
|
+
|
|
1221
|
+
feature_shape = shape(feature['geometry'])
|
|
1222
|
+
# This will only work with geometries that have an "exterior", so not Point
|
|
1223
|
+
if not feature_shape.geom_type=='Point':
|
|
1224
|
+
feature_bounds = list(feature_shape.exterior.bounds)
|
|
1225
|
+
else:
|
|
1226
|
+
raise TypeError("Feature geometries of type: 'Point' are not implemented, try again using .buffer(1) or other to extract a valid bounding box")
|
|
1227
|
+
|
|
1228
|
+
if style is None:
|
|
1229
|
+
|
|
1230
|
+
feature_image, mime_type = tile_source.getRegion(
|
|
1231
|
+
region = {
|
|
1232
|
+
'left': feature_bounds[0],
|
|
1233
|
+
'top': feature_bounds[1],
|
|
1234
|
+
'right': feature_bounds[2],
|
|
1235
|
+
'bottom': feature_bounds[3]
|
|
1236
|
+
},
|
|
1237
|
+
format = large_image.constants.TILE_FORMAT_NUMPY
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
|
|
1168
1250
|
|
|
@@ -17,6 +17,7 @@ src/fusion_tools/components/maps.py
|
|
|
17
17
|
src/fusion_tools/components/segmentation.py
|
|
18
18
|
src/fusion_tools/components/tools.py
|
|
19
19
|
src/fusion_tools/utils/__init__.py
|
|
20
|
+
src/fusion_tools/utils/images.py
|
|
20
21
|
src/fusion_tools/utils/omics.py
|
|
21
22
|
src/fusion_tools/utils/shapes.py
|
|
22
23
|
src/fusion_tools/utils/stats.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|