supervisely 6.73.284__py3-none-any.whl → 6.73.285__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/_utils.py +9 -0
- supervisely/api/entity_annotation/figure_api.py +3 -0
- supervisely/api/module_api.py +35 -1
- supervisely/api/video/video_api.py +1 -1
- supervisely/api/video_annotation_tool_api.py +58 -7
- supervisely/nn/inference/cache.py +19 -1
- supervisely/nn/inference/inference.py +22 -0
- supervisely/nn/inference/tracking/base_tracking.py +362 -0
- supervisely/nn/inference/tracking/bbox_tracking.py +179 -129
- supervisely/nn/inference/tracking/mask_tracking.py +420 -329
- supervisely/nn/inference/tracking/point_tracking.py +325 -288
- supervisely/nn/inference/tracking/tracker_interface.py +346 -13
- {supervisely-6.73.284.dist-info → supervisely-6.73.285.dist-info}/METADATA +1 -1
- {supervisely-6.73.284.dist-info → supervisely-6.73.285.dist-info}/RECORD +18 -17
- {supervisely-6.73.284.dist-info → supervisely-6.73.285.dist-info}/LICENSE +0 -0
- {supervisely-6.73.284.dist-info → supervisely-6.73.285.dist-info}/WHEEL +0 -0
- {supervisely-6.73.284.dist-info → supervisely-6.73.285.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.284.dist-info → supervisely-6.73.285.dist-info}/top_level.txt +0 -0
|
@@ -1,60 +1,401 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import json
|
|
3
1
|
import time
|
|
2
|
+
import uuid
|
|
4
3
|
from queue import Queue
|
|
5
4
|
from threading import Event, Thread
|
|
6
|
-
from typing import
|
|
5
|
+
from typing import BinaryIO, Dict, List, Tuple
|
|
7
6
|
|
|
8
7
|
import numpy as np
|
|
9
|
-
from
|
|
8
|
+
from pydantic import ValidationError
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
import supervisely.nn.inference.tracking.functional as F
|
|
10
|
+
from supervisely._utils import find_value_by_keys
|
|
13
11
|
from supervisely.annotation.label import Geometry, Label
|
|
14
|
-
from supervisely.
|
|
15
|
-
from supervisely.
|
|
16
|
-
from supervisely.
|
|
12
|
+
from supervisely.annotation.obj_class import ObjClass
|
|
13
|
+
from supervisely.api.api import Api
|
|
14
|
+
from supervisely.api.module_api import ApiField
|
|
15
|
+
from supervisely.api.video.video_figure_api import FigureInfo
|
|
16
|
+
from supervisely.geometry.bitmap import Bitmap
|
|
17
|
+
from supervisely.geometry.helpers import deserialize_geometry
|
|
18
|
+
from supervisely.geometry.polygon import Polygon
|
|
19
|
+
from supervisely.imaging import image as sly_image
|
|
20
|
+
from supervisely.nn.inference.tracking.base_tracking import BaseTracking
|
|
21
|
+
from supervisely.nn.inference.tracking.tracker_interface import (
|
|
22
|
+
TrackerInterface,
|
|
23
|
+
TrackerInterfaceV2,
|
|
24
|
+
)
|
|
25
|
+
from supervisely.sly_logger import logger
|
|
26
|
+
from supervisely.task.progress import Progress
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MaskTracking(BaseTracking):
|
|
30
|
+
def _deserialize_geometry(self, data: dict):
|
|
31
|
+
geometry_type_str = data["type"]
|
|
32
|
+
geometry_json = data["data"]
|
|
33
|
+
return deserialize_geometry(geometry_type_str, geometry_json)
|
|
17
34
|
|
|
35
|
+
def _inference(self, frames: List[np.ndarray], geometries: List[Geometry]):
|
|
36
|
+
results = [[] for _ in range(len(frames) - 1)]
|
|
37
|
+
for geometry in geometries:
|
|
38
|
+
if not isinstance(geometry, Bitmap) and not isinstance(geometry, Polygon):
|
|
39
|
+
raise TypeError(f"This app does not support {geometry.geometry_name()} tracking")
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
# combine several binary masks into one multilabel mask
|
|
42
|
+
i = 0
|
|
43
|
+
label2id = {}
|
|
44
|
+
|
|
45
|
+
original_geometry = geometry.clone()
|
|
46
|
+
|
|
47
|
+
# convert polygon to bitmap
|
|
48
|
+
if isinstance(geometry, Polygon):
|
|
49
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
50
|
+
polygon_label = Label(geometry, polygon_obj_class)
|
|
51
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
52
|
+
bitmap_label = polygon_label.convert(bitmap_obj_class)[0]
|
|
53
|
+
geometry = bitmap_label.geometry
|
|
54
|
+
if i == 0:
|
|
55
|
+
multilabel_mask = geometry.data.astype(int)
|
|
56
|
+
multilabel_mask = np.zeros(frames[0].shape, dtype=np.uint8)
|
|
57
|
+
geometry.draw(bitmap=multilabel_mask, color=[1, 1, 1])
|
|
58
|
+
i += 1
|
|
59
|
+
else:
|
|
60
|
+
i += 1
|
|
61
|
+
geometry.draw(bitmap=multilabel_mask, color=[i, i, i])
|
|
62
|
+
label2id[i] = {
|
|
63
|
+
"original_geometry": original_geometry.geometry_name(),
|
|
64
|
+
}
|
|
65
|
+
# run tracker
|
|
66
|
+
tracked_multilabel_masks = self.predict(
|
|
67
|
+
frames=frames, input_mask=multilabel_mask[:, :, 0]
|
|
68
|
+
)
|
|
69
|
+
tracked_multilabel_masks = np.array(tracked_multilabel_masks)
|
|
70
|
+
# decompose multilabel masks into binary masks
|
|
71
|
+
for i in np.unique(tracked_multilabel_masks):
|
|
72
|
+
if i != 0:
|
|
73
|
+
binary_masks = tracked_multilabel_masks == i
|
|
74
|
+
geometry_type = label2id[i]["original_geometry"]
|
|
75
|
+
for j, mask in enumerate(binary_masks[1:]):
|
|
76
|
+
# check if mask is not empty
|
|
77
|
+
if np.any(mask):
|
|
78
|
+
if geometry_type == "polygon":
|
|
79
|
+
bitmap_geometry = Bitmap(mask)
|
|
80
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
81
|
+
bitmap_label = Label(bitmap_geometry, bitmap_obj_class)
|
|
82
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
83
|
+
polygon_labels = bitmap_label.convert(polygon_obj_class)
|
|
84
|
+
geometries = [label.geometry for label in polygon_labels]
|
|
85
|
+
else:
|
|
86
|
+
geometries = [Bitmap(mask)]
|
|
87
|
+
results[j].extend(
|
|
88
|
+
[
|
|
89
|
+
{"type": geom.geometry_name(), "data": geom.to_json()}
|
|
90
|
+
for geom in geometries
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
return results
|
|
94
|
+
|
|
95
|
+
def _track(self, api: Api, context: Dict):
|
|
96
|
+
self.video_interface = TrackerInterface(
|
|
97
|
+
context=context,
|
|
98
|
+
api=api,
|
|
99
|
+
load_all_frames=True,
|
|
100
|
+
notify_in_predict=True,
|
|
101
|
+
per_point_polygon_tracking=False,
|
|
102
|
+
frame_loader=self.cache.download_frame,
|
|
103
|
+
frames_loader=self.cache.download_frames,
|
|
31
104
|
)
|
|
105
|
+
range_of_frames = [
|
|
106
|
+
self.video_interface.frames_indexes[0],
|
|
107
|
+
self.video_interface.frames_indexes[-1],
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
if self.cache.is_persistent:
|
|
111
|
+
# if cache is persistent, run cache task for whole video
|
|
112
|
+
self.cache.run_cache_task_manually(
|
|
113
|
+
api,
|
|
114
|
+
None,
|
|
115
|
+
video_id=self.video_interface.video_id,
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# if cache is not persistent, run cache task for range of frames
|
|
119
|
+
self.cache.run_cache_task_manually(
|
|
120
|
+
api,
|
|
121
|
+
[range_of_frames],
|
|
122
|
+
video_id=self.video_interface.video_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
api.logger.info("Starting tracking process")
|
|
126
|
+
# load frames
|
|
127
|
+
frames = self.video_interface.frames
|
|
128
|
+
# combine several binary masks into one multilabel mask
|
|
129
|
+
i = 0
|
|
130
|
+
label2id = {}
|
|
131
|
+
|
|
132
|
+
def _upload_loop(q: Queue, stop_event: Event, video_interface: TrackerInterface):
|
|
133
|
+
try:
|
|
134
|
+
while True:
|
|
135
|
+
items = []
|
|
136
|
+
while not q.empty():
|
|
137
|
+
items.append(q.get_nowait())
|
|
138
|
+
if len(items) > 0:
|
|
139
|
+
video_interface.add_object_geometries_on_frames(*list(zip(*items)))
|
|
140
|
+
continue
|
|
141
|
+
if stop_event.is_set():
|
|
142
|
+
video_interface._notify(True, task="stop tracking")
|
|
143
|
+
return
|
|
144
|
+
time.sleep(1)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
api.logger.error("Error in upload loop: %s", str(e), exc_info=True)
|
|
147
|
+
video_interface._notify(True, task="stop tracking")
|
|
148
|
+
video_interface.global_stop_indicatior = True
|
|
149
|
+
raise
|
|
150
|
+
|
|
151
|
+
upload_queue = Queue()
|
|
152
|
+
stop_upload_event = Event()
|
|
153
|
+
Thread(
|
|
154
|
+
target=_upload_loop,
|
|
155
|
+
args=[upload_queue, stop_upload_event, self.video_interface],
|
|
156
|
+
daemon=True,
|
|
157
|
+
).start()
|
|
32
158
|
|
|
33
159
|
try:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
160
|
+
for (fig_id, geometry), obj_id in zip(
|
|
161
|
+
self.video_interface.geometries.items(),
|
|
162
|
+
self.video_interface.object_ids,
|
|
163
|
+
):
|
|
164
|
+
original_geometry = geometry.clone()
|
|
165
|
+
if not isinstance(geometry, Bitmap) and not isinstance(geometry, Polygon):
|
|
166
|
+
stop_upload_event.set()
|
|
167
|
+
raise TypeError(
|
|
168
|
+
f"This app does not support {geometry.geometry_name()} tracking"
|
|
169
|
+
)
|
|
170
|
+
# convert polygon to bitmap
|
|
171
|
+
if isinstance(geometry, Polygon):
|
|
172
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
173
|
+
polygon_label = Label(geometry, polygon_obj_class)
|
|
174
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
175
|
+
bitmap_label = polygon_label.convert(bitmap_obj_class)[0]
|
|
176
|
+
geometry = bitmap_label.geometry
|
|
177
|
+
if i == 0:
|
|
178
|
+
multilabel_mask = geometry.data.astype(int)
|
|
179
|
+
multilabel_mask = np.zeros(frames[0].shape, dtype=np.uint8)
|
|
180
|
+
geometry.draw(bitmap=multilabel_mask, color=[1, 1, 1])
|
|
181
|
+
i += 1
|
|
182
|
+
else:
|
|
183
|
+
i += 1
|
|
184
|
+
geometry.draw(bitmap=multilabel_mask, color=[i, i, i])
|
|
185
|
+
label2id[i] = {
|
|
186
|
+
"fig_id": fig_id,
|
|
187
|
+
"obj_id": obj_id,
|
|
188
|
+
"original_geometry": original_geometry.geometry_name(),
|
|
189
|
+
}
|
|
190
|
+
# run tracker
|
|
191
|
+
tracked_multilabel_masks = self.predict(
|
|
192
|
+
frames=frames, input_mask=multilabel_mask[:, :, 0]
|
|
193
|
+
)
|
|
194
|
+
tracked_multilabel_masks = np.array(tracked_multilabel_masks)
|
|
195
|
+
# decompose multilabel masks into binary masks
|
|
196
|
+
for i in np.unique(tracked_multilabel_masks):
|
|
197
|
+
if i != 0:
|
|
198
|
+
binary_masks = tracked_multilabel_masks == i
|
|
199
|
+
fig_id = label2id[i]["fig_id"]
|
|
200
|
+
obj_id = label2id[i]["obj_id"]
|
|
201
|
+
geometry_type = label2id[i]["original_geometry"]
|
|
202
|
+
for j, mask in enumerate(binary_masks[1:]):
|
|
203
|
+
# check if mask is not empty
|
|
204
|
+
if not np.any(mask):
|
|
205
|
+
api.logger.info(
|
|
206
|
+
f"Skipping empty mask on frame {self.video_interface.frame_index + j + 1}"
|
|
207
|
+
)
|
|
208
|
+
# update progress bar anyway (otherwise it will not be finished)
|
|
209
|
+
self.video_interface._notify(task="add geometry on frame")
|
|
210
|
+
else:
|
|
211
|
+
if geometry_type == "polygon":
|
|
212
|
+
bitmap_geometry = Bitmap(mask)
|
|
213
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
214
|
+
bitmap_label = Label(bitmap_geometry, bitmap_obj_class)
|
|
215
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
216
|
+
polygon_labels = bitmap_label.convert(polygon_obj_class)
|
|
217
|
+
geometries = [label.geometry for label in polygon_labels]
|
|
218
|
+
else:
|
|
219
|
+
geometries = [Bitmap(mask)]
|
|
220
|
+
for l, geometry in enumerate(geometries):
|
|
221
|
+
if l == len(geometries) - 1:
|
|
222
|
+
notify = True
|
|
223
|
+
else:
|
|
224
|
+
notify = False
|
|
225
|
+
upload_queue.put(
|
|
226
|
+
(
|
|
227
|
+
geometry,
|
|
228
|
+
obj_id,
|
|
229
|
+
self.video_interface.frames_indexes[j + 1],
|
|
230
|
+
notify,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
if self.video_interface.global_stop_indicatior:
|
|
234
|
+
stop_upload_event.set()
|
|
235
|
+
return
|
|
236
|
+
api.logger.info(f"Figure with id {fig_id} was successfully tracked")
|
|
237
|
+
except Exception:
|
|
238
|
+
stop_upload_event.set()
|
|
239
|
+
raise
|
|
240
|
+
stop_upload_event.set()
|
|
241
|
+
|
|
242
|
+
def _track_async(self, api: Api, context: dict, inference_request_uuid: str = None):
|
|
243
|
+
inference_request = self._inference_requests[inference_request_uuid]
|
|
244
|
+
tracker_interface = TrackerInterfaceV2(api, context, self.cache)
|
|
245
|
+
progress: Progress = inference_request["progress"]
|
|
246
|
+
frames_count = tracker_interface.frames_count
|
|
247
|
+
figures = tracker_interface.figures
|
|
248
|
+
progress_total = frames_count * len(figures)
|
|
249
|
+
progress.total = progress_total
|
|
250
|
+
|
|
251
|
+
def _upload_f(items: List[Tuple[FigureInfo, bool]]):
|
|
252
|
+
with inference_request["lock"]:
|
|
253
|
+
inference_request["pending_results"].extend([item[0] for item in items])
|
|
254
|
+
|
|
255
|
+
def _notify_f(items: List[Tuple[FigureInfo, bool]]):
|
|
256
|
+
items_by_object_id: Dict[int, List[Tuple[FigureInfo, bool]]] = {}
|
|
257
|
+
for item in items:
|
|
258
|
+
items_by_object_id.setdefault(item[0].object_id, []).append(item)
|
|
259
|
+
|
|
260
|
+
for object_id, object_items in items_by_object_id.items():
|
|
261
|
+
frame_range = [
|
|
262
|
+
min(item[0].frame_index for item in object_items),
|
|
263
|
+
max(item[0].frame_index for item in object_items),
|
|
264
|
+
]
|
|
265
|
+
progress.iters_done_report(sum(1 for item in object_items if item[1]))
|
|
266
|
+
tracker_interface.notify_progress(progress.current, progress.total, frame_range)
|
|
46
267
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
268
|
+
# run tracker
|
|
269
|
+
frame_index = tracker_interface.frame_index
|
|
270
|
+
direction_n = tracker_interface.direction_n
|
|
271
|
+
api.logger.info("Start tracking.")
|
|
272
|
+
try:
|
|
273
|
+
with tracker_interface(_upload_f, _notify_f):
|
|
274
|
+
# combine several binary masks into one multilabel mask
|
|
275
|
+
i = 0
|
|
276
|
+
label2id = {}
|
|
277
|
+
# load frames
|
|
278
|
+
frames = tracker_interface.load_all_frames()
|
|
279
|
+
frames = [frame.image for frame in frames]
|
|
280
|
+
for figure in figures:
|
|
281
|
+
figure = api.video.figure._convert_json_info(figure)
|
|
282
|
+
fig_id = figure.id
|
|
283
|
+
obj_id = figure.object_id
|
|
284
|
+
geometry = deserialize_geometry(figure.geometry_type, figure.geometry)
|
|
285
|
+
original_geometry = geometry.clone()
|
|
286
|
+
if not isinstance(geometry, (Bitmap, Polygon)):
|
|
287
|
+
raise TypeError(
|
|
288
|
+
f"This app does not support {geometry.geometry_name()} tracking"
|
|
289
|
+
)
|
|
290
|
+
# convert polygon to bitmap
|
|
291
|
+
if isinstance(geometry, Polygon):
|
|
292
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
293
|
+
polygon_label = Label(geometry, polygon_obj_class)
|
|
294
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
295
|
+
bitmap_label = polygon_label.convert(bitmap_obj_class)[0]
|
|
296
|
+
geometry = bitmap_label.geometry
|
|
297
|
+
if i == 0:
|
|
298
|
+
multilabel_mask = geometry.data.astype(int)
|
|
299
|
+
multilabel_mask = np.zeros(frames[0].shape, dtype=np.uint8)
|
|
300
|
+
geometry.draw(bitmap=multilabel_mask, color=[1, 1, 1])
|
|
301
|
+
i += 1
|
|
302
|
+
else:
|
|
303
|
+
i += 1
|
|
304
|
+
geometry.draw(bitmap=multilabel_mask, color=[i, i, i])
|
|
305
|
+
label2id[i] = {
|
|
306
|
+
"fig_id": fig_id,
|
|
307
|
+
"obj_id": obj_id,
|
|
308
|
+
"original_geometry": original_geometry.geometry_name(),
|
|
309
|
+
}
|
|
310
|
+
if inference_request["cancel_inference"]:
|
|
311
|
+
return
|
|
312
|
+
if tracker_interface.is_stopped():
|
|
313
|
+
reason = tracker_interface.stop_reason()
|
|
314
|
+
if isinstance(reason, Exception):
|
|
315
|
+
raise reason
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# predict
|
|
319
|
+
tracked_multilabel_masks = self.predict(
|
|
320
|
+
frames=frames, input_mask=multilabel_mask[:, :, 0]
|
|
321
|
+
)
|
|
322
|
+
tracked_multilabel_masks = np.array(tracked_multilabel_masks)
|
|
51
323
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
324
|
+
# decompose multilabel masks into binary masks
|
|
325
|
+
for i in np.unique(tracked_multilabel_masks):
|
|
326
|
+
if inference_request["cancel_inference"]:
|
|
327
|
+
return
|
|
328
|
+
if tracker_interface.is_stopped():
|
|
329
|
+
reason = tracker_interface.stop_reason()
|
|
330
|
+
if isinstance(reason, Exception):
|
|
331
|
+
raise reason
|
|
332
|
+
return
|
|
333
|
+
if i != 0:
|
|
334
|
+
binary_masks = tracked_multilabel_masks == i
|
|
335
|
+
fig_id = label2id[i]["fig_id"]
|
|
336
|
+
obj_id = label2id[i]["obj_id"]
|
|
337
|
+
geometry_type = label2id[i]["original_geometry"]
|
|
338
|
+
for j, mask in enumerate(binary_masks[1:], 1):
|
|
339
|
+
if inference_request["cancel_inference"]:
|
|
340
|
+
return
|
|
341
|
+
if tracker_interface.is_stopped():
|
|
342
|
+
reason = tracker_interface.stop_reason()
|
|
343
|
+
if isinstance(reason, Exception):
|
|
344
|
+
raise reason
|
|
345
|
+
return
|
|
346
|
+
this_figure_index = frame_index + j * direction_n
|
|
347
|
+
# check if mask is not empty
|
|
348
|
+
if not np.any(mask):
|
|
349
|
+
api.logger.info(f"Skipping empty mask on frame {this_figure_index}")
|
|
350
|
+
# update progress bar anyway (otherwise it will not be finished)
|
|
351
|
+
progress.iter_done_report()
|
|
352
|
+
else:
|
|
353
|
+
if geometry_type == "polygon":
|
|
354
|
+
bitmap_geometry = Bitmap(mask)
|
|
355
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
356
|
+
bitmap_label = Label(bitmap_geometry, bitmap_obj_class)
|
|
357
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
358
|
+
polygon_labels = bitmap_label.convert(polygon_obj_class)
|
|
359
|
+
geometries = [label.geometry for label in polygon_labels]
|
|
360
|
+
else:
|
|
361
|
+
geometries = [Bitmap(mask)]
|
|
362
|
+
for l, geometry in enumerate(geometries):
|
|
363
|
+
figure_id = uuid.uuid5(
|
|
364
|
+
namespace=uuid.NAMESPACE_URL, name=f"{time.time()}"
|
|
365
|
+
).hex
|
|
366
|
+
result_figure = api.video.figure._convert_json_info(
|
|
367
|
+
{
|
|
368
|
+
ApiField.ID: figure_id,
|
|
369
|
+
ApiField.OBJECT_ID: obj_id,
|
|
370
|
+
"meta": {"frame": this_figure_index},
|
|
371
|
+
ApiField.GEOMETRY_TYPE: geometry.geometry_name(),
|
|
372
|
+
ApiField.GEOMETRY: geometry.to_json(),
|
|
373
|
+
ApiField.TRACK_ID: tracker_interface.track_id,
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
should_notify = l == len(geometries) - 1
|
|
377
|
+
tracker_interface.add_prediction((result_figure, should_notify))
|
|
378
|
+
api.logger.info(
|
|
379
|
+
"Figure [%d, %d] tracked.",
|
|
380
|
+
i,
|
|
381
|
+
len(figures),
|
|
382
|
+
extra={"figure_id": figure.id},
|
|
383
|
+
)
|
|
384
|
+
except Exception:
|
|
385
|
+
progress.message = "Error occured during tracking"
|
|
386
|
+
raise
|
|
387
|
+
else:
|
|
388
|
+
progress.message = "Ready"
|
|
389
|
+
finally:
|
|
390
|
+
progress.set(current=0, total=1, report=True)
|
|
391
|
+
|
|
392
|
+
# Implement the following methods in the derived class
|
|
393
|
+
def track(self, api: Api, state: Dict, context: Dict):
|
|
394
|
+
fn = self.send_error_data(api, context)(self._track)
|
|
395
|
+
self.schedule_task(fn, api, context)
|
|
396
|
+
return {"message": "Tracking has started."}
|
|
56
397
|
|
|
57
|
-
def
|
|
398
|
+
def track_api(self, api: Api, state: Dict, context: Dict):
|
|
58
399
|
# unused fields:
|
|
59
400
|
context["trackId"] = "auto"
|
|
60
401
|
context["objectIds"] = []
|
|
@@ -104,13 +445,13 @@ class MaskTracking(Inference):
|
|
|
104
445
|
|
|
105
446
|
for input_geom in input_geometries:
|
|
106
447
|
geometry = self._deserialize_geometry(input_geom)
|
|
107
|
-
if not isinstance(geometry,
|
|
448
|
+
if not isinstance(geometry, Bitmap) and not isinstance(geometry, Polygon):
|
|
108
449
|
raise TypeError(f"This app does not support {geometry.geometry_name()} tracking")
|
|
109
450
|
# convert polygon to bitmap
|
|
110
|
-
if isinstance(geometry,
|
|
111
|
-
polygon_obj_class =
|
|
112
|
-
polygon_label =
|
|
113
|
-
bitmap_obj_class =
|
|
451
|
+
if isinstance(geometry, Polygon):
|
|
452
|
+
polygon_obj_class = ObjClass("polygon", Polygon)
|
|
453
|
+
polygon_label = Label(geometry, polygon_obj_class)
|
|
454
|
+
bitmap_obj_class = ObjClass("bitmap", Bitmap)
|
|
114
455
|
bitmap_label = polygon_label.convert(bitmap_obj_class)[0]
|
|
115
456
|
geometry = bitmap_label.geometry
|
|
116
457
|
if i == 0:
|
|
@@ -143,7 +484,7 @@ class MaskTracking(Inference):
|
|
|
143
484
|
predictions_for_label.append(None)
|
|
144
485
|
else:
|
|
145
486
|
# frame_idx = j + 1
|
|
146
|
-
geometry =
|
|
487
|
+
geometry = Bitmap(mask)
|
|
147
488
|
predictions_for_label.append(
|
|
148
489
|
{"type": geometry.geometry_name(), "data": geometry.to_json()}
|
|
149
490
|
)
|
|
@@ -153,295 +494,45 @@ class MaskTracking(Inference):
|
|
|
153
494
|
predictions = list(map(list, zip(*predictions)))
|
|
154
495
|
return predictions
|
|
155
496
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
raise TypeError(f"This app does not support {geometry.geometry_name()} tracking")
|
|
161
|
-
|
|
162
|
-
# combine several binary masks into one multilabel mask
|
|
163
|
-
i = 0
|
|
164
|
-
label2id = {}
|
|
165
|
-
|
|
166
|
-
original_geometry = geometry.clone()
|
|
167
|
-
|
|
168
|
-
# convert polygon to bitmap
|
|
169
|
-
if isinstance(geometry, sly.Polygon):
|
|
170
|
-
polygon_obj_class = sly.ObjClass("polygon", sly.Polygon)
|
|
171
|
-
polygon_label = sly.Label(geometry, polygon_obj_class)
|
|
172
|
-
bitmap_obj_class = sly.ObjClass("bitmap", sly.Bitmap)
|
|
173
|
-
bitmap_label = polygon_label.convert(bitmap_obj_class)[0]
|
|
174
|
-
geometry = bitmap_label.geometry
|
|
175
|
-
if i == 0:
|
|
176
|
-
multilabel_mask = geometry.data.astype(int)
|
|
177
|
-
multilabel_mask = np.zeros(frames[0].shape, dtype=np.uint8)
|
|
178
|
-
geometry.draw(bitmap=multilabel_mask, color=[1, 1, 1])
|
|
179
|
-
i += 1
|
|
180
|
-
else:
|
|
181
|
-
i += 1
|
|
182
|
-
geometry.draw(bitmap=multilabel_mask, color=[i, i, i])
|
|
183
|
-
label2id[i] = {
|
|
184
|
-
"original_geometry": original_geometry.geometry_name(),
|
|
185
|
-
}
|
|
186
|
-
# run tracker
|
|
187
|
-
tracked_multilabel_masks = self.predict(
|
|
188
|
-
frames=frames, input_mask=multilabel_mask[:, :, 0]
|
|
189
|
-
)
|
|
190
|
-
tracked_multilabel_masks = np.array(tracked_multilabel_masks)
|
|
191
|
-
# decompose multilabel masks into binary masks
|
|
192
|
-
for i in np.unique(tracked_multilabel_masks):
|
|
193
|
-
if i != 0:
|
|
194
|
-
binary_masks = tracked_multilabel_masks == i
|
|
195
|
-
geometry_type = label2id[i]["original_geometry"]
|
|
196
|
-
for j, mask in enumerate(binary_masks[1:]):
|
|
197
|
-
# check if mask is not empty
|
|
198
|
-
if np.any(mask):
|
|
199
|
-
if geometry_type == "polygon":
|
|
200
|
-
bitmap_geometry = sly.Bitmap(mask)
|
|
201
|
-
bitmap_obj_class = sly.ObjClass("bitmap", sly.Bitmap)
|
|
202
|
-
bitmap_label = sly.Label(bitmap_geometry, bitmap_obj_class)
|
|
203
|
-
polygon_obj_class = sly.ObjClass("polygon", sly.Polygon)
|
|
204
|
-
polygon_labels = bitmap_label.convert(polygon_obj_class)
|
|
205
|
-
geometries = [label.geometry for label in polygon_labels]
|
|
206
|
-
else:
|
|
207
|
-
geometries = [sly.Bitmap(mask)]
|
|
208
|
-
results[j].extend(
|
|
209
|
-
[
|
|
210
|
-
{"type": geom.geometry_name(), "data": geom.to_json()}
|
|
211
|
-
for geom in geometries
|
|
212
|
-
]
|
|
213
|
-
)
|
|
214
|
-
return results
|
|
215
|
-
|
|
216
|
-
def _track_api_cached(self, request: Request, context: dict):
|
|
217
|
-
sly.logger.info(f"Start tracking with settings: {context}.")
|
|
218
|
-
video_id = context["video_id"]
|
|
219
|
-
frame_indexes = list(
|
|
220
|
-
range(context["frame_index"], context["frame_index"] + context["frames"] + 1)
|
|
221
|
-
)
|
|
222
|
-
geometries = map(self._deserialize_geometry, context["input_geometries"])
|
|
223
|
-
frames = self.cache.get_frames_from_cache(video_id, frame_indexes)
|
|
224
|
-
return self._inference(frames, geometries)
|
|
225
|
-
|
|
226
|
-
def _track_api_files(
|
|
227
|
-
self, request: Request, files: List[UploadFile], settings: str = Form("{}")
|
|
497
|
+
def track_api_files(
|
|
498
|
+
self,
|
|
499
|
+
files: List[BinaryIO],
|
|
500
|
+
settings: Dict,
|
|
228
501
|
):
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
502
|
+
logger.info(f"Start tracking with settings:", extra={"settings": settings})
|
|
503
|
+
frame_index = find_value_by_keys(settings, ["frame_index", "frameIndex"])
|
|
504
|
+
frames_count = find_value_by_keys(settings, ["frames", "framesCount"])
|
|
505
|
+
input_geometries = find_value_by_keys(settings, ["input_geometries", "inputGeometries"])
|
|
506
|
+
direction = settings.get("direction", "forward")
|
|
507
|
+
direction_n = 1 if direction == "forward" else -1
|
|
232
508
|
frame_indexes = list(
|
|
233
|
-
range(
|
|
509
|
+
range(frame_index, frame_index + frames_count * direction_n + direction_n, direction_n)
|
|
234
510
|
)
|
|
235
|
-
geometries = map(self._deserialize_geometry,
|
|
511
|
+
geometries = map(self._deserialize_geometry, input_geometries)
|
|
236
512
|
frames = []
|
|
237
513
|
for file, frame_idx in zip(files, frame_indexes):
|
|
238
|
-
img_bytes = file.
|
|
239
|
-
frame =
|
|
514
|
+
img_bytes = file.read()
|
|
515
|
+
frame = sly_image.read_bytes(img_bytes)
|
|
240
516
|
frames.append(frame)
|
|
241
|
-
|
|
517
|
+
logger.info("Start tracking.")
|
|
242
518
|
return self._inference(frames, geometries)
|
|
243
519
|
|
|
244
|
-
def
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
@server.post("/track")
|
|
251
|
-
def start_track(request: Request, task: BackgroundTasks):
|
|
252
|
-
task.add_task(track, request)
|
|
253
|
-
return {"message": "Track task started."}
|
|
254
|
-
|
|
255
|
-
@server.post("/track-api")
|
|
256
|
-
def track_api(request: Request):
|
|
257
|
-
return self._track_api(request.state.api, request.state.context)
|
|
258
|
-
|
|
259
|
-
@server.post("/track-api-files")
|
|
260
|
-
def track_api_files(
|
|
261
|
-
request: Request,
|
|
262
|
-
files: List[UploadFile],
|
|
263
|
-
settings: str = Form("{}"),
|
|
264
|
-
):
|
|
265
|
-
return self._track_api_files(request, files, settings)
|
|
266
|
-
|
|
267
|
-
def send_error_data(func):
|
|
268
|
-
@functools.wraps(func)
|
|
269
|
-
def wrapper(*args, **kwargs):
|
|
270
|
-
value = None
|
|
271
|
-
try:
|
|
272
|
-
value = func(*args, **kwargs)
|
|
273
|
-
except Exception as exc:
|
|
274
|
-
# print("An error occured:")
|
|
275
|
-
# print(traceback.format_exc())
|
|
276
|
-
request: Request = args[0]
|
|
277
|
-
context = request.state.context
|
|
278
|
-
api: sly.Api = request.state.api
|
|
279
|
-
track_id = context["trackId"]
|
|
280
|
-
api.logger.error(f"An error occured: {repr(exc)}")
|
|
281
|
-
|
|
282
|
-
api.post(
|
|
283
|
-
"videos.notify-annotation-tool",
|
|
284
|
-
data={
|
|
285
|
-
"type": "videos:tracking-error",
|
|
286
|
-
"data": {
|
|
287
|
-
"trackId": track_id,
|
|
288
|
-
"error": {"message": repr(exc)},
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
)
|
|
292
|
-
return value
|
|
293
|
-
|
|
294
|
-
return wrapper
|
|
295
|
-
|
|
296
|
-
@send_error_data
|
|
297
|
-
def track(request: Request):
|
|
298
|
-
context = request.state.context
|
|
299
|
-
api: sly.Api = request.state.api
|
|
300
|
-
|
|
301
|
-
self.video_interface = TrackerInterface(
|
|
302
|
-
context=context,
|
|
303
|
-
api=api,
|
|
304
|
-
load_all_frames=True,
|
|
305
|
-
notify_in_predict=True,
|
|
306
|
-
per_point_polygon_tracking=False,
|
|
307
|
-
frame_loader=self.cache.download_frame,
|
|
308
|
-
frames_loader=self.cache.download_frames,
|
|
520
|
+
def track_async(self, api: Api, state: Dict, context: Dict):
|
|
521
|
+
batch_size = context.get("batch_size", self.get_batch_size())
|
|
522
|
+
if self.max_batch_size is not None and batch_size > self.max_batch_size:
|
|
523
|
+
raise ValidationError(
|
|
524
|
+
f"Batch size should be less than or equal to {self.max_batch_size} for this model."
|
|
309
525
|
)
|
|
310
|
-
range_of_frames = [
|
|
311
|
-
self.video_interface.frames_indexes[0],
|
|
312
|
-
self.video_interface.frames_indexes[-1],
|
|
313
|
-
]
|
|
314
|
-
|
|
315
|
-
if self.cache.is_persistent:
|
|
316
|
-
# if cache is persistent, run cache task for whole video
|
|
317
|
-
self.cache.run_cache_task_manually(
|
|
318
|
-
api,
|
|
319
|
-
None,
|
|
320
|
-
video_id=self.video_interface.video_id,
|
|
321
|
-
)
|
|
322
|
-
else:
|
|
323
|
-
# if cache is not persistent, run cache task for range of frames
|
|
324
|
-
self.cache.run_cache_task_manually(
|
|
325
|
-
api,
|
|
326
|
-
[range_of_frames],
|
|
327
|
-
video_id=self.video_interface.video_id,
|
|
328
|
-
)
|
|
329
526
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# combine several binary masks into one multilabel mask
|
|
334
|
-
i = 0
|
|
335
|
-
label2id = {}
|
|
527
|
+
inference_request_uuid = uuid.uuid5(namespace=uuid.NAMESPACE_URL, name=f"{time.time()}").hex
|
|
528
|
+
fn = self.send_error_data(api, context)(self._track_async)
|
|
529
|
+
self.schedule_task(fn, api, context, inference_request_uuid=inference_request_uuid)
|
|
336
530
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
continue
|
|
346
|
-
if stop_event.is_set():
|
|
347
|
-
video_interface._notify(True, task="stop tracking")
|
|
348
|
-
return
|
|
349
|
-
time.sleep(1)
|
|
350
|
-
except Exception as e:
|
|
351
|
-
api.logger.error("Error in upload loop: %s", str(e), exc_info=True)
|
|
352
|
-
video_interface._notify(True, task="stop tracking")
|
|
353
|
-
video_interface.global_stop_indicatior = True
|
|
354
|
-
raise
|
|
355
|
-
|
|
356
|
-
upload_queue = Queue()
|
|
357
|
-
stop_upload_event = Event()
|
|
358
|
-
Thread(
|
|
359
|
-
target=_upload_loop,
|
|
360
|
-
args=[upload_queue, stop_upload_event, self.video_interface],
|
|
361
|
-
daemon=True,
|
|
362
|
-
).start()
|
|
363
|
-
|
|
364
|
-
try:
|
|
365
|
-
for (fig_id, geometry), obj_id in zip(
|
|
366
|
-
self.video_interface.geometries.items(),
|
|
367
|
-
self.video_interface.object_ids,
|
|
368
|
-
):
|
|
369
|
-
original_geometry = geometry.clone()
|
|
370
|
-
if not isinstance(geometry, sly.Bitmap) and not isinstance(
|
|
371
|
-
geometry, sly.Polygon
|
|
372
|
-
):
|
|
373
|
-
stop_upload_event.set()
|
|
374
|
-
raise TypeError(
|
|
375
|
-
f"This app does not support {geometry.geometry_name()} tracking"
|
|
376
|
-
)
|
|
377
|
-
# convert polygon to bitmap
|
|
378
|
-
if isinstance(geometry, sly.Polygon):
|
|
379
|
-
polygon_obj_class = sly.ObjClass("polygon", sly.Polygon)
|
|
380
|
-
polygon_label = sly.Label(geometry, polygon_obj_class)
|
|
381
|
-
bitmap_obj_class = sly.ObjClass("bitmap", sly.Bitmap)
|
|
382
|
-
bitmap_label = polygon_label.convert(bitmap_obj_class)[0]
|
|
383
|
-
geometry = bitmap_label.geometry
|
|
384
|
-
if i == 0:
|
|
385
|
-
multilabel_mask = geometry.data.astype(int)
|
|
386
|
-
multilabel_mask = np.zeros(frames[0].shape, dtype=np.uint8)
|
|
387
|
-
geometry.draw(bitmap=multilabel_mask, color=[1, 1, 1])
|
|
388
|
-
i += 1
|
|
389
|
-
else:
|
|
390
|
-
i += 1
|
|
391
|
-
geometry.draw(bitmap=multilabel_mask, color=[i, i, i])
|
|
392
|
-
label2id[i] = {
|
|
393
|
-
"fig_id": fig_id,
|
|
394
|
-
"obj_id": obj_id,
|
|
395
|
-
"original_geometry": original_geometry.geometry_name(),
|
|
396
|
-
}
|
|
397
|
-
# run tracker
|
|
398
|
-
tracked_multilabel_masks = self.predict(
|
|
399
|
-
frames=frames, input_mask=multilabel_mask[:, :, 0]
|
|
400
|
-
)
|
|
401
|
-
tracked_multilabel_masks = np.array(tracked_multilabel_masks)
|
|
402
|
-
# decompose multilabel masks into binary masks
|
|
403
|
-
for i in np.unique(tracked_multilabel_masks):
|
|
404
|
-
if i != 0:
|
|
405
|
-
binary_masks = tracked_multilabel_masks == i
|
|
406
|
-
fig_id = label2id[i]["fig_id"]
|
|
407
|
-
obj_id = label2id[i]["obj_id"]
|
|
408
|
-
geometry_type = label2id[i]["original_geometry"]
|
|
409
|
-
for j, mask in enumerate(binary_masks[1:]):
|
|
410
|
-
# check if mask is not empty
|
|
411
|
-
if not np.any(mask):
|
|
412
|
-
api.logger.info(
|
|
413
|
-
f"Skipping empty mask on frame {self.video_interface.frame_index + j + 1}"
|
|
414
|
-
)
|
|
415
|
-
# update progress bar anyway (otherwise it will not be finished)
|
|
416
|
-
self.video_interface._notify(task="add geometry on frame")
|
|
417
|
-
else:
|
|
418
|
-
if geometry_type == "polygon":
|
|
419
|
-
bitmap_geometry = sly.Bitmap(mask)
|
|
420
|
-
bitmap_obj_class = sly.ObjClass("bitmap", sly.Bitmap)
|
|
421
|
-
bitmap_label = sly.Label(bitmap_geometry, bitmap_obj_class)
|
|
422
|
-
polygon_obj_class = sly.ObjClass("polygon", sly.Polygon)
|
|
423
|
-
polygon_labels = bitmap_label.convert(polygon_obj_class)
|
|
424
|
-
geometries = [label.geometry for label in polygon_labels]
|
|
425
|
-
else:
|
|
426
|
-
geometries = [sly.Bitmap(mask)]
|
|
427
|
-
for l, geometry in enumerate(geometries):
|
|
428
|
-
if l == len(geometries) - 1:
|
|
429
|
-
notify = True
|
|
430
|
-
else:
|
|
431
|
-
notify = False
|
|
432
|
-
upload_queue.put(
|
|
433
|
-
(
|
|
434
|
-
geometry,
|
|
435
|
-
obj_id,
|
|
436
|
-
self.video_interface.frames_indexes[j + 1],
|
|
437
|
-
notify,
|
|
438
|
-
)
|
|
439
|
-
)
|
|
440
|
-
if self.video_interface.global_stop_indicatior:
|
|
441
|
-
stop_upload_event.set()
|
|
442
|
-
return
|
|
443
|
-
api.logger.info(f"Figure with id {fig_id} was successfully tracked")
|
|
444
|
-
except Exception:
|
|
445
|
-
stop_upload_event.set()
|
|
446
|
-
raise
|
|
447
|
-
stop_upload_event.set()
|
|
531
|
+
logger.debug(
|
|
532
|
+
"Inference has scheduled from 'track_async' endpoint",
|
|
533
|
+
extra={"inference_request_uuid": inference_request_uuid},
|
|
534
|
+
)
|
|
535
|
+
return {
|
|
536
|
+
"message": "Inference has started.",
|
|
537
|
+
"inference_request_uuid": inference_request_uuid,
|
|
538
|
+
}
|