supervisely 6.73.410__py3-none-any.whl → 6.73.470__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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

Files changed (190) hide show
  1. supervisely/__init__.py +136 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/annotation/label.py +80 -3
  5. supervisely/api/annotation_api.py +9 -9
  6. supervisely/api/api.py +67 -43
  7. supervisely/api/app_api.py +72 -5
  8. supervisely/api/dataset_api.py +108 -33
  9. supervisely/api/entity_annotation/figure_api.py +113 -49
  10. supervisely/api/image_api.py +82 -0
  11. supervisely/api/module_api.py +10 -0
  12. supervisely/api/nn/deploy_api.py +15 -9
  13. supervisely/api/nn/ecosystem_models_api.py +201 -0
  14. supervisely/api/nn/neural_network_api.py +12 -3
  15. supervisely/api/pointcloud/pointcloud_api.py +38 -0
  16. supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
  17. supervisely/api/project_api.py +213 -6
  18. supervisely/api/task_api.py +11 -1
  19. supervisely/api/video/video_annotation_api.py +4 -2
  20. supervisely/api/video/video_api.py +79 -1
  21. supervisely/api/video/video_figure_api.py +24 -11
  22. supervisely/api/volume/volume_api.py +38 -0
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +14 -6
  25. supervisely/app/fastapi/__init__.py +1 -0
  26. supervisely/app/fastapi/custom_static_files.py +1 -1
  27. supervisely/app/fastapi/multi_user.py +88 -0
  28. supervisely/app/fastapi/subapp.py +175 -42
  29. supervisely/app/fastapi/templating.py +1 -1
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +11 -1
  35. supervisely/app/widgets/agent_selector/template.html +1 -0
  36. supervisely/app/widgets/card/card.py +20 -0
  37. supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
  38. supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
  39. supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
  40. supervisely/app/widgets/dialog/dialog.py +12 -0
  41. supervisely/app/widgets/dialog/template.html +2 -1
  42. supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
  43. supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
  44. supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
  45. supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
  46. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
  47. supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
  48. supervisely/app/widgets/fast_table/fast_table.py +713 -126
  49. supervisely/app/widgets/fast_table/script.js +492 -95
  50. supervisely/app/widgets/fast_table/style.css +54 -0
  51. supervisely/app/widgets/fast_table/template.html +45 -5
  52. supervisely/app/widgets/heatmap/__init__.py +0 -0
  53. supervisely/app/widgets/heatmap/heatmap.py +523 -0
  54. supervisely/app/widgets/heatmap/script.js +378 -0
  55. supervisely/app/widgets/heatmap/style.css +227 -0
  56. supervisely/app/widgets/heatmap/template.html +21 -0
  57. supervisely/app/widgets/input_tag/input_tag.py +102 -15
  58. supervisely/app/widgets/input_tag_list/__init__.py +0 -0
  59. supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
  60. supervisely/app/widgets/input_tag_list/template.html +70 -0
  61. supervisely/app/widgets/radio_table/radio_table.py +10 -2
  62. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  63. supervisely/app/widgets/radio_tabs/template.html +1 -0
  64. supervisely/app/widgets/select/select.py +6 -4
  65. supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
  66. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
  67. supervisely/app/widgets/table/table.py +68 -13
  68. supervisely/app/widgets/tabs/tabs.py +22 -6
  69. supervisely/app/widgets/tabs/template.html +5 -1
  70. supervisely/app/widgets/transfer/style.css +3 -0
  71. supervisely/app/widgets/transfer/template.html +3 -1
  72. supervisely/app/widgets/transfer/transfer.py +48 -45
  73. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  74. supervisely/convert/image/csv/csv_converter.py +24 -15
  75. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
  76. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
  77. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
  78. supervisely/convert/video/video_converter.py +2 -2
  79. supervisely/geometry/polyline_3d.py +110 -0
  80. supervisely/io/env.py +161 -1
  81. supervisely/nn/artifacts/__init__.py +1 -1
  82. supervisely/nn/artifacts/artifacts.py +10 -2
  83. supervisely/nn/artifacts/detectron2.py +1 -0
  84. supervisely/nn/artifacts/hrda.py +1 -0
  85. supervisely/nn/artifacts/mmclassification.py +20 -0
  86. supervisely/nn/artifacts/mmdetection.py +5 -3
  87. supervisely/nn/artifacts/mmsegmentation.py +1 -0
  88. supervisely/nn/artifacts/ritm.py +1 -0
  89. supervisely/nn/artifacts/rtdetr.py +1 -0
  90. supervisely/nn/artifacts/unet.py +1 -0
  91. supervisely/nn/artifacts/utils.py +3 -0
  92. supervisely/nn/artifacts/yolov5.py +2 -0
  93. supervisely/nn/artifacts/yolov8.py +1 -0
  94. supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
  95. supervisely/nn/experiments.py +9 -0
  96. supervisely/nn/inference/cache.py +37 -17
  97. supervisely/nn/inference/gui/serving_gui_template.py +39 -13
  98. supervisely/nn/inference/inference.py +953 -211
  99. supervisely/nn/inference/inference_request.py +15 -8
  100. supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
  101. supervisely/nn/inference/object_detection/object_detection.py +1 -0
  102. supervisely/nn/inference/predict_app/__init__.py +0 -0
  103. supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
  104. supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
  105. supervisely/nn/inference/predict_app/gui/gui.py +915 -0
  106. supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
  107. supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
  108. supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
  109. supervisely/nn/inference/predict_app/gui/preview.py +93 -0
  110. supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
  111. supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
  112. supervisely/nn/inference/predict_app/gui/utils.py +399 -0
  113. supervisely/nn/inference/predict_app/predict_app.py +176 -0
  114. supervisely/nn/inference/session.py +47 -39
  115. supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
  116. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  117. supervisely/nn/inference/tracking/tracker_interface.py +4 -0
  118. supervisely/nn/inference/uploader.py +9 -5
  119. supervisely/nn/model/model_api.py +44 -22
  120. supervisely/nn/model/prediction.py +15 -1
  121. supervisely/nn/model/prediction_session.py +70 -14
  122. supervisely/nn/prediction_dto.py +7 -0
  123. supervisely/nn/tracker/__init__.py +6 -8
  124. supervisely/nn/tracker/base_tracker.py +54 -0
  125. supervisely/nn/tracker/botsort/__init__.py +1 -0
  126. supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
  127. supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
  128. supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
  129. supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
  130. supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
  131. supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
  132. supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
  133. supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
  134. supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
  135. supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
  136. supervisely/nn/tracker/botsort_tracker.py +273 -0
  137. supervisely/nn/tracker/calculate_metrics.py +264 -0
  138. supervisely/nn/tracker/utils.py +273 -0
  139. supervisely/nn/tracker/visualize.py +520 -0
  140. supervisely/nn/training/gui/gui.py +152 -49
  141. supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
  142. supervisely/nn/training/gui/model_selector.py +8 -6
  143. supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
  144. supervisely/nn/training/gui/training_artifacts.py +3 -1
  145. supervisely/nn/training/train_app.py +225 -46
  146. supervisely/project/pointcloud_episode_project.py +12 -8
  147. supervisely/project/pointcloud_project.py +12 -8
  148. supervisely/project/project.py +221 -75
  149. supervisely/template/experiment/experiment.html.jinja +105 -55
  150. supervisely/template/experiment/experiment_generator.py +258 -112
  151. supervisely/template/experiment/header.html.jinja +31 -13
  152. supervisely/template/experiment/sly-style.css +7 -2
  153. supervisely/versions.json +3 -1
  154. supervisely/video/sampling.py +42 -20
  155. supervisely/video/video.py +41 -12
  156. supervisely/video_annotation/video_figure.py +38 -4
  157. supervisely/volume/stl_converter.py +2 -0
  158. supervisely/worker_api/agent_rpc.py +24 -1
  159. supervisely/worker_api/rpc_servicer.py +31 -7
  160. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
  161. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
  162. supervisely_lib/__init__.py +6 -1
  163. supervisely/app/widgets/experiment_selector/style.css +0 -27
  164. supervisely/app/widgets/experiment_selector/template.html +0 -61
  165. supervisely/nn/tracker/bot_sort/__init__.py +0 -21
  166. supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
  167. supervisely/nn/tracker/bot_sort/matching.py +0 -127
  168. supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
  169. supervisely/nn/tracker/deep_sort/__init__.py +0 -6
  170. supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
  171. supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
  172. supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
  173. supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
  174. supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
  175. supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
  176. supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
  177. supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
  178. supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
  179. supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
  180. supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
  181. supervisely/nn/tracker/tracker.py +0 -285
  182. supervisely/nn/tracker/utils/kalman_filter.py +0 -492
  183. supervisely/nn/tracking/__init__.py +0 -1
  184. supervisely/nn/tracking/boxmot.py +0 -114
  185. supervisely/nn/tracking/tracking.py +0 -24
  186. /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
  187. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
  188. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
  189. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
  190. {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
@@ -693,4 +693,58 @@
693
693
  .tailwind .md\:last\:pr-6:last-child {
694
694
  padding-right: 1.5rem;
695
695
  }
696
+ }
697
+
698
+ .ellipsis {
699
+ overflow: hidden;
700
+ text-overflow: ellipsis;
701
+ }
702
+
703
+ .row-radio .el-radio__inner {
704
+ border-width: 1px;
705
+ }
706
+
707
+ .tailwind .el-checkbox__inner {
708
+ --tw-border-opacity: 1;
709
+ border-width: 1px;
710
+ border-color: rgb(226 232 240 / var(--tw-border-opacity));
711
+ }
712
+ .tailwind .el-checkbox__input.is-checked .el-checkbox__inner,
713
+ .tailwind .el-checkbox__input.is-indeterminate .el-checkbox__inner {
714
+ --tw-bg-opacity: 1;
715
+ background-color: rgb(14 165 233 / var(--tw-bg-opacity));
716
+ border-color: rgb(14 165 233 / var(--tw-bg-opacity));
717
+ }
718
+ /* refine the checkmark so it's thinner and not a full box */
719
+ .tailwind .el-checkbox__inner::after {
720
+ border-left: 0 !important;
721
+ border-top: 0 !important;
722
+ }
723
+ .tailwind .el-checkbox__input.is-checked .el-checkbox__inner::after {
724
+ border-right: 1.5px solid #fff;
725
+ border-bottom: 1.5px solid #fff;
726
+ border-left: 0 !important;
727
+ border-top: 0 !important;
728
+ }
729
+ .tailwind .el-checkbox__input.is-indeterminate .el-checkbox__inner::before {
730
+ background-color: #fff;
731
+ height: 2px;
732
+ }
733
+
734
+ .tailwind .el-checkbox__inner::after {
735
+ left: 4px;
736
+ top: 1px;
737
+ width: 4px;
738
+ height: 8px;
739
+ }
740
+
741
+ .sly-fast-table-disable-overlay {
742
+ position: absolute;
743
+ top: 0px;
744
+ left: 0px;
745
+ right: 0px;
746
+ bottom: 0px;
747
+ z-index: 2000;
748
+ background-color: rgba(227, 230, 236, 0.5);
749
+ border-radius: 0.5rem;
696
750
  }
@@ -1,6 +1,6 @@
1
1
  <link rel="stylesheet" href="./sly/css/app/widgets/fast_table/style.css" />
2
2
 
3
- <fast-table
3
+ <fast-table
4
4
  style="width: {{{widget._width}}}"
5
5
  :page.sync="state.{{{widget.widget_id}}}.page"
6
6
  :total="data.{{{widget.widget_id}}}.total"
@@ -11,13 +11,20 @@
11
11
  :project-meta="data.{{{widget.widget_id}}}.projectMeta"
12
12
  :sort.sync="state.{{{widget.widget_id}}}.sort"
13
13
  :search.sync="state.{{{widget.widget_id}}}.search"
14
- :data="data.{{{widget.widget_id}}}.data"
14
+ :data="Object.values(data.{{{widget.widget_id}}}.data || [])"
15
15
  :show-header="data.{{{widget.widget_id}}}.showHeader"
16
+ :selected-rows="state.{{{widget.widget_id}}}.selectedRows"
17
+ :selected-radio-idx="state.{{{widget.widget_id}}}.selectedRows && state.{{{widget.widget_id}}}.selectedRows.length > 0 ? state.{{{widget.widget_id}}}.selectedRows[0].idx : null"
18
+ :disabled="data.{{{widget.widget_id}}}.disabled"
19
+ @update:selected-rows="(rows) => {
20
+ state.{{{widget.widget_id}}}.selectedRows = rows;
21
+ data.{{{widget.widget_id}}}.selectionChangedHandled && post('/{{{widget.widget_id}}}/selection_changed_cb');
22
+ }"
16
23
  {%
17
24
  if
18
25
  widget._row_click_handled
19
26
  %}
20
- @row-click="state.{{{widget.widget_id}}}.selectedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
27
+ @row-click="state.{{{widget.widget_id}}}.clickedRow = $event; post('/{{{widget.widget_id}}}/row_clicked_cb')"
21
28
  {%
22
29
  endif
23
30
  %}
@@ -29,5 +36,38 @@
29
36
  {%
30
37
  endif
31
38
  %}
32
- @filters-changed="post('/{{{widget.widget_id}}}/update_data_cb')"
33
- />
39
+ @filters-changed="state.reactToChanges = false; post('/{{{widget.widget_id}}}/update_data_cb');"
40
+ >
41
+ {% if widget._header_left_content %}
42
+ <span slot="header-left-side-start" class="header-left-side-start-cls">
43
+ <div style="padding-right: 10px">
44
+ {{{ widget._header_left_content }}}
45
+ </div>
46
+ </span>
47
+ {% endif %}
48
+ {% if widget._header_right_content %}
49
+ <span slot="header-left-side-end" class="header-right-side-end-cls">
50
+ <div style="padding-left: 10px">
51
+ {{{ widget._header_right_content }}}
52
+ </div>
53
+ </span>
54
+ {% endif %}
55
+ <span slot="cell-content" slot-scope="{ row, column, cellValue, idx }">
56
+ {% for column_data in widget._columns_data %}
57
+ {% if column_data.is_widget %}
58
+ <div
59
+ v-if="column === '{{{ column_data.name }}}'"
60
+ :class="data.{{{widget.widget_id}}}.columnsOptions?.[{{{ loop.index0 }}}]?.classes || ''" JINJA TEMPLATE
61
+ :style="data.{{{widget.widget_id}}}.columnsOptions?.[{{{ loop.index0 }}}]?.style || ''"
62
+ @click.native.stop
63
+ >
64
+ {{{ column_data.widget_html }}}
65
+ </div>
66
+ {% else %}
67
+ <div v-if="column === '{{{ column_data.name }}}'">
68
+ <span v-html="highlight(col)"></span>
69
+ </div>
70
+ {% endif %}
71
+ {% endfor %}
72
+ </span>
73
+ </fast-table>
File without changes
@@ -0,0 +1,523 @@
1
+ from pathlib import Path
2
+ from typing import Any, Callable, List, 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_backup_rgb, read
14
+
15
+
16
+ def mask_to_heatmap(
17
+ mask: np.ndarray, colormap=cv2.COLORMAP_JET, transparent_low=False, vmin=None, vmax=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
+ mask_norm = cv2.GaussianBlur(mask_norm, (5, 5), 0)
34
+ heatmap_bgr = cv2.applyColorMap(mask_norm, colormap)
35
+ heatmap_bgra = cv2.cvtColor(heatmap_bgr, cv2.COLOR_BGR2BGRA)
36
+
37
+ if transparent_low:
38
+ alpha = np.where(mask_norm == 0, 0, 255).astype(np.uint8)
39
+ heatmap_bgra[..., 3] = alpha
40
+ heatmap_rgba = heatmap_bgra[..., [2, 1, 0, 3]]
41
+
42
+ return heatmap_rgba
43
+
44
+
45
+ def colormap_to_hex_list(colormap=cv2.COLORMAP_JET, n=5):
46
+ values = np.linspace(0, 255, n, dtype=np.uint8)
47
+ colors_bgr = cv2.applyColorMap(values[:, None], colormap)
48
+ colors_rgb = colors_bgr[:, 0, ::-1]
49
+ return [f"#{r:02X}{g:02X}{b:02X}" for r, g, b in colors_rgb]
50
+
51
+
52
+ def to_json_safe(val):
53
+ if val is None:
54
+ return None
55
+ if isinstance(val, (np.integer, int)):
56
+ return int(val)
57
+ if isinstance(val, (np.floating, float)):
58
+ return float(val)
59
+ return str(val)
60
+
61
+
62
+ class Heatmap(Widget):
63
+ """
64
+ Supervisely widget that displays an interactive heatmap overlay on top of a background image.
65
+
66
+ :param background_image: Background image to display under the heatmap. Can be a path to an image file or a NumPy array
67
+ :type background_image: Union[str, np.ndarray], optional
68
+ :param heatmap_mask: NumPy array representing the heatmap mask values
69
+ :type heatmap_mask: np.ndarray, optional
70
+ :param vmin: Minimum value for normalizing the heatmap. If None, it is inferred from the mask
71
+ :type vmin: Any, optional
72
+ :param vmax: Maximum value for normalizing the heatmap. If None, it is inferred from the mask
73
+ :type vmax: Any, optional
74
+ :param transparent_low: Whether to make low values in the heatmap transparent
75
+ :type transparent_low: bool, optional
76
+ :param colormap: OpenCV colormap used to colorize the heatmap (e.g., cv2.COLORMAP_JET)
77
+ :type colormap: int, optional
78
+ :param width: Width of the output heatmap in pixels
79
+ :type width: int, optional
80
+ :param height: Height of the output heatmap in pixels
81
+ :type height: int, optional
82
+ :param widget_id: Unique identifier for the widget instance
83
+ :type widget_id: str, optional
84
+
85
+ This widget provides an interactive visualization for numerical data as colored overlays.
86
+ Users can click on the heatmap to get exact values at specific coordinates.
87
+ The widget supports various colormaps, transparency controls, and value normalization.
88
+
89
+ :Usage example:
90
+
91
+ .. code-block:: python
92
+
93
+ import numpy as np
94
+ from supervisely.app.widgets import Heatmap
95
+
96
+ # Create temperature heatmap
97
+ temp_data = np.random.uniform(-20, 40, size=(100, 100))
98
+ heatmap = Heatmap(
99
+ background_image="/path/to/background.jpg",
100
+ heatmap_mask=temp_data,
101
+ vmin=-20,
102
+ vmax=40,
103
+ colormap=cv2.COLORMAP_JET
104
+ )
105
+
106
+ @heatmap.click
107
+ def handle_click(y: int, x: int, value: float):
108
+ print(f"Temperature at ({x}, {y}): {value:.1f}°C")
109
+ """
110
+
111
+ class Routes:
112
+ CLICK = "heatmap_clicked_cb"
113
+
114
+ def __init__(
115
+ self,
116
+ background_image: Union[str, np.ndarray] = None,
117
+ heatmap_mask: np.ndarray = None,
118
+ vmin: Any = None,
119
+ vmax: Any = None,
120
+ transparent_low: bool = False,
121
+ colormap: int = cv2.COLORMAP_JET,
122
+ width: int = None,
123
+ height: int = None,
124
+ widget_id: str = None,
125
+ ):
126
+ self._background_url = None
127
+ self._heatmap_url = None
128
+ self._mask_data = None # Store numpy array for efficient value lookup
129
+ self._click_callback = None # Optional user callback
130
+ self._vmin = vmin
131
+ self._vmax = vmax
132
+ self._transparent_low = transparent_low
133
+ self._colormap = colormap
134
+ self._width = width
135
+ self._height = height
136
+ self._opacity = 70
137
+ self._min_value = 0
138
+ self._max_value = 0
139
+ super().__init__(widget_id, file_path=__file__)
140
+
141
+ if background_image is not None:
142
+ self.set_background(background_image)
143
+
144
+ if heatmap_mask is not None:
145
+ self.set_heatmap(heatmap_mask)
146
+
147
+ script_path = "./sly/css/app/widgets/heatmap/script.js"
148
+ JinjaWidgets().context["__widget_scripts__"][self.__class__.__name__] = script_path
149
+
150
+ # Register default click handler to update value from server-side mask
151
+ self._register_click_handler()
152
+
153
+ def get_json_data(self):
154
+ # Get mask dimensions if available
155
+ mask_height, mask_width = 0, 0
156
+ if self._mask_data is not None:
157
+ mask_height, mask_width = self._mask_data.shape[:2]
158
+
159
+ return {
160
+ "backgroundUrl": self._background_url,
161
+ "heatmapUrl": self._heatmap_url,
162
+ "width": self._width,
163
+ "height": self._height,
164
+ "maskWidth": mask_width,
165
+ "maskHeight": mask_height,
166
+ "minValue": self._min_value,
167
+ "maxValue": self._max_value,
168
+ "legendColors": colormap_to_hex_list(self._colormap),
169
+ }
170
+
171
+ def get_json_state(self):
172
+ return {"opacity": self._opacity, "clickedValue": None, "maskX": None, "maskY": None}
173
+
174
+ def set_background(self, background_image: Union[str, np.ndarray]):
175
+ """
176
+ Sets the background image that will be displayed under the heatmap overlay.
177
+
178
+ :param background_image: Background image source. Can be a file path, URL, or NumPy array
179
+ :type background_image: Union[str, np.ndarray]
180
+ :raises ValueError: If the background image type is unsupported or file path doesn't exist
181
+ :raises Exception: If there's an error during image processing or file operations
182
+
183
+ This method handles three types of background images:
184
+ 1. **NumPy array**: Converts to PNG and encodes as data URL
185
+ 2. **HTTP/HTTPS URL**: Uses the URL directly for remote images
186
+ 3. **Local file path**: Reads file and encodes as data URL
187
+
188
+ All images are converted to data URLs for efficient in-memory serving.
189
+
190
+ :Usage example:
191
+
192
+ .. code-block:: python
193
+
194
+ from supervisely.app.widgets.heatmap import Heatmap
195
+ import numpy as np
196
+ heatmap = Heatmap()
197
+
198
+ # Using a local file path
199
+ heatmap.set_background("/path/to/image.jpg")
200
+
201
+ # Using a NumPy array (RGB image)
202
+ bg_array = np.random.randint(0, 255, size=(480, 640, 3), dtype=np.uint8)
203
+ heatmap.set_background(bg_array)
204
+
205
+ # Using a remote URL
206
+ heatmap.set_background("https://example.com/background.png")
207
+ """
208
+ try:
209
+ if isinstance(background_image, np.ndarray):
210
+ self._background_url = np_image_to_data_url_backup_rgb(background_image)
211
+ elif isinstance(background_image, str):
212
+ parsed = urlparse(background_image)
213
+ bg_image_path = Path(background_image)
214
+ if parsed.scheme in ("http", "https") and parsed.netloc:
215
+ self._background_url = background_image
216
+ elif parsed.scheme == "data":
217
+ self._background_url = background_image
218
+ elif bg_image_path.exists() and bg_image_path.is_file():
219
+ np_image = read(bg_image_path, remove_alpha_channel=False)
220
+ self._background_url = np_image_to_data_url_backup_rgb(np_image)
221
+ else:
222
+ raise ValueError(f"Unable to find image at {background_image}")
223
+ else:
224
+ raise ValueError(f"Unsupported background_image type: {type(background_image)}")
225
+ except Exception as e:
226
+ logger.error(f"Error setting background: {e}", exc_info=True)
227
+ self._background_url = None
228
+ raise
229
+ finally:
230
+ DataJson()[self.widget_id]["backgroundUrl"] = self._background_url
231
+ DataJson().send_changes()
232
+
233
+ def set_heatmap(self, mask: np.ndarray):
234
+ """
235
+ Sets the heatmap mask data and generates a colorized PNG overlay.
236
+
237
+ :param mask: NumPy array representing the heatmap values to be displayed
238
+ :type mask: np.ndarray
239
+
240
+ :raises Exception: If there's an error during heatmap generation
241
+
242
+ The heatmap is converted to a data URL for efficient in-memory serving.
243
+
244
+ :Usage example:
245
+
246
+ .. code-block:: python
247
+
248
+ from supervisely.app.widgets.heatmap import Heatmap
249
+ import numpy as np
250
+
251
+ heatmap = Heatmap()
252
+
253
+ # Create probability heatmap (0.0 to 1.0)
254
+ probability_mask = np.random.uniform(0.0, 1.0, size=(100, 100))
255
+ heatmap.set_heatmap(probability_mask)
256
+
257
+ # Create temperature heatmap (-50 to 150)
258
+ temp_mask = np.random.uniform(-50, 150, size=(200, 300))
259
+ heatmap.set_heatmap(temp_mask)
260
+ """
261
+ try:
262
+ heatmap = mask_to_heatmap(
263
+ mask,
264
+ colormap=self._colormap,
265
+ vmin=self._vmin,
266
+ vmax=self._vmax,
267
+ transparent_low=self._transparent_low,
268
+ )
269
+ self._heatmap_url = np_image_to_data_url_backup_rgb(heatmap)
270
+ self._min_value = to_json_safe(mask.min())
271
+ self._max_value = to_json_safe(mask.max())
272
+
273
+ # Store mask as numpy array for efficient server-side value lookup
274
+ self._mask_data = mask.copy()
275
+
276
+ except Exception as e:
277
+ logger.error(f"Error setting heatmap: {e}", exc_info=True)
278
+ self._heatmap_url = None
279
+ self._min_value = None
280
+ self._max_value = None
281
+ self._mask_data = None
282
+ raise
283
+ finally:
284
+ DataJson()[self.widget_id]["heatmapUrl"] = self._heatmap_url
285
+ DataJson()[self.widget_id]["minValue"] = self._min_value
286
+ DataJson()[self.widget_id]["maxValue"] = self._max_value
287
+
288
+ # Update mask dimensions
289
+ if self._mask_data is not None:
290
+ h, w = self._mask_data.shape[:2]
291
+ DataJson()[self.widget_id]["maskWidth"] = w
292
+ DataJson()[self.widget_id]["maskHeight"] = h
293
+ else:
294
+ DataJson()[self.widget_id]["maskWidth"] = 0
295
+ DataJson()[self.widget_id]["maskHeight"] = 0
296
+
297
+ # Don't send maskData - will be fetched on-demand when user clicks
298
+ DataJson().send_changes()
299
+
300
+ def set_heatmap_from_annotations(self, anns: List[Annotation], object_name: str = None):
301
+ """
302
+ Creates and sets a heatmap from Supervisely annotations showing object density/overlaps.
303
+
304
+ :param anns: List of Supervisely annotations to convert to heatmap
305
+ :type anns: List[Annotation]
306
+ :param object_name: Name of the object class to filter annotations by. If None, all objects are included
307
+ :type object_name: str, optional
308
+ :raises ValueError: If the annotations list is empty
309
+
310
+ This method creates a density heatmap mask by:
311
+ 1. Using widget dimensions (width/height) if specified, calculating missing dimension from aspect ratio
312
+ 2. Creating a zero-filled mask of the target size
313
+ 3. Drawing each matching label onto the mask, accumulating values
314
+ 4. Areas with overlapping objects will have higher values (brighter in heatmap)
315
+ 5. Setting the resulting density mask as the heatmap
316
+
317
+ :Usage example:
318
+
319
+ .. code-block:: python
320
+
321
+ from supervisely.annotation.annotation import Annotation
322
+
323
+ ann1 = Annotation.load_json_file("/path/to/ann1.json")
324
+ ann2 = Annotation.load_json_file("/path/to/ann2.json")
325
+ ann3 = Annotation.load_json_file("/path/to/ann3.json")
326
+ annotations = [ann1, ann2, ann3]
327
+ heatmap.set_heatmap_from_annotations(annotations, object_name="person")
328
+
329
+ """
330
+ if len(anns) == 0:
331
+ raise ValueError("Annotations list should have at least one element")
332
+
333
+ # Use widget dimensions if specified, otherwise calculate average from annotations
334
+ if self._width is not None and self._height is not None:
335
+ # Both dimensions specified - use them directly
336
+ target_size = (self._height, self._width)
337
+ elif self._width is not None or self._height is not None:
338
+ # Only one dimension specified - calculate the other from annotations aspect ratio
339
+ sizes = [ann.img_size for ann in anns]
340
+ avg_height = sum(size[0] for size in sizes) / len(sizes)
341
+ avg_width = sum(size[1] for size in sizes) / len(sizes)
342
+ aspect_ratio = avg_width / avg_height
343
+
344
+ if self._width is not None:
345
+ # Width specified, calculate height
346
+ target_height = int(round(self._width / aspect_ratio / 2) * 2)
347
+ target_size = (target_height, self._width)
348
+ else:
349
+ # Height specified, calculate width
350
+ target_width = int(round(self._height * aspect_ratio / 2) * 2)
351
+ target_size = (self._height, target_width)
352
+ else:
353
+ # No dimensions specified - calculate average size from annotations and round to even numbers
354
+ sizes = [ann.img_size for ann in anns]
355
+ target_size = (
356
+ int(round(sum(size[0] for size in sizes) / len(sizes) / 2) * 2),
357
+ int(round(sum(size[1] for size in sizes) / len(sizes) / 2) * 2),
358
+ )
359
+
360
+ # Count matching labels to determine max possible value
361
+ total_labels = 0
362
+ for ann in anns:
363
+ for label in ann.labels:
364
+ if object_name is None or label.obj_class.name == object_name:
365
+ total_labels += 1
366
+
367
+ if total_labels == 0:
368
+ raise ValueError(f"No labels found for object_name='{object_name}'")
369
+
370
+ # Create density mask that accumulates overlapping objects
371
+ mask = np.zeros(target_size, dtype=np.float32)
372
+
373
+ for ann in anns:
374
+ for label in ann.labels:
375
+ if object_name is None or label.obj_class.name == object_name:
376
+ # Create a resized label for the target mask size
377
+ resized_label = label.resize(ann.img_size, target_size)
378
+
379
+ # Create temporary mask for this label
380
+ temp_mask = np.zeros(target_size, dtype=np.float32)
381
+ resized_label.draw(temp_mask, color=1.0)
382
+
383
+ # Add to accumulating density mask (overlaps will sum up)
384
+ mask += temp_mask
385
+
386
+ logger.info(
387
+ f"Created density heatmap: {total_labels} labels, "
388
+ f"target size: {target_size}, "
389
+ f"max density: {mask.max():.1f}, "
390
+ f"avg density: {mask.mean():.3f}"
391
+ )
392
+
393
+ self.set_heatmap(mask)
394
+
395
+ @property
396
+ def opacity(self):
397
+ return StateJson()[self.widget_id]["opacity"]
398
+
399
+ @opacity.setter
400
+ def opacity(self, value: int):
401
+ value = max(0, value)
402
+ value = min(100, value)
403
+ StateJson()[self.widget_id]["opacity"] = value
404
+ StateJson().send_changes()
405
+
406
+ @property
407
+ def colormap(self):
408
+ return self._colormap
409
+
410
+ @colormap.setter
411
+ def colormap(self, value: int):
412
+ self._colormap = value
413
+ DataJson()[self.widget_id]["legendColors"] = colormap_to_hex_list(self._colormap)
414
+
415
+ @property
416
+ def vmin(self):
417
+ return self._vmin
418
+
419
+ @vmin.setter
420
+ def vmin(self, value):
421
+ self._vmin = value
422
+
423
+ @property
424
+ def vmax(self):
425
+ return self._vmax
426
+
427
+ @vmax.setter
428
+ def vmax(self, value):
429
+ self._vmax = value
430
+
431
+ @property
432
+ def width(self):
433
+ return self._width
434
+
435
+ @width.setter
436
+ def width(self, value):
437
+ self._width = value
438
+ DataJson()[self.widget_id]["width"] = self._width
439
+
440
+ @property
441
+ def height(self):
442
+ return self._height
443
+
444
+ @height.setter
445
+ def height(self, value):
446
+ self._height = value
447
+ DataJson()[self.widget_id]["height"] = self._height
448
+
449
+ @property
450
+ def click_x(self):
451
+ return StateJson()[self.widget_id]["maskX"]
452
+
453
+ @property
454
+ def click_y(self):
455
+ return StateJson()[self.widget_id]["maskY"]
456
+
457
+ @property
458
+ def click_value(self):
459
+ return StateJson()[self.widget_id]["clickedValue"]
460
+
461
+ def _register_click_handler(self):
462
+ """Register internal click handler to update value from server-side mask."""
463
+ route_path = self.get_route_path(self.Routes.CLICK)
464
+ server = self._sly_app.get_server()
465
+
466
+ @server.post(route_path)
467
+ def _click():
468
+ x = StateJson()[self.widget_id]["maskX"]
469
+ y = StateJson()[self.widget_id]["maskY"]
470
+
471
+ logger.debug(
472
+ f"Heatmap click: x={x}, y={y}, _mask_data shape={self._mask_data.shape if self._mask_data is not None else None}"
473
+ )
474
+
475
+ # Get value from server-side mask data
476
+ clicked_value = None
477
+ if self._mask_data is not None and x is not None and y is not None:
478
+ h, w = self._mask_data.shape[:2]
479
+ if 0 <= y < h and 0 <= x < w:
480
+ clicked_value = float(self._mask_data[y, x])
481
+ # Update state with the value
482
+ StateJson()[self.widget_id]["clickedValue"] = clicked_value
483
+ StateJson().send_changes()
484
+ logger.debug(f"Heatmap click value: {clicked_value}")
485
+ else:
486
+ logger.warning(f"Coordinates out of bounds: x={x}, y={y}, shape=({h}, {w})")
487
+ else:
488
+ if self._mask_data is None:
489
+ logger.warning("Mask data is None")
490
+ if x is None:
491
+ logger.warning("x coordinate is None")
492
+ if y is None:
493
+ logger.warning("y coordinate is None")
494
+
495
+ # Call user callback if registered
496
+ if self._click_callback is not None:
497
+ self._click_callback(y, x, clicked_value)
498
+
499
+ def click(self, func: Callable[[int, int, float], None]) -> Callable[[], None]:
500
+ """
501
+ Registers a callback for heatmap click events.
502
+
503
+ :param func: Callback function that receives click coordinates and value
504
+ :type func: Callable[[int, int, float], None]
505
+ :returns: The registered callback function
506
+ :rtype: Callable[[], None]
507
+
508
+ The callback receives coordinates in NumPy order (y, x, value), where:
509
+ - y: row index (height axis)
510
+ - x: column index (width axis)
511
+ - value: clicked pixel value (fetched from server-side mask)
512
+
513
+ :Usage example:
514
+
515
+ .. code-block:: python
516
+
517
+ @heatmap.click
518
+ def handle_click(y: int, x: int, value: float):
519
+ print(f"Clicked at row {y}, col {x}, value: {value}")
520
+
521
+ """
522
+ self._click_callback = func
523
+ return func