boris-behav-obs 9.7.7__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 boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2538 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from collections import deque
|
|
24
|
+
import datetime as dt
|
|
25
|
+
from decimal import Decimal as dec
|
|
26
|
+
import json
|
|
27
|
+
from math import log2, floor
|
|
28
|
+
import os
|
|
29
|
+
import pathlib as pl
|
|
30
|
+
import socket
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
35
|
+
from typing import List, Tuple, Optional
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
from PySide6.QtWidgets import (
|
|
39
|
+
QMessageBox,
|
|
40
|
+
QFileDialog,
|
|
41
|
+
QDateTimeEdit,
|
|
42
|
+
QComboBox,
|
|
43
|
+
QTableWidgetItem,
|
|
44
|
+
QSlider,
|
|
45
|
+
QMainWindow,
|
|
46
|
+
QDockWidget,
|
|
47
|
+
QWidget,
|
|
48
|
+
)
|
|
49
|
+
from PySide6.QtCore import Qt, QDateTime, QTimer
|
|
50
|
+
from PySide6.QtGui import QFont, QIcon, QTextCursor
|
|
51
|
+
|
|
52
|
+
from PySide6 import QtTest
|
|
53
|
+
|
|
54
|
+
from . import menu_options
|
|
55
|
+
from . import config as cfg
|
|
56
|
+
from . import dialog
|
|
57
|
+
from . import select_observations
|
|
58
|
+
from . import project_functions
|
|
59
|
+
from . import observation
|
|
60
|
+
from . import utilities as util
|
|
61
|
+
from . import plot_data_module
|
|
62
|
+
from . import player_dock_widget
|
|
63
|
+
from . import gui_utilities
|
|
64
|
+
from . import video_operations
|
|
65
|
+
from . import state_events
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def export_observations_list_clicked(self):
|
|
69
|
+
"""
|
|
70
|
+
export the list of observations
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
resultStr, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE)
|
|
74
|
+
if not resultStr or not selected_observations:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
file_formats = [
|
|
78
|
+
cfg.TSV,
|
|
79
|
+
cfg.CSV,
|
|
80
|
+
cfg.ODS,
|
|
81
|
+
cfg.XLSX,
|
|
82
|
+
cfg.XLS,
|
|
83
|
+
cfg.HTML,
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
file_name, filter_ = QFileDialog().getSaveFileName(self, "Export list of selected observations", "", ";;".join(file_formats))
|
|
87
|
+
|
|
88
|
+
if not file_name:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
output_format = cfg.FILE_NAME_SUFFIX[filter_]
|
|
92
|
+
if pl.Path(file_name).suffix != "." + output_format:
|
|
93
|
+
file_name = str(pl.Path(file_name)) + "." + output_format
|
|
94
|
+
# check if file name with extension already exists
|
|
95
|
+
if pl.Path(file_name).is_file():
|
|
96
|
+
if dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if not project_functions.export_observations_list(self.pj, selected_observations, file_name, output_format):
|
|
100
|
+
QMessageBox.warning(self, cfg.programName, "File not created due to an error")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def observations_list(self):
|
|
104
|
+
"""
|
|
105
|
+
show list of all observations of current project
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
logging.debug("observations list")
|
|
109
|
+
|
|
110
|
+
if self.playerType in cfg.VIEWERS:
|
|
111
|
+
close_observation(self)
|
|
112
|
+
|
|
113
|
+
result, selected_obs = select_observations.select_observations2(self, cfg.SINGLE)
|
|
114
|
+
|
|
115
|
+
if not selected_obs:
|
|
116
|
+
# activate main window
|
|
117
|
+
self.activateWindow()
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if self.observationId:
|
|
121
|
+
self.hide_data_files()
|
|
122
|
+
response = dialog.MessageDialog(
|
|
123
|
+
cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
|
|
124
|
+
)
|
|
125
|
+
if response == cfg.NO:
|
|
126
|
+
self.show_data_files()
|
|
127
|
+
# activate main window
|
|
128
|
+
self.activateWindow()
|
|
129
|
+
|
|
130
|
+
return ""
|
|
131
|
+
else:
|
|
132
|
+
close_observation(self)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
QtTest.QTest.qWait(1000)
|
|
136
|
+
|
|
137
|
+
if result == cfg.OPEN:
|
|
138
|
+
load_observation(self, selected_obs[0], cfg.OBS_START)
|
|
139
|
+
|
|
140
|
+
if result == cfg.VIEW:
|
|
141
|
+
load_observation(self, selected_obs[0], cfg.VIEW)
|
|
142
|
+
|
|
143
|
+
if result == cfg.EDIT:
|
|
144
|
+
if self.observationId != selected_obs[0]:
|
|
145
|
+
new_observation(self, mode=cfg.EDIT, obsId=selected_obs[0]) # observation id to edit
|
|
146
|
+
else:
|
|
147
|
+
QMessageBox.warning(
|
|
148
|
+
self,
|
|
149
|
+
cfg.programName,
|
|
150
|
+
(f"The observation <b>{self.observationId}</b> is running!<br>Close it before editing."),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logging.debug("end observations list")
|
|
154
|
+
# activate main window
|
|
155
|
+
self.activateWindow()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def open_observation(self, mode: str) -> str:
|
|
159
|
+
"""
|
|
160
|
+
start or view an observation
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
mode (str): "start" to start observation
|
|
164
|
+
"view" to view observation
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
logging.debug("open observation")
|
|
168
|
+
|
|
169
|
+
# check if current observation must be closed to open a new one
|
|
170
|
+
if self.observationId:
|
|
171
|
+
self.hide_data_files()
|
|
172
|
+
response = dialog.MessageDialog(
|
|
173
|
+
cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
|
|
174
|
+
)
|
|
175
|
+
if response == cfg.NO:
|
|
176
|
+
self.show_data_files()
|
|
177
|
+
return ""
|
|
178
|
+
else:
|
|
179
|
+
close_observation(self)
|
|
180
|
+
selected_observations = []
|
|
181
|
+
if mode == cfg.OBS_START:
|
|
182
|
+
_, selected_observations = select_observations.select_observations2(self, cfg.OPEN)
|
|
183
|
+
if mode == cfg.VIEW:
|
|
184
|
+
_, selected_observations = select_observations.select_observations2(self, cfg.VIEW)
|
|
185
|
+
|
|
186
|
+
if selected_observations:
|
|
187
|
+
return load_observation(self, selected_observations[0], mode)
|
|
188
|
+
else:
|
|
189
|
+
return ""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
|
|
193
|
+
"""
|
|
194
|
+
load observation obs_id
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
obsId (str): observation id
|
|
198
|
+
mode (str): "start" to start observation
|
|
199
|
+
"view" to view observation
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
logging.debug("load observation")
|
|
203
|
+
|
|
204
|
+
if obs_id not in self.pj[cfg.OBSERVATIONS]:
|
|
205
|
+
return "Error: Observation not found"
|
|
206
|
+
|
|
207
|
+
if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] not in (cfg.IMAGES, cfg.LIVE, cfg.MEDIA):
|
|
208
|
+
return f"Error: Observation type {self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]} not found"
|
|
209
|
+
|
|
210
|
+
self.observationId = obs_id
|
|
211
|
+
|
|
212
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
213
|
+
self.image_idx = 0
|
|
214
|
+
self.images_list = []
|
|
215
|
+
|
|
216
|
+
if mode == cfg.OBS_START:
|
|
217
|
+
self.playerType = cfg.IMAGES
|
|
218
|
+
initialize_new_images_observation(self)
|
|
219
|
+
|
|
220
|
+
if mode == cfg.VIEW:
|
|
221
|
+
self.playerType = cfg.VIEWER_IMAGES
|
|
222
|
+
self.dwEvents.setVisible(True)
|
|
223
|
+
|
|
224
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
|
|
225
|
+
if mode == cfg.OBS_START:
|
|
226
|
+
initialize_new_live_observation(self)
|
|
227
|
+
|
|
228
|
+
if mode == cfg.VIEW:
|
|
229
|
+
self.playerType = cfg.VIEWER_LIVE
|
|
230
|
+
self.dwEvents.setVisible(True)
|
|
231
|
+
|
|
232
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
233
|
+
if mode == cfg.OBS_START:
|
|
234
|
+
if not initialize_new_media_observation(self):
|
|
235
|
+
close_observation(self)
|
|
236
|
+
# self.observationId = ""
|
|
237
|
+
# self.twEvents.setRowCount(0)
|
|
238
|
+
# menu_options.update_menu(self)
|
|
239
|
+
return "Error: loading observation problem"
|
|
240
|
+
|
|
241
|
+
if mode == cfg.VIEW:
|
|
242
|
+
self.playerType = cfg.VIEWER_MEDIA
|
|
243
|
+
self.dwEvents.setVisible(True)
|
|
244
|
+
|
|
245
|
+
self.load_tw_events(self.observationId)
|
|
246
|
+
|
|
247
|
+
menu_options.update_menu(self)
|
|
248
|
+
# title of dock widget “ ”
|
|
249
|
+
self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation")
|
|
250
|
+
|
|
251
|
+
logging.debug("end load observation")
|
|
252
|
+
return ""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def edit_observation(self):
|
|
256
|
+
"""
|
|
257
|
+
edit observation
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
# check if current observation must be closed to open a new one
|
|
261
|
+
if self.observationId:
|
|
262
|
+
# hide data plot
|
|
263
|
+
self.hide_data_files()
|
|
264
|
+
if (
|
|
265
|
+
dialog.MessageDialog(cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO))
|
|
266
|
+
== cfg.NO
|
|
267
|
+
):
|
|
268
|
+
# restore plots
|
|
269
|
+
self.show_data_files()
|
|
270
|
+
return
|
|
271
|
+
else:
|
|
272
|
+
close_observation(self)
|
|
273
|
+
|
|
274
|
+
_, selected_observations = select_observations.select_observations2(self, cfg.EDIT, windows_title="Edit observation")
|
|
275
|
+
|
|
276
|
+
if selected_observations:
|
|
277
|
+
new_observation(self, mode=cfg.EDIT, obsId=selected_observations[0])
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def remove_observations(self):
|
|
281
|
+
"""
|
|
282
|
+
remove observations from project file
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
_, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE, windows_title="Remove observations")
|
|
286
|
+
if not selected_observations:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
if len(selected_observations) > 1:
|
|
290
|
+
msg = "all the selected observations"
|
|
291
|
+
else:
|
|
292
|
+
msg = "the selected observation"
|
|
293
|
+
response = dialog.MessageDialog(
|
|
294
|
+
cfg.programName,
|
|
295
|
+
(
|
|
296
|
+
"<b>The removal of observations is irreversible (better make a backup of your project before?)</b>."
|
|
297
|
+
f"<br>Are you sure to remove {msg}?<br><br>"
|
|
298
|
+
f"{'<br>'.join(selected_observations)}"
|
|
299
|
+
),
|
|
300
|
+
(cfg.YES, cfg.CANCEL),
|
|
301
|
+
)
|
|
302
|
+
if response == cfg.YES:
|
|
303
|
+
for obs_id in selected_observations:
|
|
304
|
+
del self.pj[cfg.OBSERVATIONS][obs_id]
|
|
305
|
+
self.project_changed()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def coding_time(observations: dict, observations_list: list) -> Tuple[Optional[dec], Optional[dec], Optional[dec]]:
|
|
309
|
+
"""
|
|
310
|
+
returns first even timestamp, last event timestamp and duration of observation
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
observations (dict): observations of project
|
|
314
|
+
observations_list (list): list of selected observations
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
decimal.Decimal: time of first coded event, None if no event, dec(NaN) if no timestamp
|
|
318
|
+
decimal.Decimal: time of last coded event, None if no event, dec(NaN) if no timestamp
|
|
319
|
+
decimal.Decimal: duration of coding, None if no event, dec(NaN) if no timestamp
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
start_coding_list = []
|
|
323
|
+
end_coding_list = []
|
|
324
|
+
for obs_id in observations_list:
|
|
325
|
+
observation = observations[obs_id]
|
|
326
|
+
if observation[cfg.EVENTS]:
|
|
327
|
+
# check if events contain a NA timestamp
|
|
328
|
+
if [event[cfg.EVENT_TIME_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_TIME_FIELD_IDX].is_nan()]:
|
|
329
|
+
return dec("NaN"), dec("NaN"), dec("NaN")
|
|
330
|
+
start_coding_list.append(observation[cfg.EVENTS][0][cfg.EVENT_TIME_FIELD_IDX])
|
|
331
|
+
end_coding_list.append(observation[cfg.EVENTS][-1][cfg.EVENT_TIME_FIELD_IDX])
|
|
332
|
+
|
|
333
|
+
if not start_coding_list:
|
|
334
|
+
start_coding = None
|
|
335
|
+
else:
|
|
336
|
+
if start_coding_list == [x for x in start_coding_list if not x.is_nan()]:
|
|
337
|
+
start_coding = min([x for x in start_coding_list if not x.is_nan()])
|
|
338
|
+
else:
|
|
339
|
+
start_coding = dec("NaN")
|
|
340
|
+
|
|
341
|
+
if not end_coding_list:
|
|
342
|
+
end_coding = None
|
|
343
|
+
else:
|
|
344
|
+
if end_coding_list == [x for x in end_coding_list if not x.is_nan()]:
|
|
345
|
+
end_coding = min([x for x in end_coding_list if not x.is_nan()])
|
|
346
|
+
else:
|
|
347
|
+
end_coding = dec("NaN")
|
|
348
|
+
|
|
349
|
+
if any((start_coding is None, end_coding is None)):
|
|
350
|
+
coding_duration = None
|
|
351
|
+
elif any((start_coding.is_nan(), end_coding.is_nan())):
|
|
352
|
+
coding_duration = dec("NaN")
|
|
353
|
+
else:
|
|
354
|
+
coding_duration = end_coding - start_coding
|
|
355
|
+
|
|
356
|
+
return start_coding, end_coding, coding_duration
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def time_intervals_range(observations: dict, observations_list: list) -> Tuple[Optional[dec], Optional[dec]]:
|
|
360
|
+
"""
|
|
361
|
+
returns earliest start interval and latest end interval
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
observations (dict): observations of project
|
|
365
|
+
observations_list (list): list of selected observations
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
decimal.Decimal: time of earliest start interval
|
|
369
|
+
decimal.Decimal: time of latest end interval
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
start_interval_list: list = []
|
|
373
|
+
end_interval_list: list = []
|
|
374
|
+
for obs_id in observations_list:
|
|
375
|
+
observation = observations[obs_id]
|
|
376
|
+
offset = observation[cfg.TIME_OFFSET]
|
|
377
|
+
# check if observation interval is defined
|
|
378
|
+
if (
|
|
379
|
+
not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0]
|
|
380
|
+
and not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1]
|
|
381
|
+
):
|
|
382
|
+
return None, None
|
|
383
|
+
|
|
384
|
+
start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
|
|
385
|
+
end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
|
|
386
|
+
|
|
387
|
+
if not start_interval_list:
|
|
388
|
+
earliest_start_interval = None
|
|
389
|
+
else:
|
|
390
|
+
earliest_start_interval = min([x for x in start_interval_list])
|
|
391
|
+
|
|
392
|
+
if not end_interval_list:
|
|
393
|
+
latest_end_interval = None
|
|
394
|
+
else:
|
|
395
|
+
latest_end_interval = max([x for x in end_interval_list])
|
|
396
|
+
|
|
397
|
+
return earliest_start_interval, latest_end_interval
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def observation_total_length(observation: dict) -> dec:
|
|
401
|
+
"""
|
|
402
|
+
Observation media duration (if any)
|
|
403
|
+
|
|
404
|
+
media observation: if media duration is not available returns 0
|
|
405
|
+
if more media are queued, returns sum of media duration
|
|
406
|
+
if the last event is recorded after the length of media returns the last event time
|
|
407
|
+
|
|
408
|
+
live observation: returns last event time
|
|
409
|
+
|
|
410
|
+
observation from pictures: returns last event
|
|
411
|
+
if no events returns dec(0)
|
|
412
|
+
if no time returns dec(-2)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
observation (dict): observation dictionary
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
decimal.Decimal: total length in seconds (-2 if observation from pictures)
|
|
420
|
+
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
if observation[cfg.TYPE] == cfg.IMAGES:
|
|
424
|
+
if observation[cfg.EVENTS]:
|
|
425
|
+
try:
|
|
426
|
+
first_event = obs_length = min(observation[cfg.EVENTS])[cfg.TW_OBS_FIELD[cfg.IMAGES]["time"]]
|
|
427
|
+
last_event = obs_length = max(observation[cfg.EVENTS])[cfg.TW_OBS_FIELD[cfg.IMAGES]["time"]]
|
|
428
|
+
obs_length = last_event - first_event
|
|
429
|
+
except Exception:
|
|
430
|
+
logging.critical("Length of observation from images not available")
|
|
431
|
+
obs_length = dec(-2)
|
|
432
|
+
else:
|
|
433
|
+
obs_length = dec(0)
|
|
434
|
+
return obs_length
|
|
435
|
+
|
|
436
|
+
if observation[cfg.TYPE] == cfg.LIVE:
|
|
437
|
+
if observation[cfg.EVENTS]:
|
|
438
|
+
obs_length = max(observation[cfg.EVENTS])[cfg.EVENT_TIME_FIELD_IDX]
|
|
439
|
+
else:
|
|
440
|
+
obs_length = dec(0)
|
|
441
|
+
return obs_length
|
|
442
|
+
|
|
443
|
+
if observation[cfg.TYPE] == cfg.MEDIA:
|
|
444
|
+
media_max_total_length = dec(0)
|
|
445
|
+
|
|
446
|
+
media_total_length = {}
|
|
447
|
+
|
|
448
|
+
for nplayer in observation[cfg.FILE]:
|
|
449
|
+
if not observation[cfg.FILE][nplayer]:
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
media_total_length[nplayer] = dec(0)
|
|
453
|
+
for mediaFile in observation[cfg.FILE][nplayer]:
|
|
454
|
+
mediaLength = 0
|
|
455
|
+
try:
|
|
456
|
+
mediaLength = observation[cfg.MEDIA_INFO][cfg.LENGTH][mediaFile]
|
|
457
|
+
media_total_length[nplayer] += dec(mediaLength)
|
|
458
|
+
except Exception:
|
|
459
|
+
logging.critical(f"media length not found for {mediaFile}")
|
|
460
|
+
mediaLength = -1
|
|
461
|
+
media_total_length[nplayer] = -1
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
if -1 in [media_total_length[x] for x in media_total_length]:
|
|
465
|
+
return dec(-1)
|
|
466
|
+
|
|
467
|
+
# totalMediaLength = max([total_media_length[x] for x in total_media_length])
|
|
468
|
+
|
|
469
|
+
media_max_total_length = max([media_total_length[x] for x in media_total_length])
|
|
470
|
+
|
|
471
|
+
if observation[cfg.EVENTS]:
|
|
472
|
+
if max(observation[cfg.EVENTS])[cfg.EVENT_TIME_FIELD_IDX] > media_max_total_length:
|
|
473
|
+
media_max_total_length = max(observation[cfg.EVENTS])[cfg.EVENT_TIME_FIELD_IDX]
|
|
474
|
+
|
|
475
|
+
return media_max_total_length
|
|
476
|
+
|
|
477
|
+
logging.critical("observation not LIVE nor MEDIA")
|
|
478
|
+
|
|
479
|
+
return dec(0)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def media_duration(observations: dict, selected_observations: list) -> Tuple[Optional[dec], Optional[dec]]:
|
|
483
|
+
"""
|
|
484
|
+
maximum media duration and total media duration of selected observations
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
observations (dict): observations dict
|
|
488
|
+
selected_observations (list): list of selected observations
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
decimal.Decimal: maximum media duration for all observations, None if observation not from media
|
|
492
|
+
decimal.Decimal: total media duration for all observations, None if observation not from media
|
|
493
|
+
"""
|
|
494
|
+
max_media_duration_all_obs = dec("0.0")
|
|
495
|
+
total_media_duration_all_obs = dec("0.0")
|
|
496
|
+
for obs_id in selected_observations:
|
|
497
|
+
if observations[obs_id][cfg.TYPE] != cfg.MEDIA:
|
|
498
|
+
return None, None
|
|
499
|
+
total_media_duration = dec(0)
|
|
500
|
+
|
|
501
|
+
nplayer = "1" # check only player 1 as it must contain the longest media file
|
|
502
|
+
for media_file in observations[obs_id][cfg.FILE][nplayer]:
|
|
503
|
+
try:
|
|
504
|
+
media_duration = observations[obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
|
|
505
|
+
total_media_duration += dec(media_duration)
|
|
506
|
+
except Exception:
|
|
507
|
+
logging.critical(f"media length not found for {media_file}")
|
|
508
|
+
return None, None
|
|
509
|
+
total_media_duration_all_obs += total_media_duration
|
|
510
|
+
max_media_duration_all_obs = max(max_media_duration_all_obs, total_media_duration)
|
|
511
|
+
|
|
512
|
+
return max_media_duration_all_obs, total_media_duration_all_obs
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def observation_length(pj: dict, selected_observations: list) -> tuple:
|
|
516
|
+
"""
|
|
517
|
+
max length of selected observations
|
|
518
|
+
total media length
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
selected_observations (list): list of selected observations
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
decimal.Decimal: maximum media length for all observations
|
|
525
|
+
decimal.Decimal: total media length for all observations
|
|
526
|
+
"""
|
|
527
|
+
selectedObsTotalMediaLength = dec("0.0")
|
|
528
|
+
max_obs_length = dec(0)
|
|
529
|
+
for obs_id in selected_observations:
|
|
530
|
+
obs_length = observation_total_length(pj[cfg.OBSERVATIONS][obs_id])
|
|
531
|
+
if obs_length == dec(-2): # IMAGES OBS with time not available
|
|
532
|
+
selectedObsTotalMediaLength = dec(-2)
|
|
533
|
+
break
|
|
534
|
+
if obs_length in [dec(0), dec(-1)]:
|
|
535
|
+
selectedObsTotalMediaLength = dec(-1)
|
|
536
|
+
break
|
|
537
|
+
max_obs_length = max(max_obs_length, obs_length)
|
|
538
|
+
selectedObsTotalMediaLength += obs_length
|
|
539
|
+
|
|
540
|
+
# an observation media length is not available
|
|
541
|
+
if selectedObsTotalMediaLength == -1:
|
|
542
|
+
# propose to user to use max event time
|
|
543
|
+
if (
|
|
544
|
+
dialog.MessageDialog(
|
|
545
|
+
cfg.programName,
|
|
546
|
+
(f"The observation length is not available (<b>{obs_id}</b>).<br>Use last event time as observation length?"),
|
|
547
|
+
(cfg.YES, cfg.NO),
|
|
548
|
+
)
|
|
549
|
+
== cfg.YES
|
|
550
|
+
):
|
|
551
|
+
try:
|
|
552
|
+
maxTime = dec(0) # max length for all events all subjects
|
|
553
|
+
max_length = dec(0)
|
|
554
|
+
for obs_id in selected_observations:
|
|
555
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
|
|
556
|
+
maxTime += max(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS])[0]
|
|
557
|
+
max_length = max(max_length, max(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS])[0])
|
|
558
|
+
|
|
559
|
+
logging.debug(f"max time all events all subjects: {maxTime}")
|
|
560
|
+
|
|
561
|
+
max_obs_length = max_length
|
|
562
|
+
selectedObsTotalMediaLength = maxTime
|
|
563
|
+
except Exception:
|
|
564
|
+
max_obs_length = dec(-1)
|
|
565
|
+
selectedObsTotalMediaLength = dec(-1)
|
|
566
|
+
|
|
567
|
+
else:
|
|
568
|
+
max_obs_length = dec(-1)
|
|
569
|
+
selectedObsTotalMediaLength = dec(-1)
|
|
570
|
+
|
|
571
|
+
if selectedObsTotalMediaLength == dec(-2): # IMAGES OBS with time not available
|
|
572
|
+
max_obs_length = dec("NaN")
|
|
573
|
+
selectedObsTotalMediaLength = dec("NaN")
|
|
574
|
+
|
|
575
|
+
return (max_obs_length, selectedObsTotalMediaLength)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
|
|
579
|
+
"""
|
|
580
|
+
define a new observation or edit an existing observation
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
mode (str): NEW or EDIT
|
|
584
|
+
obsId (str): observation Id to be edited
|
|
585
|
+
|
|
586
|
+
Retruns:
|
|
587
|
+
None
|
|
588
|
+
|
|
589
|
+
"""
|
|
590
|
+
# check if current observation must be closed to create a new one
|
|
591
|
+
if mode == cfg.NEW and self.observationId:
|
|
592
|
+
# hide data plot
|
|
593
|
+
self.hide_data_files()
|
|
594
|
+
if (
|
|
595
|
+
dialog.MessageDialog(cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO))
|
|
596
|
+
== cfg.NO
|
|
597
|
+
):
|
|
598
|
+
# show data plot
|
|
599
|
+
self.show_data_files()
|
|
600
|
+
return
|
|
601
|
+
else:
|
|
602
|
+
close_observation(self)
|
|
603
|
+
|
|
604
|
+
observationWindow = observation.Observation(
|
|
605
|
+
tmp_dir=self.ffmpeg_cache_dir if (self.ffmpeg_cache_dir and pl.Path(self.ffmpeg_cache_dir).is_dir()) else tempfile.gettempdir(),
|
|
606
|
+
project_path=self.projectFileName,
|
|
607
|
+
converters=self.pj.get(cfg.CONVERTERS, {}),
|
|
608
|
+
time_format=self.timeFormat,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
observationWindow.pj = dict(self.pj)
|
|
612
|
+
observationWindow.sw_observation_type.setCurrentIndex(0) # no observation type
|
|
613
|
+
observationWindow.mode = mode
|
|
614
|
+
observationWindow.mem_obs_id = obsId
|
|
615
|
+
observationWindow.chunk_length = self.chunk_length
|
|
616
|
+
observationWindow.dteDate.setDateTime(QDateTime.currentDateTime())
|
|
617
|
+
# observationWindow.de_date_offset.setDateTime(QDateTime.currentDateTime())
|
|
618
|
+
observationWindow.ffmpeg_bin = self.ffmpeg_bin
|
|
619
|
+
observationWindow.project_file_name = self.projectFileName
|
|
620
|
+
observationWindow.rb_no_time.setChecked(True)
|
|
621
|
+
|
|
622
|
+
# add independent variables
|
|
623
|
+
if cfg.INDEPENDENT_VARIABLES in self.pj:
|
|
624
|
+
observationWindow.twIndepVariables.setRowCount(0)
|
|
625
|
+
for i in util.sorted_keys(self.pj[cfg.INDEPENDENT_VARIABLES]):
|
|
626
|
+
observationWindow.twIndepVariables.setRowCount(observationWindow.twIndepVariables.rowCount() + 1)
|
|
627
|
+
|
|
628
|
+
# label
|
|
629
|
+
item = QTableWidgetItem()
|
|
630
|
+
indepVarLabel = self.pj[cfg.INDEPENDENT_VARIABLES][i]["label"]
|
|
631
|
+
item.setText(indepVarLabel)
|
|
632
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
633
|
+
observationWindow.twIndepVariables.setItem(observationWindow.twIndepVariables.rowCount() - 1, 0, item)
|
|
634
|
+
|
|
635
|
+
# var type
|
|
636
|
+
item = QTableWidgetItem()
|
|
637
|
+
item.setText(self.pj[cfg.INDEPENDENT_VARIABLES][i]["type"])
|
|
638
|
+
item.setFlags(Qt.ItemIsEnabled) # not modifiable
|
|
639
|
+
observationWindow.twIndepVariables.setItem(observationWindow.twIndepVariables.rowCount() - 1, 1, item)
|
|
640
|
+
|
|
641
|
+
# var value
|
|
642
|
+
item = QTableWidgetItem()
|
|
643
|
+
# check if obs has independent variables and var label is a key
|
|
644
|
+
if (
|
|
645
|
+
mode == cfg.EDIT
|
|
646
|
+
and cfg.INDEPENDENT_VARIABLES in self.pj[cfg.OBSERVATIONS][obsId]
|
|
647
|
+
and indepVarLabel in self.pj[cfg.OBSERVATIONS][obsId][cfg.INDEPENDENT_VARIABLES]
|
|
648
|
+
):
|
|
649
|
+
txt = self.pj[cfg.OBSERVATIONS][obsId][cfg.INDEPENDENT_VARIABLES][indepVarLabel]
|
|
650
|
+
|
|
651
|
+
elif mode == cfg.NEW:
|
|
652
|
+
txt = self.pj[cfg.INDEPENDENT_VARIABLES][i]["default value"]
|
|
653
|
+
else:
|
|
654
|
+
txt = ""
|
|
655
|
+
|
|
656
|
+
if self.pj[cfg.INDEPENDENT_VARIABLES][i]["type"] == cfg.SET_OF_VALUES:
|
|
657
|
+
comboBox = QComboBox()
|
|
658
|
+
comboBox.addItems(self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(","))
|
|
659
|
+
if txt in self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(","):
|
|
660
|
+
comboBox.setCurrentIndex(self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(",").index(txt))
|
|
661
|
+
observationWindow.twIndepVariables.setCellWidget(observationWindow.twIndepVariables.rowCount() - 1, 2, comboBox)
|
|
662
|
+
|
|
663
|
+
elif self.pj[cfg.INDEPENDENT_VARIABLES][i]["type"] == cfg.TIMESTAMP:
|
|
664
|
+
cal = QDateTimeEdit()
|
|
665
|
+
cal.setDisplayFormat("yyyy-MM-dd hh:mm:ss.zzz")
|
|
666
|
+
cal.setCalendarPopup(True)
|
|
667
|
+
if len(txt) == len("yyyy-MM-ddThh:mm:ss"):
|
|
668
|
+
txt += ".000"
|
|
669
|
+
cal.setDateTime(QDateTime.fromString(txt, "yyyy-MM-ddThh:mm:ss.zzz"))
|
|
670
|
+
|
|
671
|
+
observationWindow.twIndepVariables.setCellWidget(observationWindow.twIndepVariables.rowCount() - 1, 2, cal)
|
|
672
|
+
else:
|
|
673
|
+
item.setText(txt)
|
|
674
|
+
observationWindow.twIndepVariables.setItem(observationWindow.twIndepVariables.rowCount() - 1, 2, item)
|
|
675
|
+
|
|
676
|
+
observationWindow.twIndepVariables.resizeColumnsToContents()
|
|
677
|
+
|
|
678
|
+
# adapt time offset for current time format
|
|
679
|
+
if self.timeFormat == cfg.S:
|
|
680
|
+
observationWindow.obs_time_offset.rb_seconds.setChecked(True)
|
|
681
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
682
|
+
# observationWindow.obs_time_offset.set_format_hhmmss()
|
|
683
|
+
observationWindow.obs_time_offset.rb_time.setChecked(True)
|
|
684
|
+
|
|
685
|
+
observationWindow.obs_time_offset.set_time(0)
|
|
686
|
+
|
|
687
|
+
if mode == cfg.EDIT:
|
|
688
|
+
observationWindow.setWindowTitle(f'Edit observation "{obsId}"')
|
|
689
|
+
"""mem_obs_id = obsId"""
|
|
690
|
+
observationWindow.leObservationId.setText(obsId)
|
|
691
|
+
|
|
692
|
+
# check date format for old versions of BORIS app
|
|
693
|
+
try:
|
|
694
|
+
time.strptime(self.pj[cfg.OBSERVATIONS][obsId]["date"], "%Y-%m-%d %H:%M")
|
|
695
|
+
self.pj[cfg.OBSERVATIONS][obsId]["date"] = self.pj[cfg.OBSERVATIONS][obsId]["date"].replace(" ", "T") + ":00.000"
|
|
696
|
+
logging.info("Old observation date/time format was converted")
|
|
697
|
+
except ValueError:
|
|
698
|
+
pass
|
|
699
|
+
|
|
700
|
+
# print(f"{self.pj[cfg.OBSERVATIONS][obsId]['date']=}")
|
|
701
|
+
|
|
702
|
+
# test new date (with msec)
|
|
703
|
+
if len(self.pj[cfg.OBSERVATIONS][obsId]["date"]) == len("yyyy-MM-ddThh:mm:ss.zzz"):
|
|
704
|
+
observationWindow.dteDate.setDateTime(QDateTime.fromString(self.pj[cfg.OBSERVATIONS][obsId]["date"], "yyyy-MM-ddThh:mm:ss.zzz"))
|
|
705
|
+
elif len(self.pj[cfg.OBSERVATIONS][obsId]["date"]) == len("yyyy-MM-ddThh:mm:ss"):
|
|
706
|
+
observationWindow.dteDate.setDateTime(QDateTime.fromString(self.pj[cfg.OBSERVATIONS][obsId]["date"], "yyyy-MM-ddThh:mm:ss"))
|
|
707
|
+
|
|
708
|
+
observationWindow.teDescription.setPlainText(self.pj[cfg.OBSERVATIONS][obsId][cfg.DESCRIPTION])
|
|
709
|
+
|
|
710
|
+
try:
|
|
711
|
+
observationWindow.mediaDurations = self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.LENGTH]
|
|
712
|
+
observationWindow.mediaFPS = self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.FPS]
|
|
713
|
+
except Exception:
|
|
714
|
+
observationWindow.mediaDurations = {}
|
|
715
|
+
observationWindow.mediaFPS = {}
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
if cfg.HAS_VIDEO in self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]:
|
|
719
|
+
observationWindow.mediaHasVideo = self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_VIDEO]
|
|
720
|
+
if cfg.HAS_AUDIO in self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]:
|
|
721
|
+
observationWindow.mediaHasAudio = self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_AUDIO]
|
|
722
|
+
except Exception:
|
|
723
|
+
logging.info("No Video/Audio information")
|
|
724
|
+
|
|
725
|
+
# offset
|
|
726
|
+
if self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET] > cfg.DATE_CUTOFF:
|
|
727
|
+
observationWindow.obs_time_offset.rb_datetime.setChecked(True)
|
|
728
|
+
|
|
729
|
+
# time offset
|
|
730
|
+
if self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET]:
|
|
731
|
+
observationWindow.cb_time_offset.setChecked(True)
|
|
732
|
+
observationWindow.obs_time_offset.set_time(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
|
|
733
|
+
|
|
734
|
+
if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.MEDIA:
|
|
735
|
+
observationWindow.rb_media_files.setChecked(True)
|
|
736
|
+
|
|
737
|
+
observationWindow.twVideo1.setRowCount(0)
|
|
738
|
+
for player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE]:
|
|
739
|
+
if player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE] and self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]:
|
|
740
|
+
for mediaFile in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]:
|
|
741
|
+
observationWindow.twVideo1.setRowCount(observationWindow.twVideo1.rowCount() + 1)
|
|
742
|
+
|
|
743
|
+
combobox = QComboBox()
|
|
744
|
+
combobox.addItems(cfg.ALL_PLAYERS)
|
|
745
|
+
combobox.setCurrentIndex(int(player) - 1)
|
|
746
|
+
observationWindow.twVideo1.setCellWidget(observationWindow.twVideo1.rowCount() - 1, 0, combobox)
|
|
747
|
+
|
|
748
|
+
# set media file offset
|
|
749
|
+
try:
|
|
750
|
+
observationWindow.twVideo1.setItem(
|
|
751
|
+
observationWindow.twVideo1.rowCount() - 1,
|
|
752
|
+
1,
|
|
753
|
+
QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]["offset"][player])),
|
|
754
|
+
)
|
|
755
|
+
except Exception:
|
|
756
|
+
observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 1, QTableWidgetItem("0.0"))
|
|
757
|
+
|
|
758
|
+
item = QTableWidgetItem(mediaFile)
|
|
759
|
+
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
760
|
+
observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 2, item)
|
|
761
|
+
|
|
762
|
+
# duration and FPS
|
|
763
|
+
try:
|
|
764
|
+
item = QTableWidgetItem(
|
|
765
|
+
util.seconds2time(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile])
|
|
766
|
+
)
|
|
767
|
+
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
768
|
+
observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 3, item)
|
|
769
|
+
|
|
770
|
+
item = QTableWidgetItem(f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]:.2f}")
|
|
771
|
+
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
772
|
+
observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 4, item)
|
|
773
|
+
except Exception:
|
|
774
|
+
pass
|
|
775
|
+
|
|
776
|
+
# has_video has_audio
|
|
777
|
+
try:
|
|
778
|
+
item = QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_VIDEO][mediaFile]))
|
|
779
|
+
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
780
|
+
observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 5, item)
|
|
781
|
+
|
|
782
|
+
item = QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_AUDIO][mediaFile]))
|
|
783
|
+
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
784
|
+
observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 6, item)
|
|
785
|
+
except Exception:
|
|
786
|
+
pass
|
|
787
|
+
|
|
788
|
+
observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(observationWindow.twVideo1.rowCount() > 0)
|
|
789
|
+
# spectrogram
|
|
790
|
+
observationWindow.cbVisualizeSpectrogram.setEnabled(True)
|
|
791
|
+
observationWindow.cbVisualizeSpectrogram.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_SPECTROGRAM, False))
|
|
792
|
+
# waveform
|
|
793
|
+
observationWindow.cb_visualize_waveform.setEnabled(True)
|
|
794
|
+
observationWindow.cb_visualize_waveform.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_WAVEFORM, False))
|
|
795
|
+
# use Creation date metadata tag as offset
|
|
796
|
+
observationWindow.cb_media_creation_date_as_offset.setEnabled(True)
|
|
797
|
+
|
|
798
|
+
# DEVELOPMENT (REMOVE BEFORE RELEASE)
|
|
799
|
+
# observationWindow.cb_media_creation_date_as_offset.setEnabled(False)
|
|
800
|
+
|
|
801
|
+
observationWindow.cb_media_creation_date_as_offset.setChecked(
|
|
802
|
+
self.pj[cfg.OBSERVATIONS][obsId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False)
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# scan sampling
|
|
806
|
+
observationWindow.sb_media_scan_sampling.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.MEDIA_SCAN_SAMPLING_DURATION, 0))
|
|
807
|
+
# image display duration
|
|
808
|
+
observationWindow.sb_image_display_duration.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.IMAGE_DISPLAY_DURATION, 1))
|
|
809
|
+
|
|
810
|
+
# plot data
|
|
811
|
+
if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][obsId]:
|
|
812
|
+
if self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA]:
|
|
813
|
+
observationWindow.tw_data_files.setRowCount(0)
|
|
814
|
+
for idx2 in util.sorted_keys(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA]):
|
|
815
|
+
observationWindow.tw_data_files.setRowCount(observationWindow.tw_data_files.rowCount() + 1)
|
|
816
|
+
for idx3 in cfg.DATA_PLOT_FIELDS:
|
|
817
|
+
if idx3 == cfg.PLOT_DATA_PLOTCOLOR_IDX:
|
|
818
|
+
combobox = QComboBox()
|
|
819
|
+
combobox.addItems(cfg.DATA_PLOT_STYLES)
|
|
820
|
+
combobox.setCurrentIndex(
|
|
821
|
+
cfg.DATA_PLOT_STYLES.index(
|
|
822
|
+
self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]
|
|
823
|
+
)
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
observationWindow.tw_data_files.setCellWidget(
|
|
827
|
+
observationWindow.tw_data_files.rowCount() - 1,
|
|
828
|
+
cfg.PLOT_DATA_PLOTCOLOR_IDX,
|
|
829
|
+
combobox,
|
|
830
|
+
)
|
|
831
|
+
elif idx3 == cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX:
|
|
832
|
+
combobox2 = QComboBox()
|
|
833
|
+
combobox2.addItems(["False", "True"])
|
|
834
|
+
combobox2.setCurrentIndex(
|
|
835
|
+
["False", "True"].index(
|
|
836
|
+
self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]
|
|
837
|
+
)
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
observationWindow.tw_data_files.setCellWidget(
|
|
841
|
+
observationWindow.tw_data_files.rowCount() - 1,
|
|
842
|
+
cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX,
|
|
843
|
+
combobox2,
|
|
844
|
+
)
|
|
845
|
+
elif idx3 == cfg.PLOT_DATA_CONVERTERS_IDX:
|
|
846
|
+
# convert dict to str
|
|
847
|
+
observationWindow.tw_data_files.setItem(
|
|
848
|
+
observationWindow.tw_data_files.rowCount() - 1,
|
|
849
|
+
idx3,
|
|
850
|
+
QTableWidgetItem(
|
|
851
|
+
str(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]])
|
|
852
|
+
),
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
else:
|
|
856
|
+
observationWindow.tw_data_files.setItem(
|
|
857
|
+
observationWindow.tw_data_files.rowCount() - 1,
|
|
858
|
+
idx3,
|
|
859
|
+
QTableWidgetItem(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]),
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.IMAGES:
|
|
863
|
+
observationWindow.rb_images.setChecked(True)
|
|
864
|
+
observationWindow.lw_images_directory.addItems(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DIRECTORIES_LIST, []))
|
|
865
|
+
observationWindow.rb_use_exif.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.USE_EXIF_DATE, False))
|
|
866
|
+
if self.pj[cfg.OBSERVATIONS][obsId].get(cfg.TIME_LAPSE, 0):
|
|
867
|
+
observationWindow.rb_time_lapse.setChecked(True)
|
|
868
|
+
observationWindow.sb_time_lapse.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.TIME_LAPSE, 0))
|
|
869
|
+
|
|
870
|
+
if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.LIVE:
|
|
871
|
+
observationWindow.rb_live.setChecked(True)
|
|
872
|
+
# sampling time
|
|
873
|
+
observationWindow.sbScanSampling.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.SCAN_SAMPLING_TIME, 0))
|
|
874
|
+
# start from current time
|
|
875
|
+
observationWindow.cb_start_from_current_time.setChecked(
|
|
876
|
+
self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_TIME, False)
|
|
877
|
+
or self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False)
|
|
878
|
+
)
|
|
879
|
+
# day/epoch time
|
|
880
|
+
observationWindow.rb_day_time.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_TIME, False))
|
|
881
|
+
observationWindow.rb_epoch_time.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False))
|
|
882
|
+
|
|
883
|
+
# observation time interval
|
|
884
|
+
observationWindow.cb_observation_time_interval.setEnabled(True)
|
|
885
|
+
if self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0]) != [0, 0]:
|
|
886
|
+
observationWindow.cb_observation_time_interval.setChecked(True)
|
|
887
|
+
observationWindow.observation_time_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
|
|
888
|
+
observationWindow.cb_observation_time_interval.setText(
|
|
889
|
+
(
|
|
890
|
+
"Limit observation to a time interval: "
|
|
891
|
+
f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
|
|
892
|
+
f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
|
|
893
|
+
)
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
if cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS in self.pj[cfg.OBSERVATIONS][obsId]:
|
|
897
|
+
observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setChecked(
|
|
898
|
+
self.pj[cfg.OBSERVATIONS][obsId][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS]
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
rv = observationWindow.exec()
|
|
902
|
+
|
|
903
|
+
# save geometry
|
|
904
|
+
gui_utilities.save_geometry(observationWindow, "new observation")
|
|
905
|
+
|
|
906
|
+
if rv:
|
|
907
|
+
self.project_changed()
|
|
908
|
+
|
|
909
|
+
new_obs_id = observationWindow.leObservationId.text().strip()
|
|
910
|
+
|
|
911
|
+
if mode == cfg.NEW:
|
|
912
|
+
self.observationId = new_obs_id
|
|
913
|
+
self.pj[cfg.OBSERVATIONS][self.observationId] = {
|
|
914
|
+
cfg.FILE: [],
|
|
915
|
+
cfg.TYPE: "",
|
|
916
|
+
"date": "",
|
|
917
|
+
cfg.DESCRIPTION: "",
|
|
918
|
+
cfg.TIME_OFFSET: 0,
|
|
919
|
+
cfg.EVENTS: [],
|
|
920
|
+
cfg.OBSERVATION_TIME_INTERVAL: [0, 0],
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
# check if id changed
|
|
924
|
+
if mode == cfg.EDIT and new_obs_id != obsId:
|
|
925
|
+
logging.info(f"observation id {obsId} changed in {new_obs_id}")
|
|
926
|
+
|
|
927
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id] = dict(self.pj[cfg.OBSERVATIONS][obsId])
|
|
928
|
+
del self.pj[cfg.OBSERVATIONS][obsId]
|
|
929
|
+
|
|
930
|
+
# observation date
|
|
931
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id]["date"] = observationWindow.dteDate.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
|
|
932
|
+
# observation description
|
|
933
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DESCRIPTION] = observationWindow.teDescription.toPlainText()
|
|
934
|
+
|
|
935
|
+
# observation type: read project type from radio buttons
|
|
936
|
+
if observationWindow.rb_media_files.isChecked():
|
|
937
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] = cfg.MEDIA
|
|
938
|
+
if observationWindow.rb_live.isChecked():
|
|
939
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] = cfg.LIVE
|
|
940
|
+
if observationWindow.rb_images.isChecked():
|
|
941
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] = cfg.IMAGES
|
|
942
|
+
|
|
943
|
+
# independent variables for observation
|
|
944
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES] = {}
|
|
945
|
+
for r in range(observationWindow.twIndepVariables.rowCount()):
|
|
946
|
+
# set dictionary as label (col 0) => value (col 2)
|
|
947
|
+
if observationWindow.twIndepVariables.item(r, 1).text() == cfg.SET_OF_VALUES:
|
|
948
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][observationWindow.twIndepVariables.item(r, 0).text()] = (
|
|
949
|
+
observationWindow.twIndepVariables.cellWidget(r, 2).currentText()
|
|
950
|
+
)
|
|
951
|
+
elif observationWindow.twIndepVariables.item(r, 1).text() == cfg.TIMESTAMP:
|
|
952
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][observationWindow.twIndepVariables.item(r, 0).text()] = (
|
|
953
|
+
observationWindow.twIndepVariables.cellWidget(r, 2).dateTime().toString(Qt.ISODate)
|
|
954
|
+
)
|
|
955
|
+
else:
|
|
956
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][observationWindow.twIndepVariables.item(r, 0).text()] = (
|
|
957
|
+
observationWindow.twIndepVariables.item(r, 2).text()
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# observation time offset
|
|
961
|
+
if observationWindow.cb_time_offset.isChecked():
|
|
962
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] = observationWindow.obs_time_offset.get_time()
|
|
963
|
+
else:
|
|
964
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] = dec("0.0")
|
|
965
|
+
|
|
966
|
+
# add date (epoch) if date offset checked
|
|
967
|
+
# if observationWindow.cb_date_offset.isChecked():
|
|
968
|
+
# print(f"{observationWindow.de_date_offset.date().toString(Qt.ISODate)=}")
|
|
969
|
+
# date_timestamp = dec(dt.datetime.strptime(observationWindow.de_date_offset.date().toString(Qt.ISODate), "%Y-%m-%d").timestamp())
|
|
970
|
+
# self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] += date_timestamp
|
|
971
|
+
|
|
972
|
+
if observationWindow.cb_observation_time_interval.isChecked():
|
|
973
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.OBSERVATION_TIME_INTERVAL] = observationWindow.observation_time_interval
|
|
974
|
+
|
|
975
|
+
self.display_statusbar_info(new_obs_id)
|
|
976
|
+
|
|
977
|
+
# visualize spectrogram
|
|
978
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.VISUALIZE_SPECTROGRAM] = observationWindow.cbVisualizeSpectrogram.isChecked()
|
|
979
|
+
# visualize waveform
|
|
980
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.VISUALIZE_WAVEFORM] = observationWindow.cb_visualize_waveform.isChecked()
|
|
981
|
+
# use Creation date metadata tag as offset
|
|
982
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_CREATION_DATE_AS_OFFSET] = (
|
|
983
|
+
observationWindow.cb_media_creation_date_as_offset.isChecked()
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
# media scan sampling
|
|
987
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_SCAN_SAMPLING_DURATION] = observationWindow.sb_media_scan_sampling.value()
|
|
988
|
+
# image display duration
|
|
989
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.IMAGE_DISPLAY_DURATION] = observationWindow.sb_image_display_duration.value()
|
|
990
|
+
|
|
991
|
+
# time interval for observation
|
|
992
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.OBSERVATION_TIME_INTERVAL] = observationWindow.observation_time_interval
|
|
993
|
+
|
|
994
|
+
# plot data
|
|
995
|
+
if observationWindow.tw_data_files.rowCount():
|
|
996
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA] = {}
|
|
997
|
+
for row in range(observationWindow.tw_data_files.rowCount()):
|
|
998
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)] = {}
|
|
999
|
+
for idx2 in cfg.DATA_PLOT_FIELDS:
|
|
1000
|
+
if idx2 in [cfg.PLOT_DATA_PLOTCOLOR_IDX, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX]:
|
|
1001
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = (
|
|
1002
|
+
observationWindow.tw_data_files.cellWidget(row, idx2).currentText()
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
elif idx2 == cfg.PLOT_DATA_CONVERTERS_IDX:
|
|
1006
|
+
if observationWindow.tw_data_files.item(row, idx2).text():
|
|
1007
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = eval(
|
|
1008
|
+
observationWindow.tw_data_files.item(row, idx2).text()
|
|
1009
|
+
)
|
|
1010
|
+
else:
|
|
1011
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = {}
|
|
1012
|
+
|
|
1013
|
+
else:
|
|
1014
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = (
|
|
1015
|
+
observationWindow.tw_data_files.item(row, idx2).text()
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
# Close current behaviors between video
|
|
1019
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS] = (
|
|
1020
|
+
observationWindow.cbCloseCurrentBehaviorsBetweenVideo.isChecked()
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.LIVE:
|
|
1024
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SCAN_SAMPLING_TIME] = observationWindow.sbScanSampling.value()
|
|
1025
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.START_FROM_CURRENT_TIME] = (
|
|
1026
|
+
observationWindow.cb_start_from_current_time.isChecked() and observationWindow.rb_day_time.isChecked()
|
|
1027
|
+
)
|
|
1028
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.START_FROM_CURRENT_EPOCH_TIME] = (
|
|
1029
|
+
observationWindow.cb_start_from_current_time.isChecked() and observationWindow.rb_epoch_time.isChecked()
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
# images dir
|
|
1033
|
+
if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
1034
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DIRECTORIES_LIST] = [
|
|
1035
|
+
observationWindow.lw_images_directory.item(i).text() for i in range(observationWindow.lw_images_directory.count())
|
|
1036
|
+
]
|
|
1037
|
+
|
|
1038
|
+
# check if exif data must be used
|
|
1039
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE] = observationWindow.rb_use_exif.isChecked()
|
|
1040
|
+
|
|
1041
|
+
# ask if the value of the exif date time of the first picture must be substracted
|
|
1042
|
+
# TODO: improve this
|
|
1043
|
+
if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE]:
|
|
1044
|
+
response = dialog.MessageDialog(
|
|
1045
|
+
cfg.programName,
|
|
1046
|
+
"You choose to use the EXIF metadata. Do you want to substract the date time value of the first picture?",
|
|
1047
|
+
(cfg.YES, cfg.NO),
|
|
1048
|
+
)
|
|
1049
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SUBSTRACT_FIRST_EXIF_DATE] = response == cfg.YES
|
|
1050
|
+
|
|
1051
|
+
# check if time lapse
|
|
1052
|
+
if observationWindow.rb_time_lapse.isChecked():
|
|
1053
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_LAPSE] = observationWindow.sb_time_lapse.value()
|
|
1054
|
+
else:
|
|
1055
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_LAPSE] = 0
|
|
1056
|
+
|
|
1057
|
+
# media file
|
|
1058
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE] = {}
|
|
1059
|
+
|
|
1060
|
+
# media
|
|
1061
|
+
if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
1062
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO] = {
|
|
1063
|
+
cfg.LENGTH: observationWindow.mediaDurations,
|
|
1064
|
+
cfg.FPS: observationWindow.mediaFPS,
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_CREATION_DATE_AS_OFFSET]:
|
|
1068
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME] = observationWindow.media_creation_time
|
|
1069
|
+
|
|
1070
|
+
try:
|
|
1071
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.HAS_VIDEO] = observationWindow.mediaHasVideo
|
|
1072
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.HAS_AUDIO] = observationWindow.mediaHasAudio
|
|
1073
|
+
except Exception:
|
|
1074
|
+
logging.warning("error with media_info information")
|
|
1075
|
+
|
|
1076
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]["offset"] = {}
|
|
1077
|
+
|
|
1078
|
+
logging.debug(f"media_info: {self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]}")
|
|
1079
|
+
|
|
1080
|
+
for i in range(cfg.N_PLAYER):
|
|
1081
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][str(i + 1)] = []
|
|
1082
|
+
|
|
1083
|
+
for row in range(observationWindow.twVideo1.rowCount()):
|
|
1084
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][observationWindow.twVideo1.cellWidget(row, 0).currentText()].append(
|
|
1085
|
+
observationWindow.twVideo1.item(row, 2).text()
|
|
1086
|
+
)
|
|
1087
|
+
# store offset for media player
|
|
1088
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]["offset"][
|
|
1089
|
+
observationWindow.twVideo1.cellWidget(row, 0).currentText()
|
|
1090
|
+
] = float(observationWindow.twVideo1.item(row, 1).text())
|
|
1091
|
+
|
|
1092
|
+
if rv == 1: # save
|
|
1093
|
+
self.observationId = ""
|
|
1094
|
+
menu_options.update_menu(self)
|
|
1095
|
+
|
|
1096
|
+
if rv == 2: # start
|
|
1097
|
+
self.observationId = new_obs_id
|
|
1098
|
+
|
|
1099
|
+
# title of dock widget
|
|
1100
|
+
self.dwEvents.setWindowTitle(f"Events for “{self.observationId}“ observation")
|
|
1101
|
+
|
|
1102
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
|
|
1103
|
+
self.playerType = cfg.LIVE
|
|
1104
|
+
initialize_new_live_observation(self)
|
|
1105
|
+
|
|
1106
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
1107
|
+
self.playerType = cfg.MEDIA
|
|
1108
|
+
if not initialize_new_media_observation(self):
|
|
1109
|
+
close_observation(self)
|
|
1110
|
+
return "Observation not launched"
|
|
1111
|
+
|
|
1112
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
1113
|
+
initialize_new_images_observation(self)
|
|
1114
|
+
|
|
1115
|
+
self.load_tw_events(self.observationId)
|
|
1116
|
+
menu_options.update_menu(self)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def close_observation(self):
|
|
1120
|
+
"""
|
|
1121
|
+
close current observation
|
|
1122
|
+
"""
|
|
1123
|
+
|
|
1124
|
+
logging.info(f"Close observation (player type: {self.playerType})")
|
|
1125
|
+
|
|
1126
|
+
# check observation state events
|
|
1127
|
+
|
|
1128
|
+
flag_ok, msg = project_functions.check_state_events_obs(
|
|
1129
|
+
self.observationId,
|
|
1130
|
+
self.pj[cfg.ETHOGRAM],
|
|
1131
|
+
self.pj[cfg.OBSERVATIONS][self.observationId],
|
|
1132
|
+
time_format=cfg.HHMMSS,
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
if not flag_ok:
|
|
1136
|
+
out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
|
|
1137
|
+
results = dialog.Results_dialog_exit_code()
|
|
1138
|
+
results.setWindowTitle(f"{cfg.programName} - Check selected observations")
|
|
1139
|
+
results.ptText.setReadOnly(True)
|
|
1140
|
+
results.ptText.appendHtml(out)
|
|
1141
|
+
|
|
1142
|
+
results.pb1.setText("Close observation")
|
|
1143
|
+
results.pb2.setText("Return to observation")
|
|
1144
|
+
if self.playerType == cfg.IMAGES:
|
|
1145
|
+
results.pb3.setVisible(False)
|
|
1146
|
+
else:
|
|
1147
|
+
results.pb3.setText("Fix unpaired state events")
|
|
1148
|
+
|
|
1149
|
+
r = results.exec()
|
|
1150
|
+
if r == 2: # Return to observation
|
|
1151
|
+
return
|
|
1152
|
+
if r == 3: # Fix unpaired state events
|
|
1153
|
+
state_events.fix_unpaired_events(self, silent_mode=True)
|
|
1154
|
+
|
|
1155
|
+
self.saved_state = self.saveState()
|
|
1156
|
+
|
|
1157
|
+
if self.playerType == cfg.MEDIA:
|
|
1158
|
+
self.media_scan_sampling_mem = []
|
|
1159
|
+
logging.info("Stop plot timer")
|
|
1160
|
+
self.plot_timer.stop()
|
|
1161
|
+
|
|
1162
|
+
if self.MPV_IPC_MODE:
|
|
1163
|
+
self.main_window_activation_timer.stop()
|
|
1164
|
+
|
|
1165
|
+
for i, player in enumerate(self.dw_player):
|
|
1166
|
+
if (
|
|
1167
|
+
str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
|
|
1168
|
+
and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][str(i + 1)]
|
|
1169
|
+
):
|
|
1170
|
+
logging.info(f"Stop player #{i + 1}")
|
|
1171
|
+
player.player.stop()
|
|
1172
|
+
|
|
1173
|
+
if self.MPV_IPC_MODE:
|
|
1174
|
+
try:
|
|
1175
|
+
player.player.process.terminate()
|
|
1176
|
+
try:
|
|
1177
|
+
player.player.process.wait(timeout=3) # wait up to 3s
|
|
1178
|
+
except subprocess.TimeoutExpired:
|
|
1179
|
+
player.player.process.kill() # force if still alive
|
|
1180
|
+
except Exception as e:
|
|
1181
|
+
logging.warning(f"Error stopping MPV process #{i}: {e}")
|
|
1182
|
+
|
|
1183
|
+
self.verticalLayout_3.removeWidget(self.video_slider)
|
|
1184
|
+
|
|
1185
|
+
if self.video_slider is not None:
|
|
1186
|
+
self.video_slider.setVisible(False)
|
|
1187
|
+
self.video_slider.deleteLater()
|
|
1188
|
+
self.video_slider = None
|
|
1189
|
+
|
|
1190
|
+
if self.playerType == cfg.LIVE:
|
|
1191
|
+
self.liveTimer.stop()
|
|
1192
|
+
self.pb_live_obs.setEnabled(False)
|
|
1193
|
+
self.w_live.setVisible(False)
|
|
1194
|
+
self.liveObservationStarted = False
|
|
1195
|
+
self.liveStartTime = None
|
|
1196
|
+
|
|
1197
|
+
if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId] and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
|
|
1198
|
+
for x in self.ext_data_timer_list:
|
|
1199
|
+
x.stop()
|
|
1200
|
+
for pd in self.plot_data:
|
|
1201
|
+
self.plot_data[pd].close_plot()
|
|
1202
|
+
|
|
1203
|
+
logging.info("close tool window")
|
|
1204
|
+
|
|
1205
|
+
self.close_tool_windows()
|
|
1206
|
+
|
|
1207
|
+
self.observationId = ""
|
|
1208
|
+
|
|
1209
|
+
# delete undo queue
|
|
1210
|
+
self.undo_queue = deque()
|
|
1211
|
+
self.undo_description = deque()
|
|
1212
|
+
|
|
1213
|
+
if self.playerType in (cfg.MEDIA, cfg.IMAGES):
|
|
1214
|
+
"""
|
|
1215
|
+
for idx, _ in enumerate(self.dw_player):
|
|
1216
|
+
#del self.dw_player[idx].stack
|
|
1217
|
+
self.removeDockWidget(self.dw_player[idx])
|
|
1218
|
+
sip.delete(self.dw_player[idx])
|
|
1219
|
+
self.dw_player[idx] = None
|
|
1220
|
+
"""
|
|
1221
|
+
|
|
1222
|
+
for dw in self.dw_player:
|
|
1223
|
+
logging.info("remove dock widget")
|
|
1224
|
+
dw.player.log_handler = None
|
|
1225
|
+
self.removeDockWidget(dw)
|
|
1226
|
+
|
|
1227
|
+
del dw
|
|
1228
|
+
# sip.delete(dw)
|
|
1229
|
+
# dw = None
|
|
1230
|
+
|
|
1231
|
+
# self.dw_player = []
|
|
1232
|
+
|
|
1233
|
+
self.statusbar.showMessage("", 0)
|
|
1234
|
+
|
|
1235
|
+
self.dwEvents.setVisible(False)
|
|
1236
|
+
|
|
1237
|
+
self.w_obs_info.setVisible(False)
|
|
1238
|
+
|
|
1239
|
+
# self.twEvents.setRowCount(0)
|
|
1240
|
+
|
|
1241
|
+
self.lb_current_media_time.clear()
|
|
1242
|
+
self.lb_player_status.clear()
|
|
1243
|
+
self.lb_video_info.clear()
|
|
1244
|
+
self.lb_zoom_level.clear()
|
|
1245
|
+
|
|
1246
|
+
self.currentSubject = ""
|
|
1247
|
+
self.lbFocalSubject.setText(cfg.NO_FOCAL_SUBJECT)
|
|
1248
|
+
|
|
1249
|
+
# clear current state(s) column in subjects table
|
|
1250
|
+
for i in range(self.twSubjects.rowCount()):
|
|
1251
|
+
self.twSubjects.item(i, len(cfg.subjectsFields)).setText("")
|
|
1252
|
+
|
|
1253
|
+
for w in (self.lbTimeOffset, self.lb_obs_time_interval):
|
|
1254
|
+
w.clear()
|
|
1255
|
+
self.play_rate, self.playerType = 1, ""
|
|
1256
|
+
|
|
1257
|
+
menu_options.update_menu(self)
|
|
1258
|
+
|
|
1259
|
+
logging.info(f"Observation {self.playerType} closed")
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def check_creation_date(self) -> Tuple[int, dict]:
|
|
1263
|
+
"""
|
|
1264
|
+
check if media file exists
|
|
1265
|
+
check if Creation Date tag is present in metadata of media file
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
int: 0 if OK else error code: 1 -> media file date not used, 2 -> media file not found
|
|
1269
|
+
|
|
1270
|
+
"""
|
|
1271
|
+
|
|
1272
|
+
not_tagged_media_list: list = []
|
|
1273
|
+
media_creation_time: dict = {}
|
|
1274
|
+
|
|
1275
|
+
for nplayer in cfg.ALL_PLAYERS:
|
|
1276
|
+
if nplayer in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.FILE, {}):
|
|
1277
|
+
for media_file in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][nplayer]:
|
|
1278
|
+
media_path = project_functions.full_path(media_file, self.projectFileName)
|
|
1279
|
+
media_info = util.accurate_media_analysis(self.ffmpeg_bin, media_path)
|
|
1280
|
+
|
|
1281
|
+
if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
|
|
1282
|
+
not_tagged_media_list.append(media_path)
|
|
1283
|
+
else:
|
|
1284
|
+
creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
|
|
1285
|
+
media_creation_time[media_path] = creation_time_epoch
|
|
1286
|
+
|
|
1287
|
+
"""
|
|
1288
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1289
|
+
if self.twVideo1.item(row, 2).text() not in media_not_found_list:
|
|
1290
|
+
media_info = util.accurate_media_analysis(self.ffmpeg_bin, self.twVideo1.item(row, 2).text())
|
|
1291
|
+
if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
|
|
1292
|
+
not_tagged_media_list.append(self.twVideo1.item(row, 2).text())
|
|
1293
|
+
else:
|
|
1294
|
+
creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
|
|
1295
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = creation_time_epoch
|
|
1296
|
+
"""
|
|
1297
|
+
|
|
1298
|
+
if not_tagged_media_list:
|
|
1299
|
+
dlg = dialog.Results_dialog()
|
|
1300
|
+
dlg.setWindowTitle("BORIS")
|
|
1301
|
+
dlg.pbOK.setText("Yes")
|
|
1302
|
+
dlg.pbCancel.setVisible(True)
|
|
1303
|
+
dlg.pbCancel.setText("No")
|
|
1304
|
+
|
|
1305
|
+
dlg.ptText.clear()
|
|
1306
|
+
dlg.ptText.appendHtml(
|
|
1307
|
+
(
|
|
1308
|
+
"Some media file does not contain the <b>Creation date/time</b> metadata tag:<br>"
|
|
1309
|
+
f"{'<br>'.join(not_tagged_media_list)}<br><br>"
|
|
1310
|
+
"Use the media file date/time instead?"
|
|
1311
|
+
)
|
|
1312
|
+
)
|
|
1313
|
+
dlg.ptText.moveCursor(QTextCursor.Start)
|
|
1314
|
+
ret = dlg.exec_()
|
|
1315
|
+
|
|
1316
|
+
if ret == 1: # use file creation time
|
|
1317
|
+
for media in not_tagged_media_list:
|
|
1318
|
+
media_creation_time[media] = pl.Path(media).stat().st_ctime
|
|
1319
|
+
return (0, media_creation_time) # OK use media file creation date/time
|
|
1320
|
+
else:
|
|
1321
|
+
return (1, {})
|
|
1322
|
+
else:
|
|
1323
|
+
return (0, media_creation_time) # OK all media have a 'creation time' tag
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def initialize_new_media_observation(self) -> bool:
|
|
1327
|
+
"""
|
|
1328
|
+
initialize new observation from media file(s)
|
|
1329
|
+
"""
|
|
1330
|
+
|
|
1331
|
+
logging.debug("function: initialize new observation for media file(s)")
|
|
1332
|
+
|
|
1333
|
+
for dw in (self.dwEthogram, self.dwSubjects, self.dwEvents):
|
|
1334
|
+
dw.setVisible(True)
|
|
1335
|
+
|
|
1336
|
+
ok, msg = project_functions.check_if_media_available(self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName)
|
|
1337
|
+
|
|
1338
|
+
if not ok:
|
|
1339
|
+
QMessageBox.critical(
|
|
1340
|
+
self,
|
|
1341
|
+
cfg.programName,
|
|
1342
|
+
(
|
|
1343
|
+
f"{msg}<br><br>The observation will be opened in VIEW mode.<br>"
|
|
1344
|
+
"It will not be possible to log events.<br>"
|
|
1345
|
+
"Modify the media path to point an existing media file "
|
|
1346
|
+
"to log events or copy media file in the BORIS project directory."
|
|
1347
|
+
),
|
|
1348
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1349
|
+
QMessageBox.NoButton,
|
|
1350
|
+
)
|
|
1351
|
+
self.playerType = cfg.VIEWER_MEDIA
|
|
1352
|
+
return True
|
|
1353
|
+
|
|
1354
|
+
self.playerType = cfg.MEDIA
|
|
1355
|
+
self.fps = 0
|
|
1356
|
+
|
|
1357
|
+
self.pb_live_obs.setEnabled(False)
|
|
1358
|
+
self.w_live.setVisible(False)
|
|
1359
|
+
self.w_obs_info.setVisible(True)
|
|
1360
|
+
|
|
1361
|
+
font = QFont()
|
|
1362
|
+
font.setPointSize(15)
|
|
1363
|
+
self.lb_current_media_time.setFont(font)
|
|
1364
|
+
self.lb_video_info.setFont(font)
|
|
1365
|
+
self.lb_zoom_level.setFont(font)
|
|
1366
|
+
|
|
1367
|
+
# initialize video slider
|
|
1368
|
+
self.video_slider = QSlider(Qt.Horizontal, self)
|
|
1369
|
+
self.video_slider.setFocusPolicy(Qt.NoFocus)
|
|
1370
|
+
self.video_slider.setMaximum(cfg.SLIDER_MAXIMUM)
|
|
1371
|
+
self.video_slider.sliderMoved.connect(self.video_slider_sliderMoved)
|
|
1372
|
+
self.video_slider.sliderReleased.connect(self.video_slider_sliderReleased)
|
|
1373
|
+
self.verticalLayout_3.addWidget(self.video_slider)
|
|
1374
|
+
|
|
1375
|
+
# add all media files to media lists
|
|
1376
|
+
self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)
|
|
1377
|
+
self.dw_player = []
|
|
1378
|
+
|
|
1379
|
+
# check if media creation time used as offset
|
|
1380
|
+
# TODO check if cfg.MEDIA_CREATION_TIME dict is present
|
|
1381
|
+
"""
|
|
1382
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
|
|
1383
|
+
r, media_creation_time = check_creation_date(self)
|
|
1384
|
+
|
|
1385
|
+
if r:
|
|
1386
|
+
return False
|
|
1387
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME] = dict(media_creation_time)
|
|
1388
|
+
"""
|
|
1389
|
+
|
|
1390
|
+
# create dock widgets for players
|
|
1391
|
+
for i in range(cfg.N_PLAYER):
|
|
1392
|
+
n_player = str(i + 1)
|
|
1393
|
+
if (
|
|
1394
|
+
n_player not in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
|
|
1395
|
+
or not self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]
|
|
1396
|
+
):
|
|
1397
|
+
continue
|
|
1398
|
+
|
|
1399
|
+
# Not pretty but the unique solution I have found to capture the click signal for each player
|
|
1400
|
+
|
|
1401
|
+
if i == 0: # first player
|
|
1402
|
+
p0 = player_dock_widget.DW_player(0, self)
|
|
1403
|
+
|
|
1404
|
+
if not self.MPV_IPC_MODE:
|
|
1405
|
+
|
|
1406
|
+
@p0.player.property_observer("time-pos")
|
|
1407
|
+
def time_observer(_name, value):
|
|
1408
|
+
if value is not None:
|
|
1409
|
+
self.time_observer_signal.emit(value)
|
|
1410
|
+
|
|
1411
|
+
@p0.player.property_observer("eof-reached")
|
|
1412
|
+
def eof_reached(_name, value):
|
|
1413
|
+
if value is not None:
|
|
1414
|
+
self.mpv_eof_reached_signal.emit(value)
|
|
1415
|
+
|
|
1416
|
+
@p0.player.on_key_press("MBTN_LEFT")
|
|
1417
|
+
def mbtn_left0():
|
|
1418
|
+
self.video_click_signal.emit(0, "MBTN_LEFT")
|
|
1419
|
+
|
|
1420
|
+
@p0.player.on_key_press("MBTN_RIGHT")
|
|
1421
|
+
def mbtn_right0():
|
|
1422
|
+
self.video_click_signal.emit(0, "MBTN_RIGHT")
|
|
1423
|
+
|
|
1424
|
+
@p0.player.on_key_press("MBTN_LEFT_DBL")
|
|
1425
|
+
def mbtn_left_dbl0():
|
|
1426
|
+
self.video_click_signal.emit(0, "MBTN_LEFT_DBL")
|
|
1427
|
+
|
|
1428
|
+
@p0.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1429
|
+
def mbtn_right_dbl0():
|
|
1430
|
+
self.video_click_signal.emit(0, "MBTN_RIGHT_DBL")
|
|
1431
|
+
|
|
1432
|
+
@p0.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1433
|
+
def ctrl_wheel_up0():
|
|
1434
|
+
self.video_click_signal.emit(0, "Ctrl+WHEEL_UP")
|
|
1435
|
+
|
|
1436
|
+
@p0.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1437
|
+
def ctrl_wheel_down0():
|
|
1438
|
+
self.video_click_signal.emit(0, "Ctrl+WHEEL_DOWN")
|
|
1439
|
+
|
|
1440
|
+
@p0.player.on_key_press("WHEEL_UP")
|
|
1441
|
+
def wheel_up0():
|
|
1442
|
+
self.video_click_signal.emit(0, "WHEEL_UP")
|
|
1443
|
+
|
|
1444
|
+
@p0.player.on_key_press("WHEEL_DOWN")
|
|
1445
|
+
def wheel_down0():
|
|
1446
|
+
self.video_click_signal.emit(0, "WHEEL_DOWN")
|
|
1447
|
+
|
|
1448
|
+
@p0.player.on_key_press("Shift+WHEEL_UP")
|
|
1449
|
+
def shift_wheel_up0():
|
|
1450
|
+
self.video_click_signal.emit(0, "Shift+WHEEL_UP")
|
|
1451
|
+
|
|
1452
|
+
@p0.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1453
|
+
def shift_wheel_down0():
|
|
1454
|
+
self.video_click_signal.emit(0, "Shift+WHEEL_DOWN")
|
|
1455
|
+
|
|
1456
|
+
@p0.player.on_key_press("Shift+MBTN_LEFT")
|
|
1457
|
+
def shift_mbtn_left0():
|
|
1458
|
+
self.video_click_signal.emit(0, "Shift+MBTN_LEFT")
|
|
1459
|
+
|
|
1460
|
+
self.dw_player.append(p0)
|
|
1461
|
+
|
|
1462
|
+
if i == 1: # second player
|
|
1463
|
+
p1 = player_dock_widget.DW_player(1, self)
|
|
1464
|
+
|
|
1465
|
+
if not self.MPV_IPC_MODE:
|
|
1466
|
+
|
|
1467
|
+
@p1.player.on_key_press("MBTN_LEFT")
|
|
1468
|
+
def mbtn_left1():
|
|
1469
|
+
self.video_click_signal.emit(1, "MBTN_LEFT")
|
|
1470
|
+
|
|
1471
|
+
@p1.player.on_key_press("MBTN_RIGHT")
|
|
1472
|
+
def mbtn_right1():
|
|
1473
|
+
self.video_click_signal.emit(1, "MBTN_RIGHT")
|
|
1474
|
+
|
|
1475
|
+
@p1.player.on_key_press("MBTN_LEFT_DBL")
|
|
1476
|
+
def mbtn_left_dbl1():
|
|
1477
|
+
self.video_click_signal.emit(1, "MBTN_LEFT_DBL")
|
|
1478
|
+
|
|
1479
|
+
@p1.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1480
|
+
def mbtn_right_dbl1():
|
|
1481
|
+
self.video_click_signal.emit(1, "MBTN_RIGHT_DBL")
|
|
1482
|
+
|
|
1483
|
+
@p1.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1484
|
+
def ctrl_wheel_up1():
|
|
1485
|
+
self.video_click_signal.emit(1, "Ctrl+WHEEL_UP")
|
|
1486
|
+
|
|
1487
|
+
@p1.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1488
|
+
def ctrl_wheel_down1():
|
|
1489
|
+
self.video_click_signal.emit(1, "Ctrl+WHEEL_DOWN")
|
|
1490
|
+
|
|
1491
|
+
@p1.player.on_key_press("WHEEL_UP")
|
|
1492
|
+
def wheel_up1():
|
|
1493
|
+
self.video_click_signal.emit(1, "WHEEL_UP")
|
|
1494
|
+
|
|
1495
|
+
@p1.player.on_key_press("WHEEL_DOWN")
|
|
1496
|
+
def wheel_down1():
|
|
1497
|
+
self.video_click_signal.emit(1, "WHEEL_DOWN")
|
|
1498
|
+
|
|
1499
|
+
@p1.player.on_key_press("Shift+WHEEL_UP")
|
|
1500
|
+
def shift_wheel_up1():
|
|
1501
|
+
self.video_click_signal.emit(1, "Shift+WHEEL_UP")
|
|
1502
|
+
|
|
1503
|
+
@p1.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1504
|
+
def shift_wheel_down1():
|
|
1505
|
+
self.video_click_signal.emit(1, "Shift+WHEEL_DOWN")
|
|
1506
|
+
|
|
1507
|
+
@p1.player.on_key_press("Shift+MBTN_LEFT")
|
|
1508
|
+
def shift_mbtn_left1():
|
|
1509
|
+
self.video_click_signal.emit(1, "Shift+MBTN_LEFT")
|
|
1510
|
+
|
|
1511
|
+
self.dw_player.append(p1)
|
|
1512
|
+
|
|
1513
|
+
if i == 2:
|
|
1514
|
+
p2 = player_dock_widget.DW_player(2, self)
|
|
1515
|
+
|
|
1516
|
+
if not self.MPV_IPC_MODE:
|
|
1517
|
+
|
|
1518
|
+
@p2.player.on_key_press("MBTN_LEFT")
|
|
1519
|
+
def mbtn_left2():
|
|
1520
|
+
self.video_click_signal.emit(2, "MBTN_LEFT")
|
|
1521
|
+
|
|
1522
|
+
@p2.player.on_key_press("MBTN_RIGHT")
|
|
1523
|
+
def mbtn_right2():
|
|
1524
|
+
self.video_click_signal.emit(2, "MBTN_RIGHT")
|
|
1525
|
+
|
|
1526
|
+
@p2.player.on_key_press("MBTN_LEFT_DBL")
|
|
1527
|
+
def mbtn_left_dbl2():
|
|
1528
|
+
self.video_click_signal.emit(2, "MBTN_LEFT_DBL")
|
|
1529
|
+
|
|
1530
|
+
@p2.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1531
|
+
def mbtn_right_dbl2():
|
|
1532
|
+
self.video_click_signal.emit(2, "MBTN_RIGHT_DBL")
|
|
1533
|
+
|
|
1534
|
+
@p2.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1535
|
+
def ctrl_wheel_up2():
|
|
1536
|
+
self.video_click_signal.emit(2, "Ctrl+WHEEL_UP")
|
|
1537
|
+
|
|
1538
|
+
@p2.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1539
|
+
def ctrl_wheel_down2():
|
|
1540
|
+
self.video_click_signal.emit(2, "Ctrl+WHEEL_DOWN")
|
|
1541
|
+
|
|
1542
|
+
@p2.player.on_key_press("WHEEL_UP")
|
|
1543
|
+
def wheel_up2():
|
|
1544
|
+
self.video_click_signal.emit(2, "WHEEL_UP")
|
|
1545
|
+
|
|
1546
|
+
@p2.player.on_key_press("WHEEL_DOWN")
|
|
1547
|
+
def wheel_down2():
|
|
1548
|
+
self.video_click_signal.emit(2, "WHEEL_DOWN")
|
|
1549
|
+
|
|
1550
|
+
@p2.player.on_key_press("Shift+WHEEL_UP")
|
|
1551
|
+
def shift_wheel_up2():
|
|
1552
|
+
self.video_click_signal.emit(2, "Shift+WHEEL_UP")
|
|
1553
|
+
|
|
1554
|
+
@p2.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1555
|
+
def shift_wheel_down2():
|
|
1556
|
+
self.video_click_signal.emit(2, "Shift+WHEEL_DOWN")
|
|
1557
|
+
|
|
1558
|
+
@p2.player.on_key_press("Shift+MBTN_LEFT")
|
|
1559
|
+
def shift_mbtn_left2():
|
|
1560
|
+
self.video_click_signal.emit(2, "Shift+MBTN_LEFT")
|
|
1561
|
+
|
|
1562
|
+
self.dw_player.append(p2)
|
|
1563
|
+
|
|
1564
|
+
if i == 3:
|
|
1565
|
+
p3 = player_dock_widget.DW_player(3, self)
|
|
1566
|
+
|
|
1567
|
+
if not self.MPV_IPC_MODE:
|
|
1568
|
+
|
|
1569
|
+
@p3.player.on_key_press("MBTN_LEFT")
|
|
1570
|
+
def mbtn_left3():
|
|
1571
|
+
self.video_click_signal.emit(3, "MBTN_LEFT")
|
|
1572
|
+
|
|
1573
|
+
@p3.player.on_key_press("MBTN_RIGHT")
|
|
1574
|
+
def mbtn_right3():
|
|
1575
|
+
self.video_click_signal.emit(3, "MBTN_RIGHT")
|
|
1576
|
+
|
|
1577
|
+
@p3.player.on_key_press("MBTN_LEFT_DBL")
|
|
1578
|
+
def mbtn_left_dbl3():
|
|
1579
|
+
self.video_click_signal.emit(3, "MBTN_LEFT_DBL")
|
|
1580
|
+
|
|
1581
|
+
@p3.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1582
|
+
def mbtn_right_dbl3():
|
|
1583
|
+
self.video_click_signal.emit(3, "MBTN_RIGHT_DBL")
|
|
1584
|
+
|
|
1585
|
+
@p3.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1586
|
+
def ctrl_wheel_up3():
|
|
1587
|
+
self.video_click_signal.emit(3, "Ctrl+WHEEL_UP")
|
|
1588
|
+
|
|
1589
|
+
@p3.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1590
|
+
def ctrl_wheel_down3():
|
|
1591
|
+
self.video_click_signal.emit(3, "Ctrl+WHEEL_DOWN")
|
|
1592
|
+
|
|
1593
|
+
@p3.player.on_key_press("WHEEL_UP")
|
|
1594
|
+
def wheel_up3():
|
|
1595
|
+
self.video_click_signal.emit(3, "WHEEL_UP")
|
|
1596
|
+
|
|
1597
|
+
@p3.player.on_key_press("WHEEL_DOWN")
|
|
1598
|
+
def wheel_down3():
|
|
1599
|
+
self.video_click_signal.emit(3, "WHEEL_DOWN")
|
|
1600
|
+
|
|
1601
|
+
@p3.player.on_key_press("Shift+WHEEL_UP")
|
|
1602
|
+
def shift_wheel_up3():
|
|
1603
|
+
self.video_click_signal.emit(3, "Shift+WHEEL_UP")
|
|
1604
|
+
|
|
1605
|
+
@p3.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1606
|
+
def shift_wheel_down3():
|
|
1607
|
+
self.video_click_signal.emit(3, "Shift+WHEEL_DOWN")
|
|
1608
|
+
|
|
1609
|
+
@p3.player.on_key_press("Shift+MBTN_LEFT")
|
|
1610
|
+
def shift_mbtn_left3():
|
|
1611
|
+
self.video_click_signal.emit(3, "Shift+MBTN_LEFT")
|
|
1612
|
+
|
|
1613
|
+
self.dw_player.append(p3)
|
|
1614
|
+
|
|
1615
|
+
if i == 4:
|
|
1616
|
+
p4 = player_dock_widget.DW_player(4, self)
|
|
1617
|
+
|
|
1618
|
+
if not self.MPV_IPC_MODE:
|
|
1619
|
+
|
|
1620
|
+
@p4.player.on_key_press("MBTN_LEFT")
|
|
1621
|
+
def mbtn_left4():
|
|
1622
|
+
self.video_click_signal.emit(4, "MBTN_LEFT")
|
|
1623
|
+
|
|
1624
|
+
@p4.player.on_key_press("MBTN_RIGHT")
|
|
1625
|
+
def mbtn_right4():
|
|
1626
|
+
self.video_click_signal.emit(4, "MBTN_RIGHT")
|
|
1627
|
+
|
|
1628
|
+
@p4.player.on_key_press("MBTN_LEFT_DBL")
|
|
1629
|
+
def mbtn_left_dbl4():
|
|
1630
|
+
self.video_click_signal.emit(4, "MBTN_LEFT_DBL")
|
|
1631
|
+
|
|
1632
|
+
@p4.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1633
|
+
def mbtn_right_dbl4():
|
|
1634
|
+
self.video_click_signal.emit(4, "MBTN_RIGHT_DBL")
|
|
1635
|
+
|
|
1636
|
+
@p4.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1637
|
+
def ctrl_wheel_up4():
|
|
1638
|
+
self.video_click_signal.emit(4, "Ctrl+WHEEL_UP")
|
|
1639
|
+
|
|
1640
|
+
@p4.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1641
|
+
def ctrl_wheel_down4():
|
|
1642
|
+
self.video_click_signal.emit(4, "Ctrl+WHEEL_DOWN")
|
|
1643
|
+
|
|
1644
|
+
@p4.player.on_key_press("WHEEL_UP")
|
|
1645
|
+
def wheel_up4():
|
|
1646
|
+
self.video_click_signal.emit(4, "WHEEL_UP")
|
|
1647
|
+
|
|
1648
|
+
@p4.player.on_key_press("WHEEL_DOWN")
|
|
1649
|
+
def wheel_down4():
|
|
1650
|
+
self.video_click_signal.emit(4, "WHEEL_DOWN")
|
|
1651
|
+
|
|
1652
|
+
@p4.player.on_key_press("Shift+WHEEL_UP")
|
|
1653
|
+
def shift_wheel_up4():
|
|
1654
|
+
self.video_click_signal.emit(4, "Shift+WHEEL_UP")
|
|
1655
|
+
|
|
1656
|
+
@p4.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1657
|
+
def shift_wheel_down4():
|
|
1658
|
+
self.video_click_signal.emit(4, "Shift+WHEEL_DOWN")
|
|
1659
|
+
|
|
1660
|
+
@p4.player.on_key_press("Shift+MBTN_LEFT")
|
|
1661
|
+
def shift_mbtn_left4():
|
|
1662
|
+
self.video_click_signal.emit(4, "Shift+MBTN_LEFT")
|
|
1663
|
+
|
|
1664
|
+
self.dw_player.append(p4)
|
|
1665
|
+
|
|
1666
|
+
if i == 5:
|
|
1667
|
+
p5 = player_dock_widget.DW_player(5, self)
|
|
1668
|
+
|
|
1669
|
+
if not self.MPV_IPC_MODE:
|
|
1670
|
+
|
|
1671
|
+
@p5.player.on_key_press("MBTN_LEFT")
|
|
1672
|
+
def mbtn_left5():
|
|
1673
|
+
self.video_click_signal.emit(5, "MBTN_LEFT")
|
|
1674
|
+
|
|
1675
|
+
@p5.player.on_key_press("MBTN_RIGHT")
|
|
1676
|
+
def mbtn_right5():
|
|
1677
|
+
self.video_click_signal.emit(5, "MBTN_RIGHT")
|
|
1678
|
+
|
|
1679
|
+
@p5.player.on_key_press("MBTN_LEFT_DBL")
|
|
1680
|
+
def mbtn_left_dbl5():
|
|
1681
|
+
self.video_click_signal.emit(5, "MBTN_LEFT_DBL")
|
|
1682
|
+
|
|
1683
|
+
@p5.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1684
|
+
def mbtn_right_dbl5():
|
|
1685
|
+
self.video_click_signal.emit(5, "MBTN_RIGHT_DBL")
|
|
1686
|
+
|
|
1687
|
+
@p5.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1688
|
+
def ctrl_wheel_up5():
|
|
1689
|
+
self.video_click_signal.emit(5, "Ctrl+WHEEL_UP")
|
|
1690
|
+
|
|
1691
|
+
@p5.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1692
|
+
def ctrl_wheel_down5():
|
|
1693
|
+
self.video_click_signal.emit(5, "Ctrl+WHEEL_DOWN")
|
|
1694
|
+
|
|
1695
|
+
@p5.player.on_key_press("WHEEL_UP")
|
|
1696
|
+
def wheel_up5():
|
|
1697
|
+
self.video_click_signal.emit(5, "WHEEL_UP")
|
|
1698
|
+
|
|
1699
|
+
@p5.player.on_key_press("WHEEL_DOWN")
|
|
1700
|
+
def wheel_down5():
|
|
1701
|
+
self.video_click_signal.emit(5, "WHEEL_DOWN")
|
|
1702
|
+
|
|
1703
|
+
@p5.player.on_key_press("Shift+WHEEL_UP")
|
|
1704
|
+
def shift_wheel_up5():
|
|
1705
|
+
self.video_click_signal.emit(5, "Shift+WHEEL_UP")
|
|
1706
|
+
|
|
1707
|
+
@p5.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1708
|
+
def shift_wheel_down5():
|
|
1709
|
+
self.video_click_signal.emit(5, "Shift+WHEEL_DOWN")
|
|
1710
|
+
|
|
1711
|
+
@p5.player.on_key_press("Shift+MBTN_LEFT")
|
|
1712
|
+
def shift_mbtn_left5():
|
|
1713
|
+
self.video_click_signal.emit(5, "Shift+MBTN_LEFT")
|
|
1714
|
+
|
|
1715
|
+
self.dw_player.append(p5)
|
|
1716
|
+
|
|
1717
|
+
if i == 6:
|
|
1718
|
+
p6 = player_dock_widget.DW_player(6, self)
|
|
1719
|
+
if not self.MPV_IPC_MODE:
|
|
1720
|
+
|
|
1721
|
+
@p6.player.on_key_press("MBTN_LEFT")
|
|
1722
|
+
def mbtn_left6():
|
|
1723
|
+
self.video_click_signal.emit(6, "MBTN_LEFT")
|
|
1724
|
+
|
|
1725
|
+
@p6.player.on_key_press("MBTN_RIGHT")
|
|
1726
|
+
def mbtn_right6():
|
|
1727
|
+
self.video_click_signal.emit(6, "MBTN_RIGHT")
|
|
1728
|
+
|
|
1729
|
+
@p6.player.on_key_press("MBTN_LEFT_DBL")
|
|
1730
|
+
def mbtn_left_dbl6():
|
|
1731
|
+
self.video_click_signal.emit(6, "MBTN_LEFT_DBL")
|
|
1732
|
+
|
|
1733
|
+
@p6.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1734
|
+
def mbtn_right_dbl6():
|
|
1735
|
+
self.video_click_signal.emit(6, "MBTN_RIGHT_DBL")
|
|
1736
|
+
|
|
1737
|
+
@p6.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1738
|
+
def ctrl_wheel_up6():
|
|
1739
|
+
self.video_click_signal.emit(6, "Ctrl+WHEEL_UP")
|
|
1740
|
+
|
|
1741
|
+
@p6.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1742
|
+
def ctrl_wheel_down6():
|
|
1743
|
+
self.video_click_signal.emit(6, "Ctrl+WHEEL_DOWN")
|
|
1744
|
+
|
|
1745
|
+
@p6.player.on_key_press("WHEEL_UP")
|
|
1746
|
+
def wheel_up6():
|
|
1747
|
+
self.video_click_signal.emit(6, "WHEEL_UP")
|
|
1748
|
+
|
|
1749
|
+
@p6.player.on_key_press("WHEEL_DOWN")
|
|
1750
|
+
def wheel_down6():
|
|
1751
|
+
self.video_click_signal.emit(6, "WHEEL_DOWN")
|
|
1752
|
+
|
|
1753
|
+
@p6.player.on_key_press("Shift+WHEEL_UP")
|
|
1754
|
+
def shift_wheel_up6():
|
|
1755
|
+
self.video_click_signal.emit(6, "Shift+WHEEL_UP")
|
|
1756
|
+
|
|
1757
|
+
@p6.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1758
|
+
def shift_wheel_down6():
|
|
1759
|
+
self.video_click_signal.emit(6, "Shift+WHEEL_DOWN")
|
|
1760
|
+
|
|
1761
|
+
@p6.player.on_key_press("Shift+MBTN_LEFT")
|
|
1762
|
+
def shift_mbtn_left6():
|
|
1763
|
+
self.video_click_signal.emit(6, "Shift+MBTN_LEFT")
|
|
1764
|
+
|
|
1765
|
+
self.dw_player.append(p6)
|
|
1766
|
+
|
|
1767
|
+
if i == 7:
|
|
1768
|
+
p7 = player_dock_widget.DW_player(7, self)
|
|
1769
|
+
|
|
1770
|
+
if not self.MPV_IPC_MODE:
|
|
1771
|
+
|
|
1772
|
+
@p7.player.on_key_press("MBTN_LEFT")
|
|
1773
|
+
def mbtn_left7():
|
|
1774
|
+
self.video_click_signal.emit(7, "MBTN_LEFT")
|
|
1775
|
+
|
|
1776
|
+
@p7.player.on_key_press("MBTN_RIGHT")
|
|
1777
|
+
def mbtn_right7():
|
|
1778
|
+
self.video_click_signal.emit(7, "MBTN_RIGHT")
|
|
1779
|
+
|
|
1780
|
+
@p7.player.on_key_press("MBTN_LEFT_DBL")
|
|
1781
|
+
def mbtn_left_dbl7():
|
|
1782
|
+
self.video_click_signal.emit(7, "MBTN_LEFT_DBL")
|
|
1783
|
+
|
|
1784
|
+
@p7.player.on_key_press("MBTN_RIGHT_DBL")
|
|
1785
|
+
def mbtn_right_dbl7():
|
|
1786
|
+
self.video_click_signal.emit(7, "MBTN_RIGHT_DBL")
|
|
1787
|
+
|
|
1788
|
+
@p7.player.on_key_press("Ctrl+WHEEL_UP")
|
|
1789
|
+
def ctrl_wheel_up7():
|
|
1790
|
+
self.video_click_signal.emit(7, "Ctrl+WHEEL_UP")
|
|
1791
|
+
|
|
1792
|
+
@p7.player.on_key_press("Ctrl+WHEEL_DOWN")
|
|
1793
|
+
def ctrl_wheel_down7():
|
|
1794
|
+
self.video_click_signal.emit(7, "Ctrl+WHEEL_DOWN")
|
|
1795
|
+
|
|
1796
|
+
@p7.player.on_key_press("WHEEL_UP")
|
|
1797
|
+
def wheel_up7():
|
|
1798
|
+
self.video_click_signal.emit(7, "WHEEL_UP")
|
|
1799
|
+
|
|
1800
|
+
@p7.player.on_key_press("WHEEL_DOWN")
|
|
1801
|
+
def wheel_down7():
|
|
1802
|
+
self.video_click_signal.emit(7, "WHEEL_DOWN")
|
|
1803
|
+
|
|
1804
|
+
@p7.player.on_key_press("Shift+WHEEL_UP")
|
|
1805
|
+
def shift_wheel_up7():
|
|
1806
|
+
self.video_click_signal.emit(7, "Shift+WHEEL_UP")
|
|
1807
|
+
|
|
1808
|
+
@p7.player.on_key_press("Shift+WHEEL_DOWN")
|
|
1809
|
+
def shift_wheel_down7():
|
|
1810
|
+
self.video_click_signal.emit(7, "Shift+WHEEL_DOWN")
|
|
1811
|
+
|
|
1812
|
+
@p7.player.on_key_press("Shift+MBTN_LEFT")
|
|
1813
|
+
def shift_mbtn_left7():
|
|
1814
|
+
self.video_click_signal.emit(7, "Shift+MBTN_LEFT")
|
|
1815
|
+
|
|
1816
|
+
self.dw_player.append(p7)
|
|
1817
|
+
|
|
1818
|
+
self.dw_player[-1].setFloating(False)
|
|
1819
|
+
self.dw_player[-1].setVisible(False)
|
|
1820
|
+
self.dw_player[-1].setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable)
|
|
1821
|
+
|
|
1822
|
+
# place 4 players at the top of the main window and 4 at the bottom
|
|
1823
|
+
self.addDockWidget(Qt.TopDockWidgetArea if i < 4 else Qt.BottomDockWidgetArea, self.dw_player[-1])
|
|
1824
|
+
|
|
1825
|
+
self.dw_player[i].setVisible(True)
|
|
1826
|
+
|
|
1827
|
+
# for receiving mouse event from frame viewer
|
|
1828
|
+
self.dw_player[i].frame_viewer.mouse_pressed_signal.connect(self.frame_image_clicked)
|
|
1829
|
+
|
|
1830
|
+
# for receiving key event from dock widget
|
|
1831
|
+
self.dw_player[i].key_pressed_signal.connect(self.signal_from_widget)
|
|
1832
|
+
|
|
1833
|
+
# for receiving event from volume slider
|
|
1834
|
+
self.dw_player[i].volume_slider_moved_signal.connect(self.set_volume)
|
|
1835
|
+
|
|
1836
|
+
# for receiving event from mute toolbutton
|
|
1837
|
+
self.dw_player[i].mute_action_triggered_signal.connect(self.set_mute)
|
|
1838
|
+
|
|
1839
|
+
# for receiving resize event from dock widget
|
|
1840
|
+
self.dw_player[i].resize_signal.connect(self.resize_dw)
|
|
1841
|
+
|
|
1842
|
+
# add durations list
|
|
1843
|
+
self.dw_player[i].media_durations = []
|
|
1844
|
+
self.dw_player[i].cumul_media_durations = [0] # [idx for idx,x in enumerate(l) if l[idx-1]<pos<=x]
|
|
1845
|
+
|
|
1846
|
+
# add fps list
|
|
1847
|
+
self.dw_player[i].fps = {}
|
|
1848
|
+
|
|
1849
|
+
if self.MPV_IPC_MODE:
|
|
1850
|
+
while True:
|
|
1851
|
+
r = util.test_mpv_ipc(f"{cfg.MPV_SOCKET}{i}")
|
|
1852
|
+
logging.debug(f"MPV IPC started: {r}")
|
|
1853
|
+
if r:
|
|
1854
|
+
break
|
|
1855
|
+
|
|
1856
|
+
# start timer for activating the main window
|
|
1857
|
+
self.main_window_activation_timer = QTimer()
|
|
1858
|
+
self.main_window_activation_timer.setInterval(500)
|
|
1859
|
+
#self.main_window_activation_timer.timeout.connect(self.activateWindow)
|
|
1860
|
+
self.main_window_activation_timer.timeout.connect(self.activate_main_window)
|
|
1861
|
+
self.main_window_activation_timer.start()
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
|
|
1865
|
+
logging.debug(f"media file: {mediaFile}")
|
|
1866
|
+
|
|
1867
|
+
media_full_path = project_functions.full_path(mediaFile, self.projectFileName)
|
|
1868
|
+
|
|
1869
|
+
logging.debug(f"media_full_path: {media_full_path}")
|
|
1870
|
+
|
|
1871
|
+
# media duration
|
|
1872
|
+
try:
|
|
1873
|
+
mediaLength = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile] * 1000
|
|
1874
|
+
mediaFPS = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]
|
|
1875
|
+
except Exception:
|
|
1876
|
+
logging.debug("media_info key not found in project")
|
|
1877
|
+
|
|
1878
|
+
r = util.accurate_media_analysis(self.ffmpeg_bin, media_full_path)
|
|
1879
|
+
if "error" not in r:
|
|
1880
|
+
if cfg.MEDIA_INFO not in self.pj[cfg.OBSERVATIONS][self.observationId]:
|
|
1881
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO] = {
|
|
1882
|
+
cfg.LENGTH: {},
|
|
1883
|
+
cfg.FPS: {},
|
|
1884
|
+
}
|
|
1885
|
+
if cfg.LENGTH not in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1886
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH] = {}
|
|
1887
|
+
if cfg.FPS not in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1888
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.FPS] = {}
|
|
1889
|
+
|
|
1890
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile] = r["duration"]
|
|
1891
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.FPS][mediaFile] = r[cfg.FPS]
|
|
1892
|
+
|
|
1893
|
+
mediaLength = r["duration"] * 1000
|
|
1894
|
+
mediaFPS = r[cfg.FPS]
|
|
1895
|
+
|
|
1896
|
+
self.project_changed()
|
|
1897
|
+
|
|
1898
|
+
self.dw_player[i].media_durations.append(int(mediaLength))
|
|
1899
|
+
self.dw_player[i].cumul_media_durations.append(self.dw_player[i].cumul_media_durations[-1] + int(mediaLength))
|
|
1900
|
+
|
|
1901
|
+
self.dw_player[i].fps[mediaFile] = mediaFPS
|
|
1902
|
+
|
|
1903
|
+
# add media file to playlist
|
|
1904
|
+
self.dw_player[i].player.playlist_append(media_full_path)
|
|
1905
|
+
|
|
1906
|
+
# add media file name to player window title
|
|
1907
|
+
self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({pl.Path(media_full_path).name})")
|
|
1908
|
+
|
|
1909
|
+
# media duration cumuled in seconds
|
|
1910
|
+
self.dw_player[i].cumul_media_durations_sec = [round(dec(x / 1000), 3) for x in self.dw_player[i].cumul_media_durations]
|
|
1911
|
+
|
|
1912
|
+
# check if BORIS is running on a Windows VM with the 'WMIC COMPUTERSYSTEM GET SERIALNUMBER' command
|
|
1913
|
+
# because "auto" or "auto-safe" crash in Windows VM
|
|
1914
|
+
# see https://superuser.com/questions/1128339/how-can-i-detect-if-im-within-a-vm-or-not
|
|
1915
|
+
|
|
1916
|
+
if not self.MPV_IPC_MODE:
|
|
1917
|
+
flag_vm = False
|
|
1918
|
+
if sys.platform.startswith("win"):
|
|
1919
|
+
p = subprocess.Popen(
|
|
1920
|
+
["WMIC", "BIOS", "GET", "SERIALNUMBER"],
|
|
1921
|
+
stdout=subprocess.PIPE,
|
|
1922
|
+
stderr=subprocess.PIPE,
|
|
1923
|
+
shell=True,
|
|
1924
|
+
)
|
|
1925
|
+
out, _ = p.communicate()
|
|
1926
|
+
flag_vm = b"SerialNumber \r\r\n0 " in out
|
|
1927
|
+
logging.debug(f"Running on Windows VM: {flag_vm}")
|
|
1928
|
+
|
|
1929
|
+
if not flag_vm:
|
|
1930
|
+
self.dw_player[i].player.hwdec = self.config_param.get(cfg.MPV_HWDEC, cfg.MPV_HWDEC_DEFAULT_VALUE)
|
|
1931
|
+
else:
|
|
1932
|
+
self.dw_player[i].player.hwdec = cfg.MPV_HWDEC_NO
|
|
1933
|
+
|
|
1934
|
+
logging.debug(f"Player hwdec of player #{i} set to: {self.dw_player[i].player.hwdec}")
|
|
1935
|
+
self.config_param[cfg.MPV_HWDEC] = self.dw_player[i].player.hwdec
|
|
1936
|
+
|
|
1937
|
+
self.dw_player[i].player.playlist_pos = 0
|
|
1938
|
+
self.dw_player[i].player.wait_until_playing()
|
|
1939
|
+
self.dw_player[i].player.pause = True
|
|
1940
|
+
time.sleep(0.2)
|
|
1941
|
+
# self.dw_player[i].player.wait_until_paused()
|
|
1942
|
+
self.dw_player[i].player.seek(0, "absolute")
|
|
1943
|
+
# do not close when playing finished
|
|
1944
|
+
self.dw_player[i].player.keep_open = True
|
|
1945
|
+
self.dw_player[i].player.keep_open_pause = False
|
|
1946
|
+
|
|
1947
|
+
self.dw_player[i].player.image_display_duration = self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.IMAGE_DISPLAY_DURATION, 1)
|
|
1948
|
+
|
|
1949
|
+
# position media
|
|
1950
|
+
self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]), player=i)
|
|
1951
|
+
|
|
1952
|
+
# restore video zoom level
|
|
1953
|
+
if cfg.ZOOM_LEVEL in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1954
|
+
self.dw_player[i].player.video_zoom = log2(
|
|
1955
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.ZOOM_LEVEL].get(n_player, 0)
|
|
1956
|
+
)
|
|
1957
|
+
|
|
1958
|
+
# restore video pan
|
|
1959
|
+
if cfg.PAN_X in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1960
|
+
self.dw_player[i].player.video_pan_x = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.PAN_X].get(n_player, 0)
|
|
1961
|
+
if cfg.PAN_Y in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1962
|
+
self.dw_player[i].player.video_pan_y = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.PAN_Y].get(n_player, 0)
|
|
1963
|
+
|
|
1964
|
+
# restore rotation angle
|
|
1965
|
+
if cfg.ROTATION_ANGLE in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1966
|
+
self.dw_player[i].player.video_rotate = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.ROTATION_ANGLE].get(
|
|
1967
|
+
n_player, 0
|
|
1968
|
+
)
|
|
1969
|
+
|
|
1970
|
+
# restore subtitle visibility
|
|
1971
|
+
if cfg.DISPLAY_MEDIA_SUBTITLES in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1972
|
+
self.dw_player[i].player.sub_visibility = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][
|
|
1973
|
+
cfg.DISPLAY_MEDIA_SUBTITLES
|
|
1974
|
+
].get(n_player, True)
|
|
1975
|
+
|
|
1976
|
+
# restore overlays
|
|
1977
|
+
if cfg.OVERLAY in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
|
|
1978
|
+
if n_player in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY]:
|
|
1979
|
+
self.overlays[i] = self.dw_player[i].player.create_image_overlay()
|
|
1980
|
+
self.resize_dw(i)
|
|
1981
|
+
|
|
1982
|
+
menu_options.update_menu(self)
|
|
1983
|
+
|
|
1984
|
+
if self.MPV_IPC_MODE:
|
|
1985
|
+
# activate timer
|
|
1986
|
+
self.ipc_mpv_timer = QTimer()
|
|
1987
|
+
self.ipc_mpv_timer.setInterval(500)
|
|
1988
|
+
self.ipc_mpv_timer.timeout.connect(self.mpv_timer_out)
|
|
1989
|
+
|
|
1990
|
+
else:
|
|
1991
|
+
self.ipc_mpv_timer = None
|
|
1992
|
+
self.time_observer_signal.connect(self.mpv_timer_out)
|
|
1993
|
+
|
|
1994
|
+
self.mpv_eof_reached_signal.connect(self.mpv_eof_reached)
|
|
1995
|
+
self.video_click_signal.connect(self.player_clicked)
|
|
1996
|
+
|
|
1997
|
+
self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
|
|
1998
|
+
|
|
1999
|
+
self.display_statusbar_info(self.observationId)
|
|
2000
|
+
|
|
2001
|
+
self.currentSubject = ""
|
|
2002
|
+
# store state behaviors for subject current state
|
|
2003
|
+
self.state_behaviors_codes = tuple(util.state_behavior_codes(self.pj[cfg.ETHOGRAM]))
|
|
2004
|
+
|
|
2005
|
+
video_operations.display_play_rate(self)
|
|
2006
|
+
video_operations.display_zoom_level(self)
|
|
2007
|
+
|
|
2008
|
+
# spectrogram
|
|
2009
|
+
if (
|
|
2010
|
+
cfg.VISUALIZE_SPECTROGRAM in self.pj[cfg.OBSERVATIONS][self.observationId]
|
|
2011
|
+
and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.VISUALIZE_SPECTROGRAM]
|
|
2012
|
+
):
|
|
2013
|
+
tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
|
|
2014
|
+
|
|
2015
|
+
wav_file_path = (
|
|
2016
|
+
pl.Path(tmp_dir) / pl.Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
|
|
2017
|
+
)
|
|
2018
|
+
|
|
2019
|
+
if not wav_file_path.is_file():
|
|
2020
|
+
self.generate_wav_file_from_media()
|
|
2021
|
+
|
|
2022
|
+
self.show_plot_widget("spectrogram", warning=False)
|
|
2023
|
+
|
|
2024
|
+
# waveform
|
|
2025
|
+
if (
|
|
2026
|
+
cfg.VISUALIZE_WAVEFORM in self.pj[cfg.OBSERVATIONS][self.observationId]
|
|
2027
|
+
and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.VISUALIZE_WAVEFORM]
|
|
2028
|
+
):
|
|
2029
|
+
tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
|
|
2030
|
+
|
|
2031
|
+
wav_file_path = (
|
|
2032
|
+
pl.Path(tmp_dir) / pl.Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
if not wav_file_path.is_file():
|
|
2036
|
+
self.generate_wav_file_from_media()
|
|
2037
|
+
|
|
2038
|
+
self.show_plot_widget("waveform", warning=False)
|
|
2039
|
+
|
|
2040
|
+
# external data plot
|
|
2041
|
+
if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId] and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
|
|
2042
|
+
self.plot_data = {}
|
|
2043
|
+
self.ext_data_timer_list = []
|
|
2044
|
+
count = 0
|
|
2045
|
+
for idx in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
|
|
2046
|
+
if count == 0:
|
|
2047
|
+
data_ok: bool = True
|
|
2048
|
+
data_file_path = project_functions.full_path(
|
|
2049
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"],
|
|
2050
|
+
self.projectFileName,
|
|
2051
|
+
)
|
|
2052
|
+
if not data_file_path:
|
|
2053
|
+
QMessageBox.critical(
|
|
2054
|
+
self,
|
|
2055
|
+
cfg.programName,
|
|
2056
|
+
"Data file not found:\n{}".format(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]),
|
|
2057
|
+
)
|
|
2058
|
+
data_ok = False
|
|
2059
|
+
# return False
|
|
2060
|
+
|
|
2061
|
+
w1 = plot_data_module.Plot_data(
|
|
2062
|
+
data_file_path,
|
|
2063
|
+
int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["time_interval"]),
|
|
2064
|
+
str(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["time_offset"]),
|
|
2065
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["color"],
|
|
2066
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["title"],
|
|
2067
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["variable_name"],
|
|
2068
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["columns"],
|
|
2069
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["substract_first_value"],
|
|
2070
|
+
self.pj[cfg.CONVERTERS] if cfg.CONVERTERS in self.pj else {},
|
|
2071
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["converters"],
|
|
2072
|
+
log_level=logging.getLogger().getEffectiveLevel(),
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
if w1.error_msg:
|
|
2076
|
+
QMessageBox.critical(
|
|
2077
|
+
self,
|
|
2078
|
+
cfg.programName,
|
|
2079
|
+
(
|
|
2080
|
+
"Impossible to plot data from file "
|
|
2081
|
+
f"{os.path.basename(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]['file_path'])}:\n"
|
|
2082
|
+
f"{w1.error_msg}"
|
|
2083
|
+
),
|
|
2084
|
+
)
|
|
2085
|
+
del w1
|
|
2086
|
+
data_ok = False
|
|
2087
|
+
# return False
|
|
2088
|
+
|
|
2089
|
+
if data_ok:
|
|
2090
|
+
w1.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
2091
|
+
w1.sendEvent.connect(self.signal_from_widget) # keypress event
|
|
2092
|
+
|
|
2093
|
+
w1.show()
|
|
2094
|
+
|
|
2095
|
+
self.ext_data_timer_list.append(QTimer())
|
|
2096
|
+
self.ext_data_timer_list[-1].setInterval(w1.time_out)
|
|
2097
|
+
self.ext_data_timer_list[-1].timeout.connect(lambda: self.timer_plot_data_out(w1))
|
|
2098
|
+
self.timer_plot_data_out(w1)
|
|
2099
|
+
|
|
2100
|
+
self.plot_data[count] = w1
|
|
2101
|
+
|
|
2102
|
+
if count == 1:
|
|
2103
|
+
data_ok: bool = True
|
|
2104
|
+
data_file_path = project_functions.full_path(
|
|
2105
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"],
|
|
2106
|
+
self.projectFileName,
|
|
2107
|
+
)
|
|
2108
|
+
if not data_file_path:
|
|
2109
|
+
QMessageBox.critical(
|
|
2110
|
+
self,
|
|
2111
|
+
cfg.programName,
|
|
2112
|
+
"Data file not found:\n{}".format(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]),
|
|
2113
|
+
)
|
|
2114
|
+
data_ok = False
|
|
2115
|
+
# return False
|
|
2116
|
+
|
|
2117
|
+
w2 = plot_data_module.Plot_data(
|
|
2118
|
+
data_file_path,
|
|
2119
|
+
int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["time_interval"]),
|
|
2120
|
+
str(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["time_offset"]),
|
|
2121
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["color"],
|
|
2122
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["title"],
|
|
2123
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["variable_name"],
|
|
2124
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["columns"],
|
|
2125
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["substract_first_value"],
|
|
2126
|
+
self.pj[cfg.CONVERTERS] if cfg.CONVERTERS in self.pj else {},
|
|
2127
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["converters"],
|
|
2128
|
+
log_level=logging.getLogger().getEffectiveLevel(),
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
if w2.error_msg:
|
|
2132
|
+
QMessageBox.critical(
|
|
2133
|
+
self,
|
|
2134
|
+
cfg.programName,
|
|
2135
|
+
(
|
|
2136
|
+
f"Impossible to plot data from file "
|
|
2137
|
+
f"{os.path.basename(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]['file_path'])}:\n{w2.error_msg}"
|
|
2138
|
+
),
|
|
2139
|
+
)
|
|
2140
|
+
del w2
|
|
2141
|
+
data_ok = False
|
|
2142
|
+
# return False
|
|
2143
|
+
|
|
2144
|
+
if data_ok:
|
|
2145
|
+
w2.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
2146
|
+
w2.sendEvent.connect(self.signal_from_widget)
|
|
2147
|
+
|
|
2148
|
+
w2.show()
|
|
2149
|
+
self.ext_data_timer_list.append(QTimer())
|
|
2150
|
+
self.ext_data_timer_list[-1].setInterval(w2.time_out)
|
|
2151
|
+
self.ext_data_timer_list[-1].timeout.connect(lambda: self.timer_plot_data_out(w2))
|
|
2152
|
+
self.timer_plot_data_out(w2)
|
|
2153
|
+
|
|
2154
|
+
self.plot_data[count] = w2
|
|
2155
|
+
|
|
2156
|
+
count += 1
|
|
2157
|
+
|
|
2158
|
+
# check if "filtered behaviors"
|
|
2159
|
+
if cfg.FILTERED_BEHAVIORS in self.pj[cfg.OBSERVATIONS][self.observationId]:
|
|
2160
|
+
self.load_behaviors_in_twEthogram(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILTERED_BEHAVIORS])
|
|
2161
|
+
|
|
2162
|
+
# restore windows state: dockwidget positions ...
|
|
2163
|
+
if self.saved_state is None:
|
|
2164
|
+
self.saved_state = self.saveState()
|
|
2165
|
+
self.restoreState(self.saved_state)
|
|
2166
|
+
else:
|
|
2167
|
+
try:
|
|
2168
|
+
self.restoreState(self.saved_state)
|
|
2169
|
+
except TypeError:
|
|
2170
|
+
logging.critical("state not restored: Type error")
|
|
2171
|
+
self.saved_state = self.saveState()
|
|
2172
|
+
self.restoreState(self.saved_state)
|
|
2173
|
+
|
|
2174
|
+
for player in self.dw_player:
|
|
2175
|
+
player.setVisible(True)
|
|
2176
|
+
|
|
2177
|
+
self.load_tw_events(self.observationId)
|
|
2178
|
+
|
|
2179
|
+
# initial synchro
|
|
2180
|
+
if not self.MPV_IPC_MODE:
|
|
2181
|
+
for n_player in range(1, len(self.dw_player)):
|
|
2182
|
+
self.sync_time(n_player, 0)
|
|
2183
|
+
|
|
2184
|
+
self.mpv_timer_out(value=0.0)
|
|
2185
|
+
|
|
2186
|
+
"""
|
|
2187
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO].get(cfg.OVERLAY, {}):
|
|
2188
|
+
for i in range(cfg.N_PLAYER):
|
|
2189
|
+
# restore overlays
|
|
2190
|
+
if str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY]:
|
|
2191
|
+
self.overlays[i] = self.dw_player[i].player.create_image_overlay()
|
|
2192
|
+
self.resize_dw(i)
|
|
2193
|
+
"""
|
|
2194
|
+
|
|
2195
|
+
return True
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
def initialize_new_live_observation(self):
|
|
2199
|
+
"""
|
|
2200
|
+
initialize a new live observation
|
|
2201
|
+
"""
|
|
2202
|
+
logging.debug(f"function: initialize new live obs: {self.observationId}")
|
|
2203
|
+
|
|
2204
|
+
self.playerType = cfg.LIVE
|
|
2205
|
+
|
|
2206
|
+
self.pb_live_obs.setMinimumHeight(60)
|
|
2207
|
+
|
|
2208
|
+
font = QFont()
|
|
2209
|
+
font.setPointSize(48)
|
|
2210
|
+
self.lb_current_media_time.setFont(font)
|
|
2211
|
+
|
|
2212
|
+
for dw in [self.dwEthogram, self.dwSubjects, self.dwEvents]:
|
|
2213
|
+
dw.setVisible(True)
|
|
2214
|
+
|
|
2215
|
+
# button start enabled
|
|
2216
|
+
self.pb_live_obs.setEnabled(True)
|
|
2217
|
+
|
|
2218
|
+
self.w_live.setVisible(True)
|
|
2219
|
+
self.w_obs_info.setVisible(True)
|
|
2220
|
+
|
|
2221
|
+
menu_options.update_menu(self)
|
|
2222
|
+
|
|
2223
|
+
self.liveObservationStarted = False
|
|
2224
|
+
self.pb_live_obs.setText("Start live observation")
|
|
2225
|
+
|
|
2226
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.START_FROM_CURRENT_TIME, False):
|
|
2227
|
+
current_time = util.seconds_of_day(dt.datetime.now())
|
|
2228
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False):
|
|
2229
|
+
current_time = time.mktime(dt.datetime.now().timetuple())
|
|
2230
|
+
else:
|
|
2231
|
+
current_time = 0
|
|
2232
|
+
|
|
2233
|
+
self.lb_current_media_time.setText(util.convertTime(self.timeFormat, current_time))
|
|
2234
|
+
|
|
2235
|
+
# display observation time interval (if any)
|
|
2236
|
+
self.lb_obs_time_interval.setVisible(True)
|
|
2237
|
+
self.display_statusbar_info(self.observationId)
|
|
2238
|
+
|
|
2239
|
+
self.currentSubject = ""
|
|
2240
|
+
# store state behaviors for subject current state
|
|
2241
|
+
self.state_behaviors_codes = tuple(util.state_behavior_codes(self.pj[cfg.ETHOGRAM]))
|
|
2242
|
+
|
|
2243
|
+
self.lbCurrentStates.setText("")
|
|
2244
|
+
|
|
2245
|
+
self.liveStartTime = None
|
|
2246
|
+
self.liveTimer.stop()
|
|
2247
|
+
|
|
2248
|
+
self.load_tw_events(self.observationId)
|
|
2249
|
+
|
|
2250
|
+
self.get_events_current_row()
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
def initialize_new_images_observation(self):
|
|
2254
|
+
"""
|
|
2255
|
+
initialize a new observation from directories of images
|
|
2256
|
+
"""
|
|
2257
|
+
|
|
2258
|
+
for dw in (self.dwEthogram, self.dwSubjects, self.dwEvents):
|
|
2259
|
+
dw.setVisible(True)
|
|
2260
|
+
# disable start live button
|
|
2261
|
+
self.pb_live_obs.setEnabled(False)
|
|
2262
|
+
self.w_live.setVisible(False)
|
|
2263
|
+
|
|
2264
|
+
# check if directories are available
|
|
2265
|
+
ok, msg = project_functions.check_directories_availability(self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName)
|
|
2266
|
+
|
|
2267
|
+
if not ok:
|
|
2268
|
+
QMessageBox.critical(
|
|
2269
|
+
self,
|
|
2270
|
+
cfg.programName,
|
|
2271
|
+
(
|
|
2272
|
+
f"{msg}<br><br>The observation will be opened in VIEW mode.<br>"
|
|
2273
|
+
"It will not be possible to log events.<br>"
|
|
2274
|
+
"Modify the directoriy path(s) to point existing directory "
|
|
2275
|
+
),
|
|
2276
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
2277
|
+
QMessageBox.NoButton,
|
|
2278
|
+
)
|
|
2279
|
+
self.playerType = cfg.VIEWER_IMAGES
|
|
2280
|
+
return
|
|
2281
|
+
|
|
2282
|
+
# count number of images in all directories
|
|
2283
|
+
tot_images_number = 0
|
|
2284
|
+
for dir_path in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
|
|
2285
|
+
full_dir_path = project_functions.full_path(dir_path, self.projectFileName)
|
|
2286
|
+
result = util.dir_images_number(full_dir_path)
|
|
2287
|
+
tot_images_number += result.get("number of images", 0)
|
|
2288
|
+
|
|
2289
|
+
if not tot_images_number:
|
|
2290
|
+
QMessageBox.critical(
|
|
2291
|
+
self,
|
|
2292
|
+
cfg.programName,
|
|
2293
|
+
(
|
|
2294
|
+
"No images were found in directory(ies).<br><br>The observation will be opened in VIEW mode.<br>"
|
|
2295
|
+
"It will not be possible to log events.<br>"
|
|
2296
|
+
"Modify the directoriy path(s) to point existing directory "
|
|
2297
|
+
),
|
|
2298
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
2299
|
+
QMessageBox.NoButton,
|
|
2300
|
+
)
|
|
2301
|
+
self.playerType = cfg.VIEWER_IMAGES
|
|
2302
|
+
return
|
|
2303
|
+
|
|
2304
|
+
self.playerType = cfg.IMAGES
|
|
2305
|
+
# load image paths
|
|
2306
|
+
# directories user order is maintained
|
|
2307
|
+
# images are sorted inside each directory
|
|
2308
|
+
self.images_list: list = []
|
|
2309
|
+
for dir_path in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
|
|
2310
|
+
full_dir_path = project_functions.full_path(dir_path, self.projectFileName)
|
|
2311
|
+
for pattern in cfg.IMAGE_EXTENSIONS:
|
|
2312
|
+
self.images_list.extend(
|
|
2313
|
+
sorted(
|
|
2314
|
+
list(
|
|
2315
|
+
set(
|
|
2316
|
+
[str(x) for x in pl.Path(full_dir_path).glob(pattern)]
|
|
2317
|
+
+ [str(x) for x in pl.Path(full_dir_path).glob(pattern.upper())]
|
|
2318
|
+
)
|
|
2319
|
+
)
|
|
2320
|
+
)
|
|
2321
|
+
)
|
|
2322
|
+
|
|
2323
|
+
# logging.debug(self.images_list)
|
|
2324
|
+
|
|
2325
|
+
self.image_idx = 0
|
|
2326
|
+
self.image_time_ref = None
|
|
2327
|
+
|
|
2328
|
+
self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)
|
|
2329
|
+
self.dw_player = []
|
|
2330
|
+
i = 0
|
|
2331
|
+
self.dw_player.append(player_dock_widget.DW_player(i, self))
|
|
2332
|
+
self.addDockWidget(Qt.TopDockWidgetArea, self.dw_player[i])
|
|
2333
|
+
self.dw_player[i].setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable)
|
|
2334
|
+
|
|
2335
|
+
self.dw_player[i].setVisible(True)
|
|
2336
|
+
|
|
2337
|
+
# for receiving mouse event from frame viewer
|
|
2338
|
+
self.dw_player[i].frame_viewer.mouse_pressed_signal.connect(self.frame_image_clicked)
|
|
2339
|
+
|
|
2340
|
+
# for receiving key event from dock widget
|
|
2341
|
+
self.dw_player[i].key_pressed_signal.connect(self.signal_from_widget)
|
|
2342
|
+
|
|
2343
|
+
# for receiving resize event from dock widget
|
|
2344
|
+
self.dw_player[i].resize_signal.connect(self.resize_dw)
|
|
2345
|
+
|
|
2346
|
+
self.dw_player[i].stack.setCurrentIndex(cfg.PICTURE_VIEWER)
|
|
2347
|
+
|
|
2348
|
+
menu_options.update_menu(self)
|
|
2349
|
+
|
|
2350
|
+
self.display_statusbar_info(self.observationId)
|
|
2351
|
+
|
|
2352
|
+
self.currentSubject = ""
|
|
2353
|
+
# store state behaviors for subject current state
|
|
2354
|
+
self.state_behaviors_codes = tuple(util.state_behavior_codes(self.pj[cfg.ETHOGRAM]))
|
|
2355
|
+
|
|
2356
|
+
# check if "filtered behaviors"
|
|
2357
|
+
if cfg.FILTERED_BEHAVIORS in self.pj[cfg.OBSERVATIONS][self.observationId]:
|
|
2358
|
+
self.load_behaviors_in_twEthogram(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILTERED_BEHAVIORS])
|
|
2359
|
+
|
|
2360
|
+
# restore windows state: dockwidget positions ...
|
|
2361
|
+
if self.saved_state is None:
|
|
2362
|
+
self.saved_state = self.saveState()
|
|
2363
|
+
self.restoreState(self.saved_state)
|
|
2364
|
+
else:
|
|
2365
|
+
try:
|
|
2366
|
+
self.restoreState(self.saved_state)
|
|
2367
|
+
except TypeError:
|
|
2368
|
+
logging.critical("state not restored: Type error")
|
|
2369
|
+
self.saved_state = self.saveState()
|
|
2370
|
+
self.restoreState(self.saved_state)
|
|
2371
|
+
|
|
2372
|
+
self.extract_frame(self.dw_player[i])
|
|
2373
|
+
self.w_obs_info.setVisible(True)
|
|
2374
|
+
|
|
2375
|
+
self.get_events_current_row()
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
2379
|
+
"""
|
|
2380
|
+
returns the media file name corresponding to the event (start time in case of state event)
|
|
2381
|
+
|
|
2382
|
+
Args:
|
|
2383
|
+
observation (dict): observation
|
|
2384
|
+
timestamp (dec): time stamp
|
|
2385
|
+
|
|
2386
|
+
Returns:
|
|
2387
|
+
str: path of media file containing the event
|
|
2388
|
+
"""
|
|
2389
|
+
if observation.get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
|
|
2390
|
+
# media creation date/time was used for coding
|
|
2391
|
+
video_file_name = None
|
|
2392
|
+
for media_path in observation[cfg.MEDIA_INFO].get(cfg.MEDIA_CREATION_TIME, {}):
|
|
2393
|
+
start_media = observation[cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_path]
|
|
2394
|
+
duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_path]
|
|
2395
|
+
if start_media <= timestamp <= start_media + duration:
|
|
2396
|
+
video_file_name = media_path
|
|
2397
|
+
break
|
|
2398
|
+
|
|
2399
|
+
else: # no media creation date
|
|
2400
|
+
cumul_media_durations: list = [dec(0)]
|
|
2401
|
+
for media_file in observation[cfg.FILE][cfg.PLAYER1]:
|
|
2402
|
+
try:
|
|
2403
|
+
media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
|
|
2404
|
+
# cut off media duration to 3 decimal places as that is how fine the player is
|
|
2405
|
+
media_duration = floor(media_duration * 10**3) / dec(10**3)
|
|
2406
|
+
cumul_media_durations.append(floor((cumul_media_durations[-1] + media_duration) * 10**3) / dec(10**3))
|
|
2407
|
+
except KeyError:
|
|
2408
|
+
return None
|
|
2409
|
+
|
|
2410
|
+
"""
|
|
2411
|
+
cumul_media_durations: list = [dec(0)]
|
|
2412
|
+
for media_file in observation[cfg.FILE][cfg.PLAYER1]:
|
|
2413
|
+
try:
|
|
2414
|
+
media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
|
|
2415
|
+
cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
|
|
2416
|
+
except KeyError:
|
|
2417
|
+
return None
|
|
2418
|
+
"""
|
|
2419
|
+
|
|
2420
|
+
cumul_media_durations.remove(dec(0))
|
|
2421
|
+
|
|
2422
|
+
logging.debug(f"{cumul_media_durations=}")
|
|
2423
|
+
|
|
2424
|
+
# test if timestamp is at end of last media
|
|
2425
|
+
if timestamp == cumul_media_durations[-1]:
|
|
2426
|
+
player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
|
|
2427
|
+
else:
|
|
2428
|
+
player_idx = None
|
|
2429
|
+
for idx, value in enumerate(cumul_media_durations):
|
|
2430
|
+
start = 0 if idx == 0 else cumul_media_durations[idx - 1]
|
|
2431
|
+
if start <= timestamp < value:
|
|
2432
|
+
player_idx = idx
|
|
2433
|
+
break
|
|
2434
|
+
|
|
2435
|
+
video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx] if player_idx is not None else None
|
|
2436
|
+
|
|
2437
|
+
return video_file_name
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
def create_observations(self):
|
|
2441
|
+
"""
|
|
2442
|
+
Create observations from a media file directory
|
|
2443
|
+
"""
|
|
2444
|
+
# print(self.pj[cfg.OBSERVATIONS])
|
|
2445
|
+
|
|
2446
|
+
dir_path = QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))
|
|
2447
|
+
if not dir_path:
|
|
2448
|
+
return
|
|
2449
|
+
|
|
2450
|
+
dlg = dialog.Input_dialog(
|
|
2451
|
+
label_caption="Set the following observation parameters",
|
|
2452
|
+
elements_list=[
|
|
2453
|
+
("cb", "Recurse the subdirectories", False),
|
|
2454
|
+
("cb", "Save the absolute media file path", True),
|
|
2455
|
+
("cb", "Visualize spectrogram", False),
|
|
2456
|
+
("cb", "Visualize waveform", False),
|
|
2457
|
+
("cb", "Media creation date as offset", False),
|
|
2458
|
+
("cb", "Close behaviors between videos", False),
|
|
2459
|
+
("dsb", "Time offset (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
2460
|
+
("dsb", "Media scan sampling duration (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
2461
|
+
],
|
|
2462
|
+
title="Observation parameters",
|
|
2463
|
+
)
|
|
2464
|
+
if not dlg.exec_():
|
|
2465
|
+
return
|
|
2466
|
+
|
|
2467
|
+
file_count: int = 0
|
|
2468
|
+
|
|
2469
|
+
if dlg.elements["Recurse the subdirectories"].isChecked():
|
|
2470
|
+
files_list = pl.Path(dir_path).rglob("*")
|
|
2471
|
+
else:
|
|
2472
|
+
files_list = pl.Path(dir_path).glob("*")
|
|
2473
|
+
|
|
2474
|
+
for file in files_list:
|
|
2475
|
+
if not file.is_file():
|
|
2476
|
+
continue
|
|
2477
|
+
r = util.accurate_media_analysis(ffmpeg_bin=self.ffmpeg_bin, file_name=file)
|
|
2478
|
+
if "error" not in r:
|
|
2479
|
+
if not r.get("frames_number", 0):
|
|
2480
|
+
continue
|
|
2481
|
+
|
|
2482
|
+
if dlg.elements["Save the absolute media file path"].isChecked():
|
|
2483
|
+
media_file = str(file)
|
|
2484
|
+
else:
|
|
2485
|
+
try:
|
|
2486
|
+
media_file = str(file.relative_to(pl.Path(self.projectFileName).parent))
|
|
2487
|
+
except ValueError:
|
|
2488
|
+
QMessageBox.critical(
|
|
2489
|
+
self,
|
|
2490
|
+
cfg.programName,
|
|
2491
|
+
(
|
|
2492
|
+
f"the media file <b>{file}</b> can not be relative to the project directory "
|
|
2493
|
+
f"(<b>{pl.Path(self.projectFileName).parent}</b>)"
|
|
2494
|
+
"<br><br>Aborting the creation of observations"
|
|
2495
|
+
),
|
|
2496
|
+
)
|
|
2497
|
+
return
|
|
2498
|
+
|
|
2499
|
+
if media_file in self.pj[cfg.OBSERVATIONS]:
|
|
2500
|
+
QMessageBox.critical(
|
|
2501
|
+
self,
|
|
2502
|
+
cfg.programName,
|
|
2503
|
+
(f"The observation <b>{media_file}</b> already exists.<br><br>Aborting the creation of observations"),
|
|
2504
|
+
)
|
|
2505
|
+
return
|
|
2506
|
+
|
|
2507
|
+
self.pj[cfg.OBSERVATIONS][media_file] = {
|
|
2508
|
+
"file": {"1": [media_file], "2": [], "3": [], "4": [], "5": [], "6": [], "7": [], "8": []},
|
|
2509
|
+
"type": "MEDIA",
|
|
2510
|
+
"date": dt.datetime.now().replace(microsecond=0).isoformat(),
|
|
2511
|
+
"description": "",
|
|
2512
|
+
"time offset": dec(str(round(dlg.elements["Time offset (in seconds)"].value(), 3))),
|
|
2513
|
+
"events": [],
|
|
2514
|
+
"observation time interval": [0, 0],
|
|
2515
|
+
"independent_variables": {},
|
|
2516
|
+
"visualize_spectrogram": dlg.elements["Visualize spectrogram"].isChecked(),
|
|
2517
|
+
"visualize_waveform": dlg.elements["Visualize waveform"].isChecked(),
|
|
2518
|
+
"media_creation_date_as_offset": dlg.elements["Media creation date as offset"].isChecked(),
|
|
2519
|
+
"media_scan_sampling_duration": dec(str(round(dlg.elements["Media scan sampling duration (in seconds)"].value(), 3))),
|
|
2520
|
+
"image_display_duration": 1,
|
|
2521
|
+
"close_behaviors_between_videos": dlg.elements["Close behaviors between videos"].isChecked(),
|
|
2522
|
+
"media_info": {
|
|
2523
|
+
"length": {media_file: r["duration"]},
|
|
2524
|
+
"fps": {media_file: r["duration"]},
|
|
2525
|
+
"hasVideo": {media_file: r["has_video"]},
|
|
2526
|
+
"hasAudio": {media_file: r["has_audio"]},
|
|
2527
|
+
"offset": {"1": 0.0},
|
|
2528
|
+
},
|
|
2529
|
+
}
|
|
2530
|
+
file_count += 1
|
|
2531
|
+
self.project_changed()
|
|
2532
|
+
|
|
2533
|
+
if file_count:
|
|
2534
|
+
message: str = f"{file_count} observation(s) were created" if file_count > 1 else "One observation was created"
|
|
2535
|
+
else:
|
|
2536
|
+
message: str = f"No media file were found in {dir_path}"
|
|
2537
|
+
|
|
2538
|
+
QMessageBox.information(self, cfg.programName, message)
|