supervisely 6.73.461__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.
- supervisely/api/dataset_api.py +74 -12
- supervisely/app/widgets/__init__.py +1 -0
- supervisely/app/widgets/fast_table/fast_table.py +164 -74
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +523 -0
- supervisely/app/widgets/heatmap/script.js +378 -0
- supervisely/app/widgets/heatmap/style.css +227 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +10 -2
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/nn/inference/inference.py +266 -9
- supervisely/nn/inference/inference_request.py +3 -9
- supervisely/nn/inference/predict_app/gui/input_selector.py +53 -27
- supervisely/nn/inference/session.py +43 -35
- supervisely/video/sampling.py +41 -21
- supervisely/video/video.py +25 -10
- {supervisely-6.73.461.dist-info → supervisely-6.73.470.dist-info}/METADATA +1 -1
- {supervisely-6.73.461.dist-info → supervisely-6.73.470.dist-info}/RECORD +25 -20
- {supervisely-6.73.461.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
- {supervisely-6.73.461.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
- {supervisely-6.73.461.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.461.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
|
@@ -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
|