dtlpy 1.115.44__py3-none-any.whl → 1.116.6__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.
- dtlpy/__init__.py +491 -491
- dtlpy/__version__.py +1 -1
- dtlpy/assets/__init__.py +26 -26
- dtlpy/assets/code_server/config.yaml +2 -2
- dtlpy/assets/code_server/installation.sh +24 -24
- dtlpy/assets/code_server/launch.json +13 -13
- dtlpy/assets/code_server/settings.json +2 -2
- dtlpy/assets/main.py +53 -53
- dtlpy/assets/main_partial.py +18 -18
- dtlpy/assets/mock.json +11 -11
- dtlpy/assets/model_adapter.py +83 -83
- dtlpy/assets/package.json +61 -61
- dtlpy/assets/package_catalog.json +29 -29
- dtlpy/assets/package_gitignore +307 -307
- dtlpy/assets/service_runners/__init__.py +33 -33
- dtlpy/assets/service_runners/converter.py +96 -96
- dtlpy/assets/service_runners/multi_method.py +49 -49
- dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
- dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
- dtlpy/assets/service_runners/multi_method_item.py +52 -52
- dtlpy/assets/service_runners/multi_method_json.py +52 -52
- dtlpy/assets/service_runners/single_method.py +37 -37
- dtlpy/assets/service_runners/single_method_annotation.py +43 -43
- dtlpy/assets/service_runners/single_method_dataset.py +43 -43
- dtlpy/assets/service_runners/single_method_item.py +41 -41
- dtlpy/assets/service_runners/single_method_json.py +42 -42
- dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
- dtlpy/assets/voc_annotation_template.xml +23 -23
- dtlpy/caches/base_cache.py +32 -32
- dtlpy/caches/cache.py +473 -473
- dtlpy/caches/dl_cache.py +201 -201
- dtlpy/caches/filesystem_cache.py +89 -89
- dtlpy/caches/redis_cache.py +84 -84
- dtlpy/dlp/__init__.py +20 -20
- dtlpy/dlp/cli_utilities.py +367 -367
- dtlpy/dlp/command_executor.py +764 -764
- dtlpy/dlp/dlp +1 -1
- dtlpy/dlp/dlp.bat +1 -1
- dtlpy/dlp/dlp.py +128 -128
- dtlpy/dlp/parser.py +651 -651
- dtlpy/entities/__init__.py +83 -83
- dtlpy/entities/analytic.py +347 -347
- dtlpy/entities/annotation.py +1879 -1879
- dtlpy/entities/annotation_collection.py +699 -699
- dtlpy/entities/annotation_definitions/__init__.py +20 -20
- dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
- dtlpy/entities/annotation_definitions/box.py +195 -195
- dtlpy/entities/annotation_definitions/classification.py +67 -67
- dtlpy/entities/annotation_definitions/comparison.py +72 -72
- dtlpy/entities/annotation_definitions/cube.py +204 -204
- dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
- dtlpy/entities/annotation_definitions/description.py +32 -32
- dtlpy/entities/annotation_definitions/ellipse.py +124 -124
- dtlpy/entities/annotation_definitions/free_text.py +62 -62
- dtlpy/entities/annotation_definitions/gis.py +69 -69
- dtlpy/entities/annotation_definitions/note.py +139 -139
- dtlpy/entities/annotation_definitions/point.py +117 -117
- dtlpy/entities/annotation_definitions/polygon.py +182 -182
- dtlpy/entities/annotation_definitions/polyline.py +111 -111
- dtlpy/entities/annotation_definitions/pose.py +92 -92
- dtlpy/entities/annotation_definitions/ref_image.py +86 -86
- dtlpy/entities/annotation_definitions/segmentation.py +240 -240
- dtlpy/entities/annotation_definitions/subtitle.py +34 -34
- dtlpy/entities/annotation_definitions/text.py +85 -85
- dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
- dtlpy/entities/app.py +220 -220
- dtlpy/entities/app_module.py +107 -107
- dtlpy/entities/artifact.py +174 -174
- dtlpy/entities/assignment.py +399 -399
- dtlpy/entities/base_entity.py +214 -214
- dtlpy/entities/bot.py +113 -113
- dtlpy/entities/codebase.py +292 -292
- dtlpy/entities/collection.py +38 -38
- dtlpy/entities/command.py +169 -169
- dtlpy/entities/compute.py +449 -449
- dtlpy/entities/dataset.py +1299 -1299
- dtlpy/entities/directory_tree.py +44 -44
- dtlpy/entities/dpk.py +470 -470
- dtlpy/entities/driver.py +235 -235
- dtlpy/entities/execution.py +397 -397
- dtlpy/entities/feature.py +124 -124
- dtlpy/entities/feature_set.py +145 -145
- dtlpy/entities/filters.py +798 -798
- dtlpy/entities/gis_item.py +107 -107
- dtlpy/entities/integration.py +184 -184
- dtlpy/entities/item.py +959 -959
- dtlpy/entities/label.py +123 -123
- dtlpy/entities/links.py +85 -85
- dtlpy/entities/message.py +175 -175
- dtlpy/entities/model.py +684 -684
- dtlpy/entities/node.py +1005 -1005
- dtlpy/entities/ontology.py +810 -803
- dtlpy/entities/organization.py +287 -287
- dtlpy/entities/package.py +657 -657
- dtlpy/entities/package_defaults.py +5 -5
- dtlpy/entities/package_function.py +185 -185
- dtlpy/entities/package_module.py +113 -113
- dtlpy/entities/package_slot.py +118 -118
- dtlpy/entities/paged_entities.py +299 -299
- dtlpy/entities/pipeline.py +624 -624
- dtlpy/entities/pipeline_execution.py +279 -279
- dtlpy/entities/project.py +394 -394
- dtlpy/entities/prompt_item.py +505 -505
- dtlpy/entities/recipe.py +301 -301
- dtlpy/entities/reflect_dict.py +102 -102
- dtlpy/entities/resource_execution.py +138 -138
- dtlpy/entities/service.py +963 -963
- dtlpy/entities/service_driver.py +117 -117
- dtlpy/entities/setting.py +294 -294
- dtlpy/entities/task.py +495 -495
- dtlpy/entities/time_series.py +143 -143
- dtlpy/entities/trigger.py +426 -426
- dtlpy/entities/user.py +118 -118
- dtlpy/entities/webhook.py +124 -124
- dtlpy/examples/__init__.py +19 -19
- dtlpy/examples/add_labels.py +135 -135
- dtlpy/examples/add_metadata_to_item.py +21 -21
- dtlpy/examples/annotate_items_using_model.py +65 -65
- dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
- dtlpy/examples/annotations_convert_to_voc.py +9 -9
- dtlpy/examples/annotations_convert_to_yolo.py +9 -9
- dtlpy/examples/convert_annotation_types.py +51 -51
- dtlpy/examples/converter.py +143 -143
- dtlpy/examples/copy_annotations.py +22 -22
- dtlpy/examples/copy_folder.py +31 -31
- dtlpy/examples/create_annotations.py +51 -51
- dtlpy/examples/create_video_annotations.py +83 -83
- dtlpy/examples/delete_annotations.py +26 -26
- dtlpy/examples/filters.py +113 -113
- dtlpy/examples/move_item.py +23 -23
- dtlpy/examples/play_video_annotation.py +13 -13
- dtlpy/examples/show_item_and_mask.py +53 -53
- dtlpy/examples/triggers.py +49 -49
- dtlpy/examples/upload_batch_of_items.py +20 -20
- dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
- dtlpy/examples/upload_items_with_modalities.py +43 -43
- dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
- dtlpy/examples/upload_yolo_format_annotations.py +70 -70
- dtlpy/exceptions.py +125 -125
- dtlpy/miscellaneous/__init__.py +20 -20
- dtlpy/miscellaneous/dict_differ.py +95 -95
- dtlpy/miscellaneous/git_utils.py +217 -217
- dtlpy/miscellaneous/json_utils.py +14 -14
- dtlpy/miscellaneous/list_print.py +105 -105
- dtlpy/miscellaneous/zipping.py +130 -130
- dtlpy/ml/__init__.py +20 -20
- dtlpy/ml/base_feature_extractor_adapter.py +27 -27
- dtlpy/ml/base_model_adapter.py +1257 -1230
- dtlpy/ml/metrics.py +461 -461
- dtlpy/ml/predictions_utils.py +274 -274
- dtlpy/ml/summary_writer.py +57 -57
- dtlpy/ml/train_utils.py +60 -60
- dtlpy/new_instance.py +252 -252
- dtlpy/repositories/__init__.py +56 -56
- dtlpy/repositories/analytics.py +85 -85
- dtlpy/repositories/annotations.py +916 -916
- dtlpy/repositories/apps.py +383 -383
- dtlpy/repositories/artifacts.py +452 -452
- dtlpy/repositories/assignments.py +599 -599
- dtlpy/repositories/bots.py +213 -213
- dtlpy/repositories/codebases.py +559 -559
- dtlpy/repositories/collections.py +332 -332
- dtlpy/repositories/commands.py +152 -152
- dtlpy/repositories/compositions.py +61 -61
- dtlpy/repositories/computes.py +439 -439
- dtlpy/repositories/datasets.py +1504 -1504
- dtlpy/repositories/downloader.py +976 -923
- dtlpy/repositories/dpks.py +433 -433
- dtlpy/repositories/drivers.py +482 -482
- dtlpy/repositories/executions.py +815 -815
- dtlpy/repositories/feature_sets.py +226 -226
- dtlpy/repositories/features.py +255 -255
- dtlpy/repositories/integrations.py +484 -484
- dtlpy/repositories/items.py +912 -912
- dtlpy/repositories/messages.py +94 -94
- dtlpy/repositories/models.py +1000 -1000
- dtlpy/repositories/nodes.py +80 -80
- dtlpy/repositories/ontologies.py +511 -511
- dtlpy/repositories/organizations.py +525 -525
- dtlpy/repositories/packages.py +1941 -1941
- dtlpy/repositories/pipeline_executions.py +451 -451
- dtlpy/repositories/pipelines.py +640 -640
- dtlpy/repositories/projects.py +539 -539
- dtlpy/repositories/recipes.py +419 -399
- dtlpy/repositories/resource_executions.py +137 -137
- dtlpy/repositories/schema.py +120 -120
- dtlpy/repositories/service_drivers.py +213 -213
- dtlpy/repositories/services.py +1704 -1704
- dtlpy/repositories/settings.py +339 -339
- dtlpy/repositories/tasks.py +1477 -1477
- dtlpy/repositories/times_series.py +278 -278
- dtlpy/repositories/triggers.py +536 -536
- dtlpy/repositories/upload_element.py +257 -257
- dtlpy/repositories/uploader.py +661 -661
- dtlpy/repositories/webhooks.py +249 -249
- dtlpy/services/__init__.py +22 -22
- dtlpy/services/aihttp_retry.py +131 -131
- dtlpy/services/api_client.py +1785 -1785
- dtlpy/services/api_reference.py +40 -40
- dtlpy/services/async_utils.py +133 -133
- dtlpy/services/calls_counter.py +44 -44
- dtlpy/services/check_sdk.py +68 -68
- dtlpy/services/cookie.py +115 -115
- dtlpy/services/create_logger.py +156 -156
- dtlpy/services/events.py +84 -84
- dtlpy/services/logins.py +235 -235
- dtlpy/services/reporter.py +256 -256
- dtlpy/services/service_defaults.py +91 -91
- dtlpy/utilities/__init__.py +20 -20
- dtlpy/utilities/annotations/__init__.py +16 -16
- dtlpy/utilities/annotations/annotation_converters.py +269 -269
- dtlpy/utilities/base_package_runner.py +285 -264
- dtlpy/utilities/converter.py +1650 -1650
- dtlpy/utilities/dataset_generators/__init__.py +1 -1
- dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
- dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
- dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
- dtlpy/utilities/local_development/__init__.py +1 -1
- dtlpy/utilities/local_development/local_session.py +179 -179
- dtlpy/utilities/reports/__init__.py +2 -2
- dtlpy/utilities/reports/figures.py +343 -343
- dtlpy/utilities/reports/report.py +71 -71
- dtlpy/utilities/videos/__init__.py +17 -17
- dtlpy/utilities/videos/video_player.py +598 -598
- dtlpy/utilities/videos/videos.py +470 -470
- {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp +1 -1
- dtlpy-1.116.6.data/scripts/dlp.bat +2 -0
- {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp.py +128 -128
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/METADATA +186 -186
- dtlpy-1.116.6.dist-info/RECORD +239 -0
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/WHEEL +1 -1
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/licenses/LICENSE +200 -200
- tests/features/environment.py +551 -551
- dtlpy/assets/__pycache__/__init__.cpython-310.pyc +0 -0
- dtlpy-1.115.44.data/scripts/dlp.bat +0 -2
- dtlpy-1.115.44.dist-info/RECORD +0 -240
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/entry_points.txt +0 -0
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/top_level.txt +0 -0
|
@@ -1,598 +1,598 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import json
|
|
3
|
-
import time
|
|
4
|
-
import os
|
|
5
|
-
import numpy as np
|
|
6
|
-
import dtlpy as dl
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(name='dtlpy')
|
|
9
|
-
|
|
10
|
-
class VideoPlayer:
|
|
11
|
-
"""
|
|
12
|
-
Video Player GUI.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
def __init__(self, project_name=None, project_id=None,
|
|
16
|
-
dataset_name=None, dataset_id=None,
|
|
17
|
-
item_filepath=None, item_id=None):
|
|
18
|
-
try:
|
|
19
|
-
import tkinter
|
|
20
|
-
except ImportError:
|
|
21
|
-
logger.error(
|
|
22
|
-
'Import Error! Cant import tkinter. Annotations operations will be limited. import manually and fix errors')
|
|
23
|
-
raise
|
|
24
|
-
|
|
25
|
-
# init tkinter window
|
|
26
|
-
self.window = tkinter.Tk()
|
|
27
|
-
self.window.title('Dataloop Player')
|
|
28
|
-
|
|
29
|
-
self.video_source = None
|
|
30
|
-
self.video_annotations = None
|
|
31
|
-
self.local_annotations_filename = None
|
|
32
|
-
self.labels = None
|
|
33
|
-
self.project = None
|
|
34
|
-
self.dataset = None
|
|
35
|
-
self.item = None
|
|
36
|
-
#############################
|
|
37
|
-
# load video and annotation #
|
|
38
|
-
#############################
|
|
39
|
-
self.platform_params = {'project_name': project_name, 'project_id': project_id,
|
|
40
|
-
'dataset_name': dataset_name, 'dataset_id': dataset_id,
|
|
41
|
-
'item_filepath': item_filepath, 'item_id': item_id}
|
|
42
|
-
self.from_platform()
|
|
43
|
-
|
|
44
|
-
# load video
|
|
45
|
-
self.photo = None
|
|
46
|
-
self.vid = None
|
|
47
|
-
self.load_video()
|
|
48
|
-
|
|
49
|
-
# canvas to put frames on
|
|
50
|
-
self.canvas = tkinter.Canvas(self.window, width=self.vid.width, height=self.vid.height)
|
|
51
|
-
self.canvas.pack()
|
|
52
|
-
self.canvas.bind('<Configure>', self._resize_image)
|
|
53
|
-
|
|
54
|
-
########################
|
|
55
|
-
# create buttons panel #
|
|
56
|
-
########################
|
|
57
|
-
self.window_width = self.vid.width
|
|
58
|
-
self.window_height = self.vid.height
|
|
59
|
-
self.buttons_window = tkinter.Toplevel(master=self.window)
|
|
60
|
-
self.buttons_window.title('Controls')
|
|
61
|
-
self.buttons_window.geometry()
|
|
62
|
-
|
|
63
|
-
button_frame = tkinter.Frame(self.buttons_window)
|
|
64
|
-
button_frame.grid(sticky="W", row=0, column=0)
|
|
65
|
-
|
|
66
|
-
###############
|
|
67
|
-
# information #
|
|
68
|
-
###############
|
|
69
|
-
txt_dataset_name = tkinter.Label(button_frame)
|
|
70
|
-
txt_dataset_name.grid(sticky="W", row=2, column=0, columnspan=10)
|
|
71
|
-
txt_dataset_name.configure(text='Dataset name: %s' % self.dataset.name)
|
|
72
|
-
txt_item_name = tkinter.Label(button_frame)
|
|
73
|
-
txt_item_name.grid(sticky="W", row=3, column=0, columnspan=10)
|
|
74
|
-
txt_item_name.configure(text='Item name: %s' % self.item.name)
|
|
75
|
-
###############
|
|
76
|
-
# Exit Button #
|
|
77
|
-
###############
|
|
78
|
-
btn_exit = tkinter.Button(button_frame, text="Close", command=self.close).grid(sticky="W", row=17, column=0)
|
|
79
|
-
################
|
|
80
|
-
# Play / Pause #
|
|
81
|
-
################
|
|
82
|
-
self.btn_toggle_play = tkinter.Button(button_frame, text='Play ', command=self.toggle_play)
|
|
83
|
-
self.btn_toggle_play.grid(sticky="W", row=4, column=0)
|
|
84
|
-
###################
|
|
85
|
-
# Next prev frame #
|
|
86
|
-
###################
|
|
87
|
-
btn_next_frame = tkinter.Button(button_frame, text="Next frame", command=self.next_frame).grid(sticky="W",
|
|
88
|
-
row=4,
|
|
89
|
-
column=2)
|
|
90
|
-
btn_prev_frame = tkinter.Button(button_frame, text="Prev frame", command=self.prev_frame).grid(sticky="W",
|
|
91
|
-
row=4,
|
|
92
|
-
column=3)
|
|
93
|
-
self.btn_toggle_frame_num = tkinter.Button(button_frame,
|
|
94
|
-
text="Hide frame number",
|
|
95
|
-
command=self.toggle_show_frame_number)
|
|
96
|
-
self.btn_toggle_frame_num.grid(sticky="W", row=5, column=0, columnspan=10)
|
|
97
|
-
self.btn_toggle_annotations = tkinter.Button(button_frame,
|
|
98
|
-
text="Hide annotations",
|
|
99
|
-
command=self.toggle_show_annotations)
|
|
100
|
-
self.btn_toggle_annotations.grid(sticky="W", row=6, column=0, columnspan=10)
|
|
101
|
-
self.btn_toggle_label = tkinter.Button(button_frame,
|
|
102
|
-
text="Hide label",
|
|
103
|
-
command=self.toggle_show_label)
|
|
104
|
-
self.btn_toggle_label.grid(sticky="W", row=7, column=0, columnspan=10)
|
|
105
|
-
#################
|
|
106
|
-
# Export button #
|
|
107
|
-
#################
|
|
108
|
-
tkinter.Button(button_frame, text="Export video", command=self.export) \
|
|
109
|
-
.grid(sticky="W", row=16, column=2, columnspan=2)
|
|
110
|
-
# ################
|
|
111
|
-
# # Apply button #
|
|
112
|
-
# ################
|
|
113
|
-
# btn_apply_offset_fix = tkinter.Button(button_frame, text="Apply offset fix", command=self.apply_offset_fix) \
|
|
114
|
-
# .grid(sticky="W", row=16, column=0, columnspan=2)
|
|
115
|
-
btn_reset = tkinter.Button(button_frame, text="Reset", command=self.reset).grid(sticky="W", row=4, column=4)
|
|
116
|
-
# #####################
|
|
117
|
-
# # Annotation Offset #
|
|
118
|
-
# #####################
|
|
119
|
-
# tkinter.Label(button_frame, text="Annotation offset (in frames):") \
|
|
120
|
-
# .grid(sticky="W", row=13, column=0,
|
|
121
|
-
# columnspan=10)
|
|
122
|
-
# self.annotations_offset_entry = tkinter.Entry(button_frame)
|
|
123
|
-
# self.annotations_offset_entry.bind("<Return>", self.set_annotation_offset)
|
|
124
|
-
# self.annotations_offset_entry.grid(sticky="W", row=14, column=0, columnspan=10)
|
|
125
|
-
# self.annotation_offset_text = tkinter.Label(button_frame)
|
|
126
|
-
# self.annotation_offset_text.grid(sticky="W", row=15, column=0, columnspan=10)
|
|
127
|
-
##############
|
|
128
|
-
# Set Frames #
|
|
129
|
-
##############
|
|
130
|
-
tkinter.Label(button_frame, text="Set frame (in frames):") \
|
|
131
|
-
.grid(sticky="W", row=10, column=0, columnspan=10)
|
|
132
|
-
self.current_frame_entry = tkinter.Entry(button_frame)
|
|
133
|
-
self.current_frame_entry.bind("<Return>", self.set_current_frame)
|
|
134
|
-
self.current_frame_entry.grid(sticky="W", row=11, column=0, columnspan=10)
|
|
135
|
-
self.current_frame_text = tkinter.Label(button_frame)
|
|
136
|
-
self.current_frame_text.grid(sticky="W", row=12, column=0, columnspan=10)
|
|
137
|
-
#########################
|
|
138
|
-
# Timestamp information #
|
|
139
|
-
#########################
|
|
140
|
-
self.frame_timestamp_text = tkinter.Label(button_frame)
|
|
141
|
-
self.frame_timestamp_text.grid(sticky="W", row=8, column=0, columnspan=10)
|
|
142
|
-
self.annotations_timestamp_text = tkinter.Label(button_frame)
|
|
143
|
-
self.annotations_timestamp_text.grid(sticky="W", row=9, column=0, columnspan=10)
|
|
144
|
-
|
|
145
|
-
##############
|
|
146
|
-
# Parameters #
|
|
147
|
-
##############
|
|
148
|
-
self.delay = None
|
|
149
|
-
self.annotations_timestamp = None
|
|
150
|
-
# self.annotations_offset = None
|
|
151
|
-
self.annotations_timestamp = None
|
|
152
|
-
self.show_frame_num = True
|
|
153
|
-
self.show_annotations = True
|
|
154
|
-
self.show_label = True
|
|
155
|
-
self.playing = False
|
|
156
|
-
self.init_all_params()
|
|
157
|
-
self.show_frame(0)
|
|
158
|
-
|
|
159
|
-
###############
|
|
160
|
-
# Start video #
|
|
161
|
-
###############
|
|
162
|
-
self.delay = int(1000 * 1 / self.vid.fps)
|
|
163
|
-
self.update()
|
|
164
|
-
button_frame.lift()
|
|
165
|
-
self.window.mainloop()
|
|
166
|
-
|
|
167
|
-
def _resize_image(self, event):
|
|
168
|
-
"""
|
|
169
|
-
Resize frame to match window size
|
|
170
|
-
:param event:
|
|
171
|
-
:return:
|
|
172
|
-
"""
|
|
173
|
-
self.window_width = event.width
|
|
174
|
-
self.window_height = event.height
|
|
175
|
-
|
|
176
|
-
def reset(self):
|
|
177
|
-
"""
|
|
178
|
-
Reset video and annotation
|
|
179
|
-
:return:
|
|
180
|
-
"""
|
|
181
|
-
self.from_platform()
|
|
182
|
-
self.load_video()
|
|
183
|
-
self.init_all_params()
|
|
184
|
-
self.show_frame(0)
|
|
185
|
-
|
|
186
|
-
def export(self):
|
|
187
|
-
"""
|
|
188
|
-
Create an annotated video saved to file
|
|
189
|
-
:return:
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
try:
|
|
193
|
-
import cv2
|
|
194
|
-
except (ImportError, ModuleNotFoundError):
|
|
195
|
-
logger.error(
|
|
196
|
-
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
197
|
-
raise
|
|
198
|
-
|
|
199
|
-
try:
|
|
200
|
-
from tkinter import ttk
|
|
201
|
-
except ImportError:
|
|
202
|
-
logger.error(
|
|
203
|
-
'Import Error! Cant import ttk from tkinter. Annotations operations will be limited. import manually and fix errors')
|
|
204
|
-
raise
|
|
205
|
-
|
|
206
|
-
# start progress bar
|
|
207
|
-
p, ext = os.path.splitext(self.video_source)
|
|
208
|
-
output_filename = p + '_out.mp4'
|
|
209
|
-
|
|
210
|
-
# read input video
|
|
211
|
-
reader = cv2.VideoCapture(self.video_source)
|
|
212
|
-
width = int(reader.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
213
|
-
height = int(reader.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
214
|
-
fps = reader.get(cv2.CAP_PROP_FPS)
|
|
215
|
-
encoding = int(reader.get(cv2.CAP_PROP_FOURCC))
|
|
216
|
-
n_frames = int(reader.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
217
|
-
writer = cv2.VideoWriter(output_filename, cv2.VideoWriter_fourcc(*'MP4V'), fps, (width, height))
|
|
218
|
-
|
|
219
|
-
# create popup
|
|
220
|
-
popup = tkinter.Toplevel()
|
|
221
|
-
tkinter.Label(popup, text='Exporting to\n%s' % output_filename).grid(row=0, column=0)
|
|
222
|
-
progress_var = tkinter.DoubleVar()
|
|
223
|
-
progress_bar = ttk.Progressbar(popup, variable=progress_var, maximum=n_frames)
|
|
224
|
-
progress_bar.grid(row=1, column=0) # .pack(fill=tk.X, expand=1, side=tk.BOTTOM)
|
|
225
|
-
popup.pack_slaves()
|
|
226
|
-
|
|
227
|
-
i_frame = 0
|
|
228
|
-
while reader.isOpened():
|
|
229
|
-
popup.update()
|
|
230
|
-
ret, frame = reader.read()
|
|
231
|
-
if not ret:
|
|
232
|
-
break
|
|
233
|
-
# mark on frame
|
|
234
|
-
annotations = [frame_annotation.get_annotation_by_frame(frame=i_frame)
|
|
235
|
-
for frame_annotation in self.video_annotations]
|
|
236
|
-
|
|
237
|
-
for annotation in annotations:
|
|
238
|
-
if annotation is None:
|
|
239
|
-
continue
|
|
240
|
-
frame = annotation.show(image=frame,
|
|
241
|
-
color=self.get_class_color(annotation.label))
|
|
242
|
-
if self.show_label:
|
|
243
|
-
text = '%s-%s' % (annotation.label, ','.join(annotation.attributes))
|
|
244
|
-
frame = cv2.putText(frame,
|
|
245
|
-
text=text,
|
|
246
|
-
org=tuple([int(np.round(annotation.left)), int(np.round(annotation.top))]),
|
|
247
|
-
color=(255, 0, 0),
|
|
248
|
-
fontFace=cv2.FONT_HERSHEY_DUPLEX,
|
|
249
|
-
fontScale=1,
|
|
250
|
-
thickness=2)
|
|
251
|
-
# write
|
|
252
|
-
writer.write(frame)
|
|
253
|
-
i_frame += 1
|
|
254
|
-
progress_var.set(i_frame)
|
|
255
|
-
reader.release()
|
|
256
|
-
writer.release()
|
|
257
|
-
popup.destroy()
|
|
258
|
-
|
|
259
|
-
def load_video(self):
|
|
260
|
-
"""
|
|
261
|
-
Load VideoCapture instance
|
|
262
|
-
:return:
|
|
263
|
-
"""
|
|
264
|
-
self.vid = VideoCapture(self.video_source)
|
|
265
|
-
|
|
266
|
-
def init_all_params(self):
|
|
267
|
-
"""
|
|
268
|
-
Reset all initial variables
|
|
269
|
-
:return:
|
|
270
|
-
"""
|
|
271
|
-
self.annotations_timestamp = 0
|
|
272
|
-
# self.annotations_offset = 0
|
|
273
|
-
# self.annotation_offset_text.configure(text='Current: %d' % self.annotations_offset)
|
|
274
|
-
self.annotations_timestamp_text.configure(text='Annotation timestamp:\n %d' % self.annotations_timestamp)
|
|
275
|
-
self.annotations_timestamp_text.grid(sticky="W", row=9, column=0, columnspan=10)
|
|
276
|
-
# set text frames
|
|
277
|
-
# self.annotations_offset_entry.delete(0, 'end')
|
|
278
|
-
# self.annotations_offset_entry.insert(0, str(self.annotations_offset))
|
|
279
|
-
self.current_frame_entry.delete(0, 'end')
|
|
280
|
-
self.current_frame_entry.insert(0, str(self.vid.frame_number))
|
|
281
|
-
|
|
282
|
-
def from_local(self):
|
|
283
|
-
"""
|
|
284
|
-
Load local video and annotation
|
|
285
|
-
:return:
|
|
286
|
-
"""
|
|
287
|
-
if self.local_annotations_filename is not None:
|
|
288
|
-
with open(self.local_annotations_filename, 'r') as f:
|
|
289
|
-
data = json.load(f)
|
|
290
|
-
self.video_annotations = dl.AnnotationCollection.from_json(data['annotations'])
|
|
291
|
-
|
|
292
|
-
def from_platform(self):
|
|
293
|
-
"""
|
|
294
|
-
Load video and annotations from platform
|
|
295
|
-
:return:
|
|
296
|
-
"""
|
|
297
|
-
project_name = self.platform_params['project_name']
|
|
298
|
-
project_id = self.platform_params['project_id']
|
|
299
|
-
dataset_name = self.platform_params['dataset_name']
|
|
300
|
-
dataset_id = self.platform_params['dataset_id']
|
|
301
|
-
item_filepath = self.platform_params['item_filepath']
|
|
302
|
-
item_id = self.platform_params['item_id']
|
|
303
|
-
|
|
304
|
-
# load remote item
|
|
305
|
-
if dataset_id is None:
|
|
306
|
-
self.project = dl.projects.get(project_name=project_name, project_id=project_id)
|
|
307
|
-
if self.project is None:
|
|
308
|
-
raise ValueError('Project doesnt exists. name: %s, id: %s' % (project_name, project_id))
|
|
309
|
-
self.dataset = self.project.datasets.get(dataset_name=dataset_name, dataset_id=dataset_id)
|
|
310
|
-
else:
|
|
311
|
-
self.dataset = dl.datasets.get(dataset_id=dataset_id)
|
|
312
|
-
if self.dataset is None:
|
|
313
|
-
raise ValueError('Dataset doesnt exists. name: %s, id: %s' % (dataset_name, dataset_id))
|
|
314
|
-
self.item = self.dataset.items.get(filepath=item_filepath, item_id=item_id)
|
|
315
|
-
if self.item is None:
|
|
316
|
-
raise ValueError('Item doesnt exists. name: %s, id: %s' % (item_filepath, item_id))
|
|
317
|
-
self.labels = {label.tag: label.rgb for label in self.dataset.labels}
|
|
318
|
-
_, ext = os.path.splitext(self.item.filename[1:])
|
|
319
|
-
video_filename = os.path.join(self.dataset.__get_local_path__(), self.item.filename[1:])
|
|
320
|
-
if not os.path.isdir(os.path.dirname(video_filename)):
|
|
321
|
-
os.makedirs(os.path.dirname(video_filename))
|
|
322
|
-
if not os.path.isfile(video_filename):
|
|
323
|
-
self.item.download(local_path=os.path.dirname(video_filename), to_items_folder=False)
|
|
324
|
-
self.video_source = video_filename
|
|
325
|
-
self.video_annotations = self.item.annotations.list()
|
|
326
|
-
|
|
327
|
-
def close(self):
|
|
328
|
-
"""
|
|
329
|
-
Terminate window and application
|
|
330
|
-
:return:
|
|
331
|
-
"""
|
|
332
|
-
self.window.destroy()
|
|
333
|
-
self.buttons_window.destroy()
|
|
334
|
-
|
|
335
|
-
#
|
|
336
|
-
# def set_annotation_offset(self, entry):
|
|
337
|
-
# """
|
|
338
|
-
# Set annotations offset to video start time
|
|
339
|
-
# :param entry:
|
|
340
|
-
# :return:
|
|
341
|
-
# """
|
|
342
|
-
# self.annotations_offset = int(self.annotations_offset_entry.get())
|
|
343
|
-
# self.annotation_offset_text.configure(text='Current: %d' % self.annotations_offset)
|
|
344
|
-
|
|
345
|
-
def set_current_frame(self, entry):
|
|
346
|
-
"""
|
|
347
|
-
Go to specific frame
|
|
348
|
-
:param entry:
|
|
349
|
-
:return:
|
|
350
|
-
"""
|
|
351
|
-
input_frame = int(self.current_frame_entry.get())
|
|
352
|
-
self.show_frame(frame_number=input_frame)
|
|
353
|
-
|
|
354
|
-
def toggle_show_frame_number(self):
|
|
355
|
-
"""
|
|
356
|
-
Show/hide frame number on frames
|
|
357
|
-
:return:
|
|
358
|
-
"""
|
|
359
|
-
if self.show_frame_num:
|
|
360
|
-
self.show_frame_num = False
|
|
361
|
-
self.btn_toggle_frame_num.config(text='Show frame num')
|
|
362
|
-
else:
|
|
363
|
-
self.show_frame_num = True
|
|
364
|
-
self.btn_toggle_frame_num.config(text='Hide frame num')
|
|
365
|
-
|
|
366
|
-
def toggle_show_annotations(self):
|
|
367
|
-
"""
|
|
368
|
-
Show/hide annotations from frame
|
|
369
|
-
:return:
|
|
370
|
-
"""
|
|
371
|
-
if self.show_annotations:
|
|
372
|
-
self.show_annotations = False
|
|
373
|
-
self.btn_toggle_annotations.config(text='Show annotations')
|
|
374
|
-
else:
|
|
375
|
-
self.show_annotations = True
|
|
376
|
-
self.btn_toggle_annotations.config(text='Hide annotations')
|
|
377
|
-
|
|
378
|
-
def toggle_show_label(self):
|
|
379
|
-
"""
|
|
380
|
-
Show/hide label per annotations
|
|
381
|
-
:return:
|
|
382
|
-
"""
|
|
383
|
-
if self.show_label:
|
|
384
|
-
self.show_label = False
|
|
385
|
-
self.btn_toggle_label.config(text='Show label')
|
|
386
|
-
else:
|
|
387
|
-
self.show_label = True
|
|
388
|
-
self.btn_toggle_label.config(text='Hide label')
|
|
389
|
-
|
|
390
|
-
def get_class_color(self, label):
|
|
391
|
-
"""
|
|
392
|
-
Color of label
|
|
393
|
-
:param label:
|
|
394
|
-
:return:
|
|
395
|
-
"""
|
|
396
|
-
if label not in self.labels:
|
|
397
|
-
print('[WARNING] label not in dataset labels: %s' % label)
|
|
398
|
-
return (255, 0, 0)
|
|
399
|
-
color = self.labels[label]
|
|
400
|
-
if isinstance(color, str):
|
|
401
|
-
if color.startswith('rgb'):
|
|
402
|
-
color = tuple(eval(color.lstrip('rgb')))
|
|
403
|
-
elif color.startswith('#'):
|
|
404
|
-
color = tuple(int(color.lstrip('#')[i:i + 2], 16) for i in (0, 2, 4))
|
|
405
|
-
else:
|
|
406
|
-
print('[WARNING] Unknown color scheme: %s' % color)
|
|
407
|
-
color = (255, 0, 0)
|
|
408
|
-
return color
|
|
409
|
-
|
|
410
|
-
def get_annotations(self, frame):
|
|
411
|
-
"""
|
|
412
|
-
Get all annotations of frame
|
|
413
|
-
:param frame:
|
|
414
|
-
:return:
|
|
415
|
-
"""
|
|
416
|
-
# self.annotations_timestamp = (self.vid.frame_number + self.annotations_offset) / self.vid.fps
|
|
417
|
-
self.annotations_timestamp = self.vid.frame_number / self.vid.fps
|
|
418
|
-
frame = self.video_annotations.get_frame(frame_num=self.vid.frame_number).show(image=frame,
|
|
419
|
-
height=frame.shape[0],
|
|
420
|
-
width=frame.shape[1],
|
|
421
|
-
with_text=self.show_label)
|
|
422
|
-
return frame
|
|
423
|
-
|
|
424
|
-
def toggle_play(self):
|
|
425
|
-
"""
|
|
426
|
-
Toggle play pause
|
|
427
|
-
:return:
|
|
428
|
-
"""
|
|
429
|
-
if self.playing:
|
|
430
|
-
# need to pause
|
|
431
|
-
self.playing = False
|
|
432
|
-
self.btn_toggle_play.config(text='Play ')
|
|
433
|
-
else:
|
|
434
|
-
# need to play
|
|
435
|
-
self.playing = True
|
|
436
|
-
self.btn_toggle_play.config(text='Pause')
|
|
437
|
-
|
|
438
|
-
def next_frame(self):
|
|
439
|
-
"""
|
|
440
|
-
Get next frame
|
|
441
|
-
:return:
|
|
442
|
-
"""
|
|
443
|
-
self.show_frame()
|
|
444
|
-
|
|
445
|
-
def prev_frame(self):
|
|
446
|
-
"""
|
|
447
|
-
Get previous frame
|
|
448
|
-
:return:
|
|
449
|
-
"""
|
|
450
|
-
self.show_frame(self.vid.frame_number - 1)
|
|
451
|
-
|
|
452
|
-
# def apply_offset_fix(self):
|
|
453
|
-
# """
|
|
454
|
-
# Apply offset to platform
|
|
455
|
-
# :return:
|
|
456
|
-
# """
|
|
457
|
-
# # delete annotations from platform
|
|
458
|
-
# for annotation in self.video_annotations.annotations:
|
|
459
|
-
# self.item.annotations.delete(annotations_id=annotation.id)
|
|
460
|
-
#
|
|
461
|
-
# # apply annotations fix
|
|
462
|
-
# new_annotations = list()
|
|
463
|
-
# for annotation in self.video_annotations.annotations:
|
|
464
|
-
# new_annotation = annotation.copy()
|
|
465
|
-
# for i in range(len(new_annotation['metadata']['snapshots_'])):
|
|
466
|
-
# new_annotation['metadata']['snapshots_'][i]['startTime'] -= (self.annotations_offset / self.vid.fps)
|
|
467
|
-
# new_annotation['metadata']['snapshots_'][i]['frameNum'] = int(
|
|
468
|
-
# new_annotation['metadata']['snapshots_'][i]['startTime'] * self.vid.fps)
|
|
469
|
-
# new_annotations.append(json.dumps(new_annotation))
|
|
470
|
-
#
|
|
471
|
-
# # upload annotations
|
|
472
|
-
# self.item.annotation.upload(annotation=[new_annotations])
|
|
473
|
-
|
|
474
|
-
def show_frame(self, frame_number=None):
|
|
475
|
-
"""
|
|
476
|
-
Get a frame from the video source
|
|
477
|
-
:param frame_number:
|
|
478
|
-
:return:
|
|
479
|
-
"""
|
|
480
|
-
try:
|
|
481
|
-
import cv2
|
|
482
|
-
except (ImportError, ModuleNotFoundError):
|
|
483
|
-
logger.error(
|
|
484
|
-
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
485
|
-
raise
|
|
486
|
-
|
|
487
|
-
try:
|
|
488
|
-
import tkinter
|
|
489
|
-
except ImportError:
|
|
490
|
-
logger.error(
|
|
491
|
-
'Import Error! Cant import tkinter. Annotations operations will be limited. import manually and fix errors')
|
|
492
|
-
raise
|
|
493
|
-
|
|
494
|
-
try:
|
|
495
|
-
import PIL.ImageTk
|
|
496
|
-
import PIL.Image
|
|
497
|
-
except ImportError:
|
|
498
|
-
logger.error(
|
|
499
|
-
'Import Error! Cant import PIL.ImageTk/PIL.ImageTk. Annotations operations will be limited. '
|
|
500
|
-
'import manually and fix errors')
|
|
501
|
-
raise
|
|
502
|
-
ret, frame = self.vid.get_frame(frame_number)
|
|
503
|
-
if ret:
|
|
504
|
-
if self.show_annotations:
|
|
505
|
-
frame = self.get_annotations(frame)
|
|
506
|
-
if self.show_frame_num:
|
|
507
|
-
text = '%d - %d' % (self.vid.frame_number, np.round(self.annotations_timestamp * self.vid.fps))
|
|
508
|
-
frame = cv2.putText(frame,
|
|
509
|
-
text=text,
|
|
510
|
-
org=(100, 100),
|
|
511
|
-
color=(0, 0, 255),
|
|
512
|
-
fontFace=cv2.FONT_HERSHEY_DUPLEX,
|
|
513
|
-
fontScale=2,
|
|
514
|
-
thickness=3)
|
|
515
|
-
|
|
516
|
-
self.photo = PIL.ImageTk.PhotoImage(
|
|
517
|
-
image=PIL.Image.fromarray(frame).resize((self.window_width, self.window_height)),
|
|
518
|
-
master=self.canvas)
|
|
519
|
-
self.canvas.create_image(0, 0, image=self.photo, anchor=tkinter.NW)
|
|
520
|
-
# set timestamp
|
|
521
|
-
self.current_frame_text.configure(text='Frame number:\n%d' % self.vid.frame_number)
|
|
522
|
-
self.current_frame_text.grid(sticky="W", row=12, column=0, columnspan=10)
|
|
523
|
-
millis = int(1000 * self.vid.frame_number / self.vid.fps)
|
|
524
|
-
seconds = (millis / 1000) % 60
|
|
525
|
-
minutes = int((millis / (1000 * 60)) % 60)
|
|
526
|
-
hours = int((millis / (1000 * 60 * 60)) % 24)
|
|
527
|
-
self.frame_timestamp_text.configure(
|
|
528
|
-
text='Frame timestamp:\n{:02d}:{:02d}:{:.3f}'.format(hours, minutes, seconds))
|
|
529
|
-
self.frame_timestamp_text.grid(sticky="W", row=8, column=0, columnspan=10)
|
|
530
|
-
|
|
531
|
-
def update(self):
|
|
532
|
-
"""
|
|
533
|
-
Playing the movie. Get frames at the FPS and show
|
|
534
|
-
:return:
|
|
535
|
-
"""
|
|
536
|
-
tic = time.time()
|
|
537
|
-
if self.playing:
|
|
538
|
-
self.show_frame()
|
|
539
|
-
add_delay = np.maximum(1, self.delay - int(1000 * (time.time() - tic)))
|
|
540
|
-
self.window.after(ms=add_delay, func=self.update)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
class VideoCapture:
|
|
544
|
-
"""
|
|
545
|
-
Video class using cv2 to play video
|
|
546
|
-
"""
|
|
547
|
-
def __init__(self, source=0):
|
|
548
|
-
try:
|
|
549
|
-
import cv2
|
|
550
|
-
except (ImportError, ModuleNotFoundError):
|
|
551
|
-
logger.error(
|
|
552
|
-
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
553
|
-
raise
|
|
554
|
-
# Open the video source
|
|
555
|
-
self.vid = cv2.VideoCapture(source)
|
|
556
|
-
if not self.vid.isOpened():
|
|
557
|
-
raise ValueError("Unable to open video source", source)
|
|
558
|
-
|
|
559
|
-
# Get video source width and height
|
|
560
|
-
self.width = self.vid.get(cv2.CAP_PROP_FRAME_WIDTH)
|
|
561
|
-
self.height = self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
|
562
|
-
self.fps = self.vid.get(cv2.CAP_PROP_FPS)
|
|
563
|
-
self.frame_number = self.vid.get(cv2.CAP_PROP_POS_FRAMES)
|
|
564
|
-
|
|
565
|
-
def get_frame(self, frame_number=None):
|
|
566
|
-
"""
|
|
567
|
-
get the frame from video
|
|
568
|
-
:param frame_number:
|
|
569
|
-
:return:
|
|
570
|
-
"""
|
|
571
|
-
try:
|
|
572
|
-
import cv2
|
|
573
|
-
except (ImportError, ModuleNotFoundError):
|
|
574
|
-
logger.error(
|
|
575
|
-
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
576
|
-
raise
|
|
577
|
-
if self.vid.isOpened():
|
|
578
|
-
if self.frame_number is None:
|
|
579
|
-
self.frame_number = self.vid.get(cv2.CAP_PROP_POS_FRAMES)
|
|
580
|
-
else:
|
|
581
|
-
self.frame_number += 1
|
|
582
|
-
if frame_number is not None:
|
|
583
|
-
self.frame_number = frame_number
|
|
584
|
-
self.vid.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
|
585
|
-
ret, frame = self.vid.read()
|
|
586
|
-
|
|
587
|
-
if ret:
|
|
588
|
-
# Return a boolean success flag and the current frame converted to BGR
|
|
589
|
-
return ret, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
590
|
-
else:
|
|
591
|
-
return ret, None
|
|
592
|
-
else:
|
|
593
|
-
return False, None
|
|
594
|
-
|
|
595
|
-
# Release the video source when the object is destroyed
|
|
596
|
-
def __del__(self):
|
|
597
|
-
if self.vid.isOpened():
|
|
598
|
-
self.vid.release()
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import os
|
|
5
|
+
import numpy as np
|
|
6
|
+
import dtlpy as dl
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(name='dtlpy')
|
|
9
|
+
|
|
10
|
+
class VideoPlayer:
|
|
11
|
+
"""
|
|
12
|
+
Video Player GUI.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, project_name=None, project_id=None,
|
|
16
|
+
dataset_name=None, dataset_id=None,
|
|
17
|
+
item_filepath=None, item_id=None):
|
|
18
|
+
try:
|
|
19
|
+
import tkinter
|
|
20
|
+
except ImportError:
|
|
21
|
+
logger.error(
|
|
22
|
+
'Import Error! Cant import tkinter. Annotations operations will be limited. import manually and fix errors')
|
|
23
|
+
raise
|
|
24
|
+
|
|
25
|
+
# init tkinter window
|
|
26
|
+
self.window = tkinter.Tk()
|
|
27
|
+
self.window.title('Dataloop Player')
|
|
28
|
+
|
|
29
|
+
self.video_source = None
|
|
30
|
+
self.video_annotations = None
|
|
31
|
+
self.local_annotations_filename = None
|
|
32
|
+
self.labels = None
|
|
33
|
+
self.project = None
|
|
34
|
+
self.dataset = None
|
|
35
|
+
self.item = None
|
|
36
|
+
#############################
|
|
37
|
+
# load video and annotation #
|
|
38
|
+
#############################
|
|
39
|
+
self.platform_params = {'project_name': project_name, 'project_id': project_id,
|
|
40
|
+
'dataset_name': dataset_name, 'dataset_id': dataset_id,
|
|
41
|
+
'item_filepath': item_filepath, 'item_id': item_id}
|
|
42
|
+
self.from_platform()
|
|
43
|
+
|
|
44
|
+
# load video
|
|
45
|
+
self.photo = None
|
|
46
|
+
self.vid = None
|
|
47
|
+
self.load_video()
|
|
48
|
+
|
|
49
|
+
# canvas to put frames on
|
|
50
|
+
self.canvas = tkinter.Canvas(self.window, width=self.vid.width, height=self.vid.height)
|
|
51
|
+
self.canvas.pack()
|
|
52
|
+
self.canvas.bind('<Configure>', self._resize_image)
|
|
53
|
+
|
|
54
|
+
########################
|
|
55
|
+
# create buttons panel #
|
|
56
|
+
########################
|
|
57
|
+
self.window_width = self.vid.width
|
|
58
|
+
self.window_height = self.vid.height
|
|
59
|
+
self.buttons_window = tkinter.Toplevel(master=self.window)
|
|
60
|
+
self.buttons_window.title('Controls')
|
|
61
|
+
self.buttons_window.geometry()
|
|
62
|
+
|
|
63
|
+
button_frame = tkinter.Frame(self.buttons_window)
|
|
64
|
+
button_frame.grid(sticky="W", row=0, column=0)
|
|
65
|
+
|
|
66
|
+
###############
|
|
67
|
+
# information #
|
|
68
|
+
###############
|
|
69
|
+
txt_dataset_name = tkinter.Label(button_frame)
|
|
70
|
+
txt_dataset_name.grid(sticky="W", row=2, column=0, columnspan=10)
|
|
71
|
+
txt_dataset_name.configure(text='Dataset name: %s' % self.dataset.name)
|
|
72
|
+
txt_item_name = tkinter.Label(button_frame)
|
|
73
|
+
txt_item_name.grid(sticky="W", row=3, column=0, columnspan=10)
|
|
74
|
+
txt_item_name.configure(text='Item name: %s' % self.item.name)
|
|
75
|
+
###############
|
|
76
|
+
# Exit Button #
|
|
77
|
+
###############
|
|
78
|
+
btn_exit = tkinter.Button(button_frame, text="Close", command=self.close).grid(sticky="W", row=17, column=0)
|
|
79
|
+
################
|
|
80
|
+
# Play / Pause #
|
|
81
|
+
################
|
|
82
|
+
self.btn_toggle_play = tkinter.Button(button_frame, text='Play ', command=self.toggle_play)
|
|
83
|
+
self.btn_toggle_play.grid(sticky="W", row=4, column=0)
|
|
84
|
+
###################
|
|
85
|
+
# Next prev frame #
|
|
86
|
+
###################
|
|
87
|
+
btn_next_frame = tkinter.Button(button_frame, text="Next frame", command=self.next_frame).grid(sticky="W",
|
|
88
|
+
row=4,
|
|
89
|
+
column=2)
|
|
90
|
+
btn_prev_frame = tkinter.Button(button_frame, text="Prev frame", command=self.prev_frame).grid(sticky="W",
|
|
91
|
+
row=4,
|
|
92
|
+
column=3)
|
|
93
|
+
self.btn_toggle_frame_num = tkinter.Button(button_frame,
|
|
94
|
+
text="Hide frame number",
|
|
95
|
+
command=self.toggle_show_frame_number)
|
|
96
|
+
self.btn_toggle_frame_num.grid(sticky="W", row=5, column=0, columnspan=10)
|
|
97
|
+
self.btn_toggle_annotations = tkinter.Button(button_frame,
|
|
98
|
+
text="Hide annotations",
|
|
99
|
+
command=self.toggle_show_annotations)
|
|
100
|
+
self.btn_toggle_annotations.grid(sticky="W", row=6, column=0, columnspan=10)
|
|
101
|
+
self.btn_toggle_label = tkinter.Button(button_frame,
|
|
102
|
+
text="Hide label",
|
|
103
|
+
command=self.toggle_show_label)
|
|
104
|
+
self.btn_toggle_label.grid(sticky="W", row=7, column=0, columnspan=10)
|
|
105
|
+
#################
|
|
106
|
+
# Export button #
|
|
107
|
+
#################
|
|
108
|
+
tkinter.Button(button_frame, text="Export video", command=self.export) \
|
|
109
|
+
.grid(sticky="W", row=16, column=2, columnspan=2)
|
|
110
|
+
# ################
|
|
111
|
+
# # Apply button #
|
|
112
|
+
# ################
|
|
113
|
+
# btn_apply_offset_fix = tkinter.Button(button_frame, text="Apply offset fix", command=self.apply_offset_fix) \
|
|
114
|
+
# .grid(sticky="W", row=16, column=0, columnspan=2)
|
|
115
|
+
btn_reset = tkinter.Button(button_frame, text="Reset", command=self.reset).grid(sticky="W", row=4, column=4)
|
|
116
|
+
# #####################
|
|
117
|
+
# # Annotation Offset #
|
|
118
|
+
# #####################
|
|
119
|
+
# tkinter.Label(button_frame, text="Annotation offset (in frames):") \
|
|
120
|
+
# .grid(sticky="W", row=13, column=0,
|
|
121
|
+
# columnspan=10)
|
|
122
|
+
# self.annotations_offset_entry = tkinter.Entry(button_frame)
|
|
123
|
+
# self.annotations_offset_entry.bind("<Return>", self.set_annotation_offset)
|
|
124
|
+
# self.annotations_offset_entry.grid(sticky="W", row=14, column=0, columnspan=10)
|
|
125
|
+
# self.annotation_offset_text = tkinter.Label(button_frame)
|
|
126
|
+
# self.annotation_offset_text.grid(sticky="W", row=15, column=0, columnspan=10)
|
|
127
|
+
##############
|
|
128
|
+
# Set Frames #
|
|
129
|
+
##############
|
|
130
|
+
tkinter.Label(button_frame, text="Set frame (in frames):") \
|
|
131
|
+
.grid(sticky="W", row=10, column=0, columnspan=10)
|
|
132
|
+
self.current_frame_entry = tkinter.Entry(button_frame)
|
|
133
|
+
self.current_frame_entry.bind("<Return>", self.set_current_frame)
|
|
134
|
+
self.current_frame_entry.grid(sticky="W", row=11, column=0, columnspan=10)
|
|
135
|
+
self.current_frame_text = tkinter.Label(button_frame)
|
|
136
|
+
self.current_frame_text.grid(sticky="W", row=12, column=0, columnspan=10)
|
|
137
|
+
#########################
|
|
138
|
+
# Timestamp information #
|
|
139
|
+
#########################
|
|
140
|
+
self.frame_timestamp_text = tkinter.Label(button_frame)
|
|
141
|
+
self.frame_timestamp_text.grid(sticky="W", row=8, column=0, columnspan=10)
|
|
142
|
+
self.annotations_timestamp_text = tkinter.Label(button_frame)
|
|
143
|
+
self.annotations_timestamp_text.grid(sticky="W", row=9, column=0, columnspan=10)
|
|
144
|
+
|
|
145
|
+
##############
|
|
146
|
+
# Parameters #
|
|
147
|
+
##############
|
|
148
|
+
self.delay = None
|
|
149
|
+
self.annotations_timestamp = None
|
|
150
|
+
# self.annotations_offset = None
|
|
151
|
+
self.annotations_timestamp = None
|
|
152
|
+
self.show_frame_num = True
|
|
153
|
+
self.show_annotations = True
|
|
154
|
+
self.show_label = True
|
|
155
|
+
self.playing = False
|
|
156
|
+
self.init_all_params()
|
|
157
|
+
self.show_frame(0)
|
|
158
|
+
|
|
159
|
+
###############
|
|
160
|
+
# Start video #
|
|
161
|
+
###############
|
|
162
|
+
self.delay = int(1000 * 1 / self.vid.fps)
|
|
163
|
+
self.update()
|
|
164
|
+
button_frame.lift()
|
|
165
|
+
self.window.mainloop()
|
|
166
|
+
|
|
167
|
+
def _resize_image(self, event):
|
|
168
|
+
"""
|
|
169
|
+
Resize frame to match window size
|
|
170
|
+
:param event:
|
|
171
|
+
:return:
|
|
172
|
+
"""
|
|
173
|
+
self.window_width = event.width
|
|
174
|
+
self.window_height = event.height
|
|
175
|
+
|
|
176
|
+
def reset(self):
|
|
177
|
+
"""
|
|
178
|
+
Reset video and annotation
|
|
179
|
+
:return:
|
|
180
|
+
"""
|
|
181
|
+
self.from_platform()
|
|
182
|
+
self.load_video()
|
|
183
|
+
self.init_all_params()
|
|
184
|
+
self.show_frame(0)
|
|
185
|
+
|
|
186
|
+
def export(self):
|
|
187
|
+
"""
|
|
188
|
+
Create an annotated video saved to file
|
|
189
|
+
:return:
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
import cv2
|
|
194
|
+
except (ImportError, ModuleNotFoundError):
|
|
195
|
+
logger.error(
|
|
196
|
+
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
from tkinter import ttk
|
|
201
|
+
except ImportError:
|
|
202
|
+
logger.error(
|
|
203
|
+
'Import Error! Cant import ttk from tkinter. Annotations operations will be limited. import manually and fix errors')
|
|
204
|
+
raise
|
|
205
|
+
|
|
206
|
+
# start progress bar
|
|
207
|
+
p, ext = os.path.splitext(self.video_source)
|
|
208
|
+
output_filename = p + '_out.mp4'
|
|
209
|
+
|
|
210
|
+
# read input video
|
|
211
|
+
reader = cv2.VideoCapture(self.video_source)
|
|
212
|
+
width = int(reader.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
213
|
+
height = int(reader.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
214
|
+
fps = reader.get(cv2.CAP_PROP_FPS)
|
|
215
|
+
encoding = int(reader.get(cv2.CAP_PROP_FOURCC))
|
|
216
|
+
n_frames = int(reader.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
217
|
+
writer = cv2.VideoWriter(output_filename, cv2.VideoWriter_fourcc(*'MP4V'), fps, (width, height))
|
|
218
|
+
|
|
219
|
+
# create popup
|
|
220
|
+
popup = tkinter.Toplevel()
|
|
221
|
+
tkinter.Label(popup, text='Exporting to\n%s' % output_filename).grid(row=0, column=0)
|
|
222
|
+
progress_var = tkinter.DoubleVar()
|
|
223
|
+
progress_bar = ttk.Progressbar(popup, variable=progress_var, maximum=n_frames)
|
|
224
|
+
progress_bar.grid(row=1, column=0) # .pack(fill=tk.X, expand=1, side=tk.BOTTOM)
|
|
225
|
+
popup.pack_slaves()
|
|
226
|
+
|
|
227
|
+
i_frame = 0
|
|
228
|
+
while reader.isOpened():
|
|
229
|
+
popup.update()
|
|
230
|
+
ret, frame = reader.read()
|
|
231
|
+
if not ret:
|
|
232
|
+
break
|
|
233
|
+
# mark on frame
|
|
234
|
+
annotations = [frame_annotation.get_annotation_by_frame(frame=i_frame)
|
|
235
|
+
for frame_annotation in self.video_annotations]
|
|
236
|
+
|
|
237
|
+
for annotation in annotations:
|
|
238
|
+
if annotation is None:
|
|
239
|
+
continue
|
|
240
|
+
frame = annotation.show(image=frame,
|
|
241
|
+
color=self.get_class_color(annotation.label))
|
|
242
|
+
if self.show_label:
|
|
243
|
+
text = '%s-%s' % (annotation.label, ','.join(annotation.attributes))
|
|
244
|
+
frame = cv2.putText(frame,
|
|
245
|
+
text=text,
|
|
246
|
+
org=tuple([int(np.round(annotation.left)), int(np.round(annotation.top))]),
|
|
247
|
+
color=(255, 0, 0),
|
|
248
|
+
fontFace=cv2.FONT_HERSHEY_DUPLEX,
|
|
249
|
+
fontScale=1,
|
|
250
|
+
thickness=2)
|
|
251
|
+
# write
|
|
252
|
+
writer.write(frame)
|
|
253
|
+
i_frame += 1
|
|
254
|
+
progress_var.set(i_frame)
|
|
255
|
+
reader.release()
|
|
256
|
+
writer.release()
|
|
257
|
+
popup.destroy()
|
|
258
|
+
|
|
259
|
+
def load_video(self):
|
|
260
|
+
"""
|
|
261
|
+
Load VideoCapture instance
|
|
262
|
+
:return:
|
|
263
|
+
"""
|
|
264
|
+
self.vid = VideoCapture(self.video_source)
|
|
265
|
+
|
|
266
|
+
def init_all_params(self):
|
|
267
|
+
"""
|
|
268
|
+
Reset all initial variables
|
|
269
|
+
:return:
|
|
270
|
+
"""
|
|
271
|
+
self.annotations_timestamp = 0
|
|
272
|
+
# self.annotations_offset = 0
|
|
273
|
+
# self.annotation_offset_text.configure(text='Current: %d' % self.annotations_offset)
|
|
274
|
+
self.annotations_timestamp_text.configure(text='Annotation timestamp:\n %d' % self.annotations_timestamp)
|
|
275
|
+
self.annotations_timestamp_text.grid(sticky="W", row=9, column=0, columnspan=10)
|
|
276
|
+
# set text frames
|
|
277
|
+
# self.annotations_offset_entry.delete(0, 'end')
|
|
278
|
+
# self.annotations_offset_entry.insert(0, str(self.annotations_offset))
|
|
279
|
+
self.current_frame_entry.delete(0, 'end')
|
|
280
|
+
self.current_frame_entry.insert(0, str(self.vid.frame_number))
|
|
281
|
+
|
|
282
|
+
def from_local(self):
|
|
283
|
+
"""
|
|
284
|
+
Load local video and annotation
|
|
285
|
+
:return:
|
|
286
|
+
"""
|
|
287
|
+
if self.local_annotations_filename is not None:
|
|
288
|
+
with open(self.local_annotations_filename, 'r') as f:
|
|
289
|
+
data = json.load(f)
|
|
290
|
+
self.video_annotations = dl.AnnotationCollection.from_json(data['annotations'])
|
|
291
|
+
|
|
292
|
+
def from_platform(self):
|
|
293
|
+
"""
|
|
294
|
+
Load video and annotations from platform
|
|
295
|
+
:return:
|
|
296
|
+
"""
|
|
297
|
+
project_name = self.platform_params['project_name']
|
|
298
|
+
project_id = self.platform_params['project_id']
|
|
299
|
+
dataset_name = self.platform_params['dataset_name']
|
|
300
|
+
dataset_id = self.platform_params['dataset_id']
|
|
301
|
+
item_filepath = self.platform_params['item_filepath']
|
|
302
|
+
item_id = self.platform_params['item_id']
|
|
303
|
+
|
|
304
|
+
# load remote item
|
|
305
|
+
if dataset_id is None:
|
|
306
|
+
self.project = dl.projects.get(project_name=project_name, project_id=project_id)
|
|
307
|
+
if self.project is None:
|
|
308
|
+
raise ValueError('Project doesnt exists. name: %s, id: %s' % (project_name, project_id))
|
|
309
|
+
self.dataset = self.project.datasets.get(dataset_name=dataset_name, dataset_id=dataset_id)
|
|
310
|
+
else:
|
|
311
|
+
self.dataset = dl.datasets.get(dataset_id=dataset_id)
|
|
312
|
+
if self.dataset is None:
|
|
313
|
+
raise ValueError('Dataset doesnt exists. name: %s, id: %s' % (dataset_name, dataset_id))
|
|
314
|
+
self.item = self.dataset.items.get(filepath=item_filepath, item_id=item_id)
|
|
315
|
+
if self.item is None:
|
|
316
|
+
raise ValueError('Item doesnt exists. name: %s, id: %s' % (item_filepath, item_id))
|
|
317
|
+
self.labels = {label.tag: label.rgb for label in self.dataset.labels}
|
|
318
|
+
_, ext = os.path.splitext(self.item.filename[1:])
|
|
319
|
+
video_filename = os.path.join(self.dataset.__get_local_path__(), self.item.filename[1:])
|
|
320
|
+
if not os.path.isdir(os.path.dirname(video_filename)):
|
|
321
|
+
os.makedirs(os.path.dirname(video_filename))
|
|
322
|
+
if not os.path.isfile(video_filename):
|
|
323
|
+
self.item.download(local_path=os.path.dirname(video_filename), to_items_folder=False)
|
|
324
|
+
self.video_source = video_filename
|
|
325
|
+
self.video_annotations = self.item.annotations.list()
|
|
326
|
+
|
|
327
|
+
def close(self):
|
|
328
|
+
"""
|
|
329
|
+
Terminate window and application
|
|
330
|
+
:return:
|
|
331
|
+
"""
|
|
332
|
+
self.window.destroy()
|
|
333
|
+
self.buttons_window.destroy()
|
|
334
|
+
|
|
335
|
+
#
|
|
336
|
+
# def set_annotation_offset(self, entry):
|
|
337
|
+
# """
|
|
338
|
+
# Set annotations offset to video start time
|
|
339
|
+
# :param entry:
|
|
340
|
+
# :return:
|
|
341
|
+
# """
|
|
342
|
+
# self.annotations_offset = int(self.annotations_offset_entry.get())
|
|
343
|
+
# self.annotation_offset_text.configure(text='Current: %d' % self.annotations_offset)
|
|
344
|
+
|
|
345
|
+
def set_current_frame(self, entry):
|
|
346
|
+
"""
|
|
347
|
+
Go to specific frame
|
|
348
|
+
:param entry:
|
|
349
|
+
:return:
|
|
350
|
+
"""
|
|
351
|
+
input_frame = int(self.current_frame_entry.get())
|
|
352
|
+
self.show_frame(frame_number=input_frame)
|
|
353
|
+
|
|
354
|
+
def toggle_show_frame_number(self):
|
|
355
|
+
"""
|
|
356
|
+
Show/hide frame number on frames
|
|
357
|
+
:return:
|
|
358
|
+
"""
|
|
359
|
+
if self.show_frame_num:
|
|
360
|
+
self.show_frame_num = False
|
|
361
|
+
self.btn_toggle_frame_num.config(text='Show frame num')
|
|
362
|
+
else:
|
|
363
|
+
self.show_frame_num = True
|
|
364
|
+
self.btn_toggle_frame_num.config(text='Hide frame num')
|
|
365
|
+
|
|
366
|
+
def toggle_show_annotations(self):
|
|
367
|
+
"""
|
|
368
|
+
Show/hide annotations from frame
|
|
369
|
+
:return:
|
|
370
|
+
"""
|
|
371
|
+
if self.show_annotations:
|
|
372
|
+
self.show_annotations = False
|
|
373
|
+
self.btn_toggle_annotations.config(text='Show annotations')
|
|
374
|
+
else:
|
|
375
|
+
self.show_annotations = True
|
|
376
|
+
self.btn_toggle_annotations.config(text='Hide annotations')
|
|
377
|
+
|
|
378
|
+
def toggle_show_label(self):
|
|
379
|
+
"""
|
|
380
|
+
Show/hide label per annotations
|
|
381
|
+
:return:
|
|
382
|
+
"""
|
|
383
|
+
if self.show_label:
|
|
384
|
+
self.show_label = False
|
|
385
|
+
self.btn_toggle_label.config(text='Show label')
|
|
386
|
+
else:
|
|
387
|
+
self.show_label = True
|
|
388
|
+
self.btn_toggle_label.config(text='Hide label')
|
|
389
|
+
|
|
390
|
+
def get_class_color(self, label):
|
|
391
|
+
"""
|
|
392
|
+
Color of label
|
|
393
|
+
:param label:
|
|
394
|
+
:return:
|
|
395
|
+
"""
|
|
396
|
+
if label not in self.labels:
|
|
397
|
+
print('[WARNING] label not in dataset labels: %s' % label)
|
|
398
|
+
return (255, 0, 0)
|
|
399
|
+
color = self.labels[label]
|
|
400
|
+
if isinstance(color, str):
|
|
401
|
+
if color.startswith('rgb'):
|
|
402
|
+
color = tuple(eval(color.lstrip('rgb')))
|
|
403
|
+
elif color.startswith('#'):
|
|
404
|
+
color = tuple(int(color.lstrip('#')[i:i + 2], 16) for i in (0, 2, 4))
|
|
405
|
+
else:
|
|
406
|
+
print('[WARNING] Unknown color scheme: %s' % color)
|
|
407
|
+
color = (255, 0, 0)
|
|
408
|
+
return color
|
|
409
|
+
|
|
410
|
+
def get_annotations(self, frame):
|
|
411
|
+
"""
|
|
412
|
+
Get all annotations of frame
|
|
413
|
+
:param frame:
|
|
414
|
+
:return:
|
|
415
|
+
"""
|
|
416
|
+
# self.annotations_timestamp = (self.vid.frame_number + self.annotations_offset) / self.vid.fps
|
|
417
|
+
self.annotations_timestamp = self.vid.frame_number / self.vid.fps
|
|
418
|
+
frame = self.video_annotations.get_frame(frame_num=self.vid.frame_number).show(image=frame,
|
|
419
|
+
height=frame.shape[0],
|
|
420
|
+
width=frame.shape[1],
|
|
421
|
+
with_text=self.show_label)
|
|
422
|
+
return frame
|
|
423
|
+
|
|
424
|
+
def toggle_play(self):
|
|
425
|
+
"""
|
|
426
|
+
Toggle play pause
|
|
427
|
+
:return:
|
|
428
|
+
"""
|
|
429
|
+
if self.playing:
|
|
430
|
+
# need to pause
|
|
431
|
+
self.playing = False
|
|
432
|
+
self.btn_toggle_play.config(text='Play ')
|
|
433
|
+
else:
|
|
434
|
+
# need to play
|
|
435
|
+
self.playing = True
|
|
436
|
+
self.btn_toggle_play.config(text='Pause')
|
|
437
|
+
|
|
438
|
+
def next_frame(self):
|
|
439
|
+
"""
|
|
440
|
+
Get next frame
|
|
441
|
+
:return:
|
|
442
|
+
"""
|
|
443
|
+
self.show_frame()
|
|
444
|
+
|
|
445
|
+
def prev_frame(self):
|
|
446
|
+
"""
|
|
447
|
+
Get previous frame
|
|
448
|
+
:return:
|
|
449
|
+
"""
|
|
450
|
+
self.show_frame(self.vid.frame_number - 1)
|
|
451
|
+
|
|
452
|
+
# def apply_offset_fix(self):
|
|
453
|
+
# """
|
|
454
|
+
# Apply offset to platform
|
|
455
|
+
# :return:
|
|
456
|
+
# """
|
|
457
|
+
# # delete annotations from platform
|
|
458
|
+
# for annotation in self.video_annotations.annotations:
|
|
459
|
+
# self.item.annotations.delete(annotations_id=annotation.id)
|
|
460
|
+
#
|
|
461
|
+
# # apply annotations fix
|
|
462
|
+
# new_annotations = list()
|
|
463
|
+
# for annotation in self.video_annotations.annotations:
|
|
464
|
+
# new_annotation = annotation.copy()
|
|
465
|
+
# for i in range(len(new_annotation['metadata']['snapshots_'])):
|
|
466
|
+
# new_annotation['metadata']['snapshots_'][i]['startTime'] -= (self.annotations_offset / self.vid.fps)
|
|
467
|
+
# new_annotation['metadata']['snapshots_'][i]['frameNum'] = int(
|
|
468
|
+
# new_annotation['metadata']['snapshots_'][i]['startTime'] * self.vid.fps)
|
|
469
|
+
# new_annotations.append(json.dumps(new_annotation))
|
|
470
|
+
#
|
|
471
|
+
# # upload annotations
|
|
472
|
+
# self.item.annotation.upload(annotation=[new_annotations])
|
|
473
|
+
|
|
474
|
+
def show_frame(self, frame_number=None):
|
|
475
|
+
"""
|
|
476
|
+
Get a frame from the video source
|
|
477
|
+
:param frame_number:
|
|
478
|
+
:return:
|
|
479
|
+
"""
|
|
480
|
+
try:
|
|
481
|
+
import cv2
|
|
482
|
+
except (ImportError, ModuleNotFoundError):
|
|
483
|
+
logger.error(
|
|
484
|
+
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
485
|
+
raise
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
import tkinter
|
|
489
|
+
except ImportError:
|
|
490
|
+
logger.error(
|
|
491
|
+
'Import Error! Cant import tkinter. Annotations operations will be limited. import manually and fix errors')
|
|
492
|
+
raise
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
import PIL.ImageTk
|
|
496
|
+
import PIL.Image
|
|
497
|
+
except ImportError:
|
|
498
|
+
logger.error(
|
|
499
|
+
'Import Error! Cant import PIL.ImageTk/PIL.ImageTk. Annotations operations will be limited. '
|
|
500
|
+
'import manually and fix errors')
|
|
501
|
+
raise
|
|
502
|
+
ret, frame = self.vid.get_frame(frame_number)
|
|
503
|
+
if ret:
|
|
504
|
+
if self.show_annotations:
|
|
505
|
+
frame = self.get_annotations(frame)
|
|
506
|
+
if self.show_frame_num:
|
|
507
|
+
text = '%d - %d' % (self.vid.frame_number, np.round(self.annotations_timestamp * self.vid.fps))
|
|
508
|
+
frame = cv2.putText(frame,
|
|
509
|
+
text=text,
|
|
510
|
+
org=(100, 100),
|
|
511
|
+
color=(0, 0, 255),
|
|
512
|
+
fontFace=cv2.FONT_HERSHEY_DUPLEX,
|
|
513
|
+
fontScale=2,
|
|
514
|
+
thickness=3)
|
|
515
|
+
|
|
516
|
+
self.photo = PIL.ImageTk.PhotoImage(
|
|
517
|
+
image=PIL.Image.fromarray(frame).resize((self.window_width, self.window_height)),
|
|
518
|
+
master=self.canvas)
|
|
519
|
+
self.canvas.create_image(0, 0, image=self.photo, anchor=tkinter.NW)
|
|
520
|
+
# set timestamp
|
|
521
|
+
self.current_frame_text.configure(text='Frame number:\n%d' % self.vid.frame_number)
|
|
522
|
+
self.current_frame_text.grid(sticky="W", row=12, column=0, columnspan=10)
|
|
523
|
+
millis = int(1000 * self.vid.frame_number / self.vid.fps)
|
|
524
|
+
seconds = (millis / 1000) % 60
|
|
525
|
+
minutes = int((millis / (1000 * 60)) % 60)
|
|
526
|
+
hours = int((millis / (1000 * 60 * 60)) % 24)
|
|
527
|
+
self.frame_timestamp_text.configure(
|
|
528
|
+
text='Frame timestamp:\n{:02d}:{:02d}:{:.3f}'.format(hours, minutes, seconds))
|
|
529
|
+
self.frame_timestamp_text.grid(sticky="W", row=8, column=0, columnspan=10)
|
|
530
|
+
|
|
531
|
+
def update(self):
|
|
532
|
+
"""
|
|
533
|
+
Playing the movie. Get frames at the FPS and show
|
|
534
|
+
:return:
|
|
535
|
+
"""
|
|
536
|
+
tic = time.time()
|
|
537
|
+
if self.playing:
|
|
538
|
+
self.show_frame()
|
|
539
|
+
add_delay = np.maximum(1, self.delay - int(1000 * (time.time() - tic)))
|
|
540
|
+
self.window.after(ms=add_delay, func=self.update)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class VideoCapture:
|
|
544
|
+
"""
|
|
545
|
+
Video class using cv2 to play video
|
|
546
|
+
"""
|
|
547
|
+
def __init__(self, source=0):
|
|
548
|
+
try:
|
|
549
|
+
import cv2
|
|
550
|
+
except (ImportError, ModuleNotFoundError):
|
|
551
|
+
logger.error(
|
|
552
|
+
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
553
|
+
raise
|
|
554
|
+
# Open the video source
|
|
555
|
+
self.vid = cv2.VideoCapture(source)
|
|
556
|
+
if not self.vid.isOpened():
|
|
557
|
+
raise ValueError("Unable to open video source", source)
|
|
558
|
+
|
|
559
|
+
# Get video source width and height
|
|
560
|
+
self.width = self.vid.get(cv2.CAP_PROP_FRAME_WIDTH)
|
|
561
|
+
self.height = self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
|
562
|
+
self.fps = self.vid.get(cv2.CAP_PROP_FPS)
|
|
563
|
+
self.frame_number = self.vid.get(cv2.CAP_PROP_POS_FRAMES)
|
|
564
|
+
|
|
565
|
+
def get_frame(self, frame_number=None):
|
|
566
|
+
"""
|
|
567
|
+
get the frame from video
|
|
568
|
+
:param frame_number:
|
|
569
|
+
:return:
|
|
570
|
+
"""
|
|
571
|
+
try:
|
|
572
|
+
import cv2
|
|
573
|
+
except (ImportError, ModuleNotFoundError):
|
|
574
|
+
logger.error(
|
|
575
|
+
'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
|
|
576
|
+
raise
|
|
577
|
+
if self.vid.isOpened():
|
|
578
|
+
if self.frame_number is None:
|
|
579
|
+
self.frame_number = self.vid.get(cv2.CAP_PROP_POS_FRAMES)
|
|
580
|
+
else:
|
|
581
|
+
self.frame_number += 1
|
|
582
|
+
if frame_number is not None:
|
|
583
|
+
self.frame_number = frame_number
|
|
584
|
+
self.vid.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
|
585
|
+
ret, frame = self.vid.read()
|
|
586
|
+
|
|
587
|
+
if ret:
|
|
588
|
+
# Return a boolean success flag and the current frame converted to BGR
|
|
589
|
+
return ret, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
590
|
+
else:
|
|
591
|
+
return ret, None
|
|
592
|
+
else:
|
|
593
|
+
return False, None
|
|
594
|
+
|
|
595
|
+
# Release the video source when the object is destroyed
|
|
596
|
+
def __del__(self):
|
|
597
|
+
if self.vid.isOpened():
|
|
598
|
+
self.vid.release()
|