supervisely 6.73.438__py3-none-any.whl → 6.73.513__py3-none-any.whl

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 (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,564 @@
1
+ from pathlib import Path
2
+ from typing import Any, Callable, List, Optional, Union
3
+ from urllib.parse import urlparse
4
+
5
+ import cv2
6
+ import numpy as np
7
+
8
+ from supervisely._utils import logger
9
+ from supervisely.annotation.annotation import Annotation
10
+ from supervisely.app.content import DataJson, StateJson
11
+ from supervisely.app.widgets import Widget
12
+ from supervisely.app.widgets_context import JinjaWidgets
13
+ from supervisely.imaging.image import np_image_to_data_url, read
14
+
15
+
16
+ def mask_to_heatmap(
17
+ mask: np.ndarray, colormap=cv2.COLORMAP_JET, transparent_low=False, vmin=None, vmax=None, blur_ksize=(5,5), blur_function: Optional[Callable[[np.ndarray], np.ndarray]] = None
18
+ ):
19
+ if mask.ndim == 3:
20
+ mask_gray = mask.mean(axis=-1)
21
+ else:
22
+ mask_gray = mask.copy()
23
+ mask_gray = mask_gray.astype(np.float64)
24
+ if vmin is None:
25
+ vmin = np.nanmin(mask_gray)
26
+ if vmax is None:
27
+ vmax = np.nanmax(mask_gray)
28
+
29
+ if vmax == vmin:
30
+ mask_norm = np.full_like(mask_gray, 128, dtype=np.uint8)
31
+ else:
32
+ mask_norm = ((mask_gray - vmin) / (vmax - vmin) * 255).astype(np.uint8)
33
+ if transparent_low:
34
+ zeros = mask_norm == mask_norm.min()
35
+ if blur_function is not None:
36
+ mask_norm = blur_function(mask_norm)
37
+ elif blur_ksize is not None:
38
+ mask_norm = cv2.GaussianBlur(mask_norm, blur_ksize, 0)
39
+ heatmap_bgr = cv2.applyColorMap(mask_norm, colormap)
40
+ heatmap_bgra = cv2.cvtColor(heatmap_bgr, cv2.COLOR_BGR2BGRA)
41
+ if transparent_low:
42
+ alpha = np.where(zeros, 0, 255).astype(np.uint8)
43
+ heatmap_bgra[..., 3] = alpha
44
+ return heatmap_bgra
45
+
46
+
47
+ def colormap_to_hex_list(colormap=cv2.COLORMAP_JET, n=5):
48
+ values = np.linspace(0, 255, n, dtype=np.uint8)
49
+ colors_bgr = cv2.applyColorMap(values[:, None], colormap)
50
+ colors_rgb = colors_bgr[:, 0, ::-1]
51
+ return [f"#{r:02X}{g:02X}{b:02X}" for r, g, b in colors_rgb]
52
+
53
+
54
+ def to_json_safe(val):
55
+ if val is None:
56
+ return None
57
+ if isinstance(val, (np.integer, int)):
58
+ return int(val)
59
+ if isinstance(val, (np.floating, float)):
60
+ return float(val)
61
+ return str(val)
62
+
63
+
64
+ class Heatmap(Widget):
65
+ """
66
+ Supervisely widget that displays an interactive heatmap overlay on top of a background image.
67
+
68
+ :param background_image: Background image to display under the heatmap. Can be a path to an image file or a NumPy array
69
+ :type background_image: Union[str, np.ndarray], optional
70
+ :param heatmap_mask: NumPy array representing the heatmap mask values
71
+ :type heatmap_mask: np.ndarray, optional
72
+ :param vmin: Minimum value for normalizing the heatmap. If None, it is inferred from the mask
73
+ :type vmin: Any, optional
74
+ :param vmax: Maximum value for normalizing the heatmap. If None, it is inferred from the mask
75
+ :type vmax: Any, optional
76
+ :param transparent_low: Whether to make low values in the heatmap transparent
77
+ :type transparent_low: bool, optional
78
+ :param colormap: OpenCV colormap used to colorize the heatmap (e.g., cv2.COLORMAP_JET)
79
+ :type colormap: int, optional
80
+ :param width: Width of the output heatmap in pixels
81
+ :type width: int, optional
82
+ :param height: Height of the output heatmap in pixels
83
+ :type height: int, optional
84
+ :param blur_ksize: Kernel size for Gaussian blur applied to the heatmap mask. Set to None to disable blurring
85
+ :type blur_ksize: Optional[Union[tuple, None]], optional
86
+ :param blur_function: Custom function to apply blurring to the heatmap mask. Overrides blur_ksize if provided
87
+ :type blur_function: Optional[Callable[[np.ndarray], np.ndarray]], optional
88
+ :param widget_id: Unique identifier for the widget instance
89
+ :type widget_id: str, optional
90
+
91
+ This widget provides an interactive visualization for numerical data as colored overlays.
92
+ Users can click on the heatmap to get exact values at specific coordinates.
93
+ The widget supports various colormaps, transparency controls, and value normalization.
94
+
95
+ Blurring can be applied to the heatmap visualization using either a Gaussian kernel (specified by `blur_ksize`)
96
+ or a custom blurring function (specified by `blur_function`). If both are provided, the custom function takes precedence.
97
+ Blurring only affects the visual representation of the heatmap and does not modify the underlying data.
98
+
99
+ :Usage example:
100
+
101
+ .. code-block:: python
102
+
103
+ import numpy as np
104
+ from supervisely.app.widgets import Heatmap
105
+
106
+ # Create temperature heatmap
107
+ temp_data = np.random.uniform(-20, 40, size=(100, 100))
108
+ heatmap = Heatmap(
109
+ background_image="/path/to/background.jpg",
110
+ heatmap_mask=temp_data,
111
+ vmin=-20,
112
+ vmax=40,
113
+ colormap=cv2.COLORMAP_JET
114
+ )
115
+
116
+ @heatmap.click
117
+ def handle_click(y: int, x: int, value: float):
118
+ print(f"Temperature at ({x}, {y}): {value:.1f}°C")
119
+
120
+
121
+ :Custom blur function example:
122
+
123
+ .. code-block:: python
124
+
125
+ from functools import partial
126
+ import cv2
127
+
128
+ # Using a predefined OpenCV function with fixed kernel size
129
+ blur_f = partial(cv2.medianBlur, ksize=5)
130
+ # Or define a custom blur function
131
+ def blur_f(img):
132
+ ksize = img.shape[0] // 20
133
+ if ksize % 2 == 0:
134
+ ksize += 1
135
+ return cv2.GaussianBlur(img, (ksize, ksize), 0)
136
+ heatmap = Heatmap(
137
+ heatmap_mask=temp_data,
138
+ blur_function=blur_f
139
+ )
140
+ """
141
+
142
+ class Routes:
143
+ CLICK = "heatmap_clicked_cb"
144
+
145
+ def __init__(
146
+ self,
147
+ background_image: Union[str, np.ndarray] = None,
148
+ heatmap_mask: np.ndarray = None,
149
+ vmin: Any = None,
150
+ vmax: Any = None,
151
+ transparent_low: bool = False,
152
+ colormap: int = cv2.COLORMAP_JET,
153
+ width: int = None,
154
+ height: int = None,
155
+ blur_ksize: Optional[Union[tuple, None]] = (5,5),
156
+ blur_function: Optional[Callable[[np.ndarray], np.ndarray]] = None,
157
+ widget_id: str = None,
158
+ ):
159
+ self._background_url = None
160
+ self._heatmap_url = None
161
+ self._mask_data = None # Store numpy array for efficient value lookup
162
+ self._click_callback = None # Optional user callback
163
+ self._vmin = vmin
164
+ self._vmax = vmax
165
+ self._transparent_low = transparent_low
166
+ self._colormap = colormap
167
+ self._width = width
168
+ self._height = height
169
+ self._opacity = 70
170
+ self._min_value = 0
171
+ self._max_value = 0
172
+ self._blur_ksize = blur_ksize
173
+ self._blur_function = blur_function
174
+
175
+ super().__init__(widget_id, file_path=__file__)
176
+
177
+ if background_image is not None:
178
+ self.set_background(background_image)
179
+
180
+ if heatmap_mask is not None:
181
+ self.set_heatmap(heatmap_mask)
182
+
183
+ script_path = "./sly/css/app/widgets/heatmap/script.js"
184
+ JinjaWidgets().context["__widget_scripts__"][self.__class__.__name__] = script_path
185
+
186
+ # Register default click handler to update value from server-side mask
187
+ self._register_click_handler()
188
+
189
+ def get_json_data(self):
190
+ # Get mask dimensions if available
191
+ mask_height, mask_width = 0, 0
192
+ if self._mask_data is not None:
193
+ mask_height, mask_width = self._mask_data.shape[:2]
194
+
195
+ return {
196
+ "backgroundUrl": self._background_url,
197
+ "heatmapUrl": self._heatmap_url,
198
+ "width": self._width,
199
+ "height": self._height,
200
+ "maskWidth": mask_width,
201
+ "maskHeight": mask_height,
202
+ "minValue": self._min_value,
203
+ "maxValue": self._max_value,
204
+ "legendColors": colormap_to_hex_list(self._colormap),
205
+ }
206
+
207
+ def get_json_state(self):
208
+ return {"opacity": self._opacity, "clickedValue": None, "maskX": None, "maskY": None}
209
+
210
+ def set_background(self, background_image: Union[str, np.ndarray]):
211
+ """
212
+ Sets the background image that will be displayed under the heatmap overlay.
213
+
214
+ :param background_image: Background image source. Can be a file path, URL, or NumPy array
215
+ :type background_image: Union[str, np.ndarray]
216
+ :raises ValueError: If the background image type is unsupported or file path doesn't exist
217
+ :raises Exception: If there's an error during image processing or file operations
218
+
219
+ This method handles three types of background images:
220
+ 1. **NumPy array**: Converts to PNG and encodes as data URL
221
+ 2. **HTTP/HTTPS URL**: Uses the URL directly for remote images
222
+ 3. **Local file path**: Reads file and encodes as data URL
223
+
224
+ All images are converted to data URLs for efficient in-memory serving.
225
+
226
+ :Usage example:
227
+
228
+ .. code-block:: python
229
+
230
+ from supervisely.app.widgets.heatmap import Heatmap
231
+ import numpy as np
232
+ heatmap = Heatmap()
233
+
234
+ # Using a local file path
235
+ heatmap.set_background("/path/to/image.jpg")
236
+
237
+ # Using a NumPy array (RGB image)
238
+ bg_array = np.random.randint(0, 255, size=(480, 640, 3), dtype=np.uint8)
239
+ heatmap.set_background(bg_array)
240
+
241
+ # Using a remote URL
242
+ heatmap.set_background("https://example.com/background.png")
243
+ """
244
+ try:
245
+ if isinstance(background_image, np.ndarray):
246
+ # rgb or rgba to bgr or bgra
247
+ background_image = background_image[..., ::-1]
248
+ self._background_url = np_image_to_data_url(background_image)
249
+ elif isinstance(background_image, str):
250
+ parsed = urlparse(background_image)
251
+ bg_image_path = Path(background_image)
252
+ if parsed.scheme in ("http", "https") and parsed.netloc:
253
+ self._background_url = background_image
254
+ elif parsed.scheme == "data":
255
+ self._background_url = background_image
256
+ elif bg_image_path.exists() and bg_image_path.is_file():
257
+ np_image = read(bg_image_path, remove_alpha_channel=False)
258
+ np_image = np_image[..., ::-1] # rgb or rgba to bgr or bgra
259
+ self._background_url = np_image_to_data_url(np_image)
260
+ else:
261
+ raise ValueError(f"Unable to find image at {background_image}")
262
+ else:
263
+ raise ValueError(f"Unsupported background_image type: {type(background_image)}")
264
+ except Exception as e:
265
+ logger.error(f"Error setting background: {e}", exc_info=True)
266
+ self._background_url = None
267
+ raise
268
+ finally:
269
+ DataJson()[self.widget_id]["backgroundUrl"] = self._background_url
270
+ DataJson().send_changes()
271
+
272
+ def set_heatmap(self, mask: np.ndarray):
273
+ """
274
+ Sets the heatmap mask data and generates a colorized PNG overlay.
275
+
276
+ :param mask: NumPy array representing the heatmap values to be displayed
277
+ :type mask: np.ndarray
278
+
279
+ :raises Exception: If there's an error during heatmap generation
280
+
281
+ The heatmap is converted to a data URL for efficient in-memory serving.
282
+
283
+ :Usage example:
284
+
285
+ .. code-block:: python
286
+
287
+ from supervisely.app.widgets.heatmap import Heatmap
288
+ import numpy as np
289
+
290
+ heatmap = Heatmap()
291
+
292
+ # Create probability heatmap (0.0 to 1.0)
293
+ probability_mask = np.random.uniform(0.0, 1.0, size=(100, 100))
294
+ heatmap.set_heatmap(probability_mask)
295
+
296
+ # Create temperature heatmap (-50 to 150)
297
+ temp_mask = np.random.uniform(-50, 150, size=(200, 300))
298
+ heatmap.set_heatmap(temp_mask)
299
+ """
300
+ try:
301
+ heatmap = mask_to_heatmap(
302
+ mask,
303
+ colormap=self._colormap,
304
+ vmin=self._vmin,
305
+ vmax=self._vmax,
306
+ transparent_low=self._transparent_low,
307
+ blur_ksize=self._blur_ksize,
308
+ blur_function=self._blur_function,
309
+ )
310
+ self._heatmap_url = np_image_to_data_url(heatmap)
311
+ self._min_value = to_json_safe(mask.min())
312
+ self._max_value = to_json_safe(mask.max())
313
+
314
+ # Store mask as numpy array for efficient server-side value lookup
315
+ self._mask_data = mask.copy()
316
+
317
+ except Exception as e:
318
+ logger.error(f"Error setting heatmap: {e}", exc_info=True)
319
+ self._heatmap_url = None
320
+ self._min_value = None
321
+ self._max_value = None
322
+ self._mask_data = None
323
+ raise
324
+ finally:
325
+ DataJson()[self.widget_id]["heatmapUrl"] = self._heatmap_url
326
+ DataJson()[self.widget_id]["minValue"] = self._min_value
327
+ DataJson()[self.widget_id]["maxValue"] = self._max_value
328
+
329
+ # Update mask dimensions
330
+ if self._mask_data is not None:
331
+ h, w = self._mask_data.shape[:2]
332
+ DataJson()[self.widget_id]["maskWidth"] = w
333
+ DataJson()[self.widget_id]["maskHeight"] = h
334
+ else:
335
+ DataJson()[self.widget_id]["maskWidth"] = 0
336
+ DataJson()[self.widget_id]["maskHeight"] = 0
337
+
338
+ # Don't send maskData - will be fetched on-demand when user clicks
339
+ DataJson().send_changes()
340
+
341
+ def set_heatmap_from_annotations(self, anns: List[Annotation], object_name: str = None):
342
+ """
343
+ Creates and sets a heatmap from Supervisely annotations showing object density/overlaps.
344
+
345
+ :param anns: List of Supervisely annotations to convert to heatmap
346
+ :type anns: List[Annotation]
347
+ :param object_name: Name of the object class to filter annotations by. If None, all objects are included
348
+ :type object_name: str, optional
349
+ :raises ValueError: If the annotations list is empty
350
+
351
+ This method creates a density heatmap mask by:
352
+ 1. Using widget dimensions (width/height) if specified, calculating missing dimension from aspect ratio
353
+ 2. Creating a zero-filled mask of the target size
354
+ 3. Drawing each matching label onto the mask, accumulating values
355
+ 4. Areas with overlapping objects will have higher values (brighter in heatmap)
356
+ 5. Setting the resulting density mask as the heatmap
357
+
358
+ :Usage example:
359
+
360
+ .. code-block:: python
361
+
362
+ from supervisely.annotation.annotation import Annotation
363
+
364
+ ann1 = Annotation.load_json_file("/path/to/ann1.json")
365
+ ann2 = Annotation.load_json_file("/path/to/ann2.json")
366
+ ann3 = Annotation.load_json_file("/path/to/ann3.json")
367
+ annotations = [ann1, ann2, ann3]
368
+ heatmap.set_heatmap_from_annotations(annotations, object_name="person")
369
+
370
+ """
371
+ if len(anns) == 0:
372
+ raise ValueError("Annotations list should have at least one element")
373
+
374
+ # Use widget dimensions if specified, otherwise calculate average from annotations
375
+ if self._width is not None and self._height is not None:
376
+ # Both dimensions specified - use them directly
377
+ target_size = (self._height, self._width)
378
+ elif self._width is not None or self._height is not None:
379
+ # Only one dimension specified - calculate the other from annotations aspect ratio
380
+ sizes = [ann.img_size for ann in anns]
381
+ avg_height = sum(size[0] for size in sizes) / len(sizes)
382
+ avg_width = sum(size[1] for size in sizes) / len(sizes)
383
+ aspect_ratio = avg_width / avg_height
384
+
385
+ if self._width is not None:
386
+ # Width specified, calculate height
387
+ target_height = int(round(self._width / aspect_ratio / 2) * 2)
388
+ target_size = (target_height, self._width)
389
+ else:
390
+ # Height specified, calculate width
391
+ target_width = int(round(self._height * aspect_ratio / 2) * 2)
392
+ target_size = (self._height, target_width)
393
+ else:
394
+ # No dimensions specified - calculate average size from annotations and round to even numbers
395
+ sizes = [ann.img_size for ann in anns]
396
+ target_size = (
397
+ int(round(sum(size[0] for size in sizes) / len(sizes) / 2) * 2),
398
+ int(round(sum(size[1] for size in sizes) / len(sizes) / 2) * 2),
399
+ )
400
+
401
+ # Count matching labels to determine max possible value
402
+ total_labels = 0
403
+ for ann in anns:
404
+ for label in ann.labels:
405
+ if object_name is None or label.obj_class.name == object_name:
406
+ total_labels += 1
407
+
408
+ if total_labels == 0:
409
+ raise ValueError(f"No labels found for object_name='{object_name}'")
410
+
411
+ # Create density mask that accumulates overlapping objects
412
+ mask = np.zeros(target_size, dtype=np.float32)
413
+
414
+ for ann in anns:
415
+ for label in ann.labels:
416
+ if object_name is None or label.obj_class.name == object_name:
417
+ # Create a resized label for the target mask size
418
+ resized_label = label.resize(ann.img_size, target_size)
419
+
420
+ # Create temporary mask for this label
421
+ temp_mask = np.zeros(target_size, dtype=np.float32)
422
+ resized_label.draw(temp_mask, color=1.0)
423
+
424
+ # Add to accumulating density mask (overlaps will sum up)
425
+ mask += temp_mask
426
+
427
+ logger.info(
428
+ f"Created density heatmap: {total_labels} labels, "
429
+ f"target size: {target_size}, "
430
+ f"max density: {mask.max():.1f}, "
431
+ f"avg density: {mask.mean():.3f}"
432
+ )
433
+
434
+ self.set_heatmap(mask)
435
+
436
+ @property
437
+ def opacity(self):
438
+ return StateJson()[self.widget_id]["opacity"]
439
+
440
+ @opacity.setter
441
+ def opacity(self, value: int):
442
+ value = max(0, value)
443
+ value = min(100, value)
444
+ StateJson()[self.widget_id]["opacity"] = value
445
+ StateJson().send_changes()
446
+
447
+ @property
448
+ def colormap(self):
449
+ return self._colormap
450
+
451
+ @colormap.setter
452
+ def colormap(self, value: int):
453
+ self._colormap = value
454
+ DataJson()[self.widget_id]["legendColors"] = colormap_to_hex_list(self._colormap)
455
+
456
+ @property
457
+ def vmin(self):
458
+ return self._vmin
459
+
460
+ @vmin.setter
461
+ def vmin(self, value):
462
+ self._vmin = value
463
+
464
+ @property
465
+ def vmax(self):
466
+ return self._vmax
467
+
468
+ @vmax.setter
469
+ def vmax(self, value):
470
+ self._vmax = value
471
+
472
+ @property
473
+ def width(self):
474
+ return self._width
475
+
476
+ @width.setter
477
+ def width(self, value):
478
+ self._width = value
479
+ DataJson()[self.widget_id]["width"] = self._width
480
+
481
+ @property
482
+ def height(self):
483
+ return self._height
484
+
485
+ @height.setter
486
+ def height(self, value):
487
+ self._height = value
488
+ DataJson()[self.widget_id]["height"] = self._height
489
+
490
+ @property
491
+ def click_x(self):
492
+ return StateJson()[self.widget_id]["maskX"]
493
+
494
+ @property
495
+ def click_y(self):
496
+ return StateJson()[self.widget_id]["maskY"]
497
+
498
+ @property
499
+ def click_value(self):
500
+ return StateJson()[self.widget_id]["clickedValue"]
501
+
502
+ def _register_click_handler(self):
503
+ """Register internal click handler to update value from server-side mask."""
504
+ route_path = self.get_route_path(self.Routes.CLICK)
505
+ server = self._sly_app.get_server()
506
+
507
+ @server.post(route_path)
508
+ def _click():
509
+ x = StateJson()[self.widget_id]["maskX"]
510
+ y = StateJson()[self.widget_id]["maskY"]
511
+
512
+ logger.debug(
513
+ f"Heatmap click: x={x}, y={y}, _mask_data shape={self._mask_data.shape if self._mask_data is not None else None}"
514
+ )
515
+
516
+ # Get value from server-side mask data
517
+ clicked_value = None
518
+ if self._mask_data is not None and x is not None and y is not None:
519
+ h, w = self._mask_data.shape[:2]
520
+ if 0 <= y < h and 0 <= x < w:
521
+ clicked_value = float(self._mask_data[y, x])
522
+ # Update state with the value
523
+ StateJson()[self.widget_id]["clickedValue"] = clicked_value
524
+ StateJson().send_changes()
525
+ logger.debug(f"Heatmap click value: {clicked_value}")
526
+ else:
527
+ logger.warning(f"Coordinates out of bounds: x={x}, y={y}, shape=({h}, {w})")
528
+ else:
529
+ if self._mask_data is None:
530
+ logger.warning("Mask data is None")
531
+ if x is None:
532
+ logger.warning("x coordinate is None")
533
+ if y is None:
534
+ logger.warning("y coordinate is None")
535
+
536
+ # Call user callback if registered
537
+ if self._click_callback is not None:
538
+ self._click_callback(y, x, clicked_value)
539
+
540
+ def click(self, func: Callable[[int, int, float], None]) -> Callable[[], None]:
541
+ """
542
+ Registers a callback for heatmap click events.
543
+
544
+ :param func: Callback function that receives click coordinates and value
545
+ :type func: Callable[[int, int, float], None]
546
+ :returns: The registered callback function
547
+ :rtype: Callable[[], None]
548
+
549
+ The callback receives coordinates in NumPy order (y, x, value), where:
550
+ - y: row index (height axis)
551
+ - x: column index (width axis)
552
+ - value: clicked pixel value (fetched from server-side mask)
553
+
554
+ :Usage example:
555
+
556
+ .. code-block:: python
557
+
558
+ @heatmap.click
559
+ def handle_click(y: int, x: int, value: float):
560
+ print(f"Clicked at row {y}, col {x}, value: {value}")
561
+
562
+ """
563
+ self._click_callback = func
564
+ return func