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.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. 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)