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.
Files changed (52) hide show
  1. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/PKG-INFO +1 -1
  2. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/setup.py +1 -1
  3. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/maps.py +2 -0
  4. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/segmentation.py +232 -84
  5. fusion_tools-2.4.4/src/fusion_tools/utils/images.py +133 -0
  6. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/shapes.py +88 -6
  7. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/PKG-INFO +1 -1
  8. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/SOURCES.txt +1 -0
  9. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/LICENSE +0 -0
  10. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/README.md +0 -0
  11. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/pyproject.toml +0 -0
  12. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/setup.cfg +0 -0
  13. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/__init__.py +0 -0
  14. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/__init__.py +0 -0
  15. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/components/tools.py +0 -0
  16. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/dataset.py +0 -0
  17. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/feature_extraction.py +0 -0
  18. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/handler.py +0 -0
  19. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/tileserver.py +0 -0
  20. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/__init__.py +0 -0
  21. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/omics.py +0 -0
  22. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/utils/stats.py +0 -0
  23. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/visualization/__init__.py +0 -0
  24. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/visualization/components.py +0 -0
  25. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools/visualization/vis_utils.py +0 -0
  26. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/dependency_links.txt +0 -0
  27. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/requires.txt +0 -0
  28. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/src/fusion_tools.egg-info/top_level.txt +0 -0
  29. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_classification_model.py +0 -0
  30. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_custom_component.py +0 -0
  31. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_dataset_classes.py +0 -0
  32. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_extract_nested_prop.py +0 -0
  33. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_extract_recursive.py +0 -0
  34. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_feature_annotation.py +0 -0
  35. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_feature_extraction.py +0 -0
  36. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_generate_property_dict.py +0 -0
  37. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_get_gene_info.py +0 -0
  38. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_hra_viewer.py +0 -0
  39. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_image_overlay.py +0 -0
  40. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_model_train.py +0 -0
  41. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_multiframe_rgb.py +0 -0
  42. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_multiframeslidemap.py +0 -0
  43. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_process.py +0 -0
  44. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_property_plotter.py +0 -0
  45. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_read_anndata.py +0 -0
  46. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_remote_slide_annotations.py +0 -0
  47. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_remote_tileserver.py +0 -0
  48. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_segmentation.py +0 -0
  49. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_slide_annotations.py +0 -0
  50. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_spatial_aggregation.py +0 -0
  51. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_tile_server.py +0 -0
  52. {fusion_tools-2.4.2 → fusion_tools-2.4.4}/tests/test_visualization_component.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fusion-tools
3
- Version: 2.4.2
3
+ Version: 2.4.4
4
4
  Summary: Modular visualization and analysis dashboard creation for high-resolution microscopy images
5
5
  Home-page: https://github.com/spborder/fusion-tools
6
6
  Author: Sam Border
@@ -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.2",
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, marker_centroids, current_labels, label_type, label_text, label_method):
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 marker_centroids: Centroids for all new markers
1945
- :type marker_centroids: list
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(marker_centroids):
1963
- if m in current_centroids:
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': m,
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
- marker_positions = [i['geometry']['coordinates'] for i in scaled_markers['features']]
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
- return [new_data]
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
- else:
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])==dict:
920
- nested_value = extract_nested_prop({p: f['properties'][p]}, nested_depth, (), [])
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
- else:
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fusion-tools
3
- Version: 2.4.2
3
+ Version: 2.4.4
4
4
  Summary: Modular visualization and analysis dashboard creation for high-resolution microscopy images
5
5
  Home-page: https://github.com/spborder/fusion-tools
6
6
  Author: Sam Border
@@ -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