boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  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 +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  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.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -19,20 +19,23 @@ Copyright 2012-2023 Olivier Friard
19
19
  MA 02110-1301, USA.
20
20
  """
21
21
 
22
- from math import log2
23
- import os
24
22
  import logging
25
- import time
26
- import tempfile
27
- import subprocess
28
- import sys
23
+ from collections import deque
24
+ import datetime as dt
29
25
  from decimal import Decimal as dec
26
+ import json
27
+ from math import log2, floor
28
+ import os
30
29
  import pathlib as pl
31
- import datetime
32
- from typing import List, Tuple, Dict, Optional
30
+ import socket
31
+ import subprocess
32
+ import sys
33
+ import tempfile
34
+ import time
35
+ from typing import List, Tuple, Optional
33
36
 
34
37
 
35
- from PyQt5.QtWidgets import (
38
+ from PySide6.QtWidgets import (
36
39
  QMessageBox,
37
40
  QFileDialog,
38
41
  QDateTimeEdit,
@@ -41,11 +44,12 @@ from PyQt5.QtWidgets import (
41
44
  QSlider,
42
45
  QMainWindow,
43
46
  QDockWidget,
47
+ QWidget,
44
48
  )
45
- from PyQt5.QtCore import Qt, QDateTime, QTimer
46
- from PyQt5.QtGui import QFont, QIcon
49
+ from PySide6.QtCore import Qt, QDateTime, QTimer
50
+ from PySide6.QtGui import QFont, QIcon, QTextCursor
47
51
 
48
- from PyQt5 import QtTest
52
+ from PySide6 import QtTest
49
53
 
50
54
  from . import menu_options
51
55
  from . import config as cfg
@@ -58,6 +62,7 @@ from . import plot_data_module
58
62
  from . import player_dock_widget
59
63
  from . import gui_utilities
60
64
  from . import video_operations
65
+ from . import state_events
61
66
 
62
67
 
63
68
  def export_observations_list_clicked(self):
@@ -78,9 +83,7 @@ def export_observations_list_clicked(self):
78
83
  cfg.HTML,
79
84
  ]
80
85
 
81
- file_name, filter_ = QFileDialog().getSaveFileName(
82
- self, "Export list of selected observations", "", ";;".join(file_formats)
83
- )
86
+ file_name, filter_ = QFileDialog().getSaveFileName(self, "Export list of selected observations", "", ";;".join(file_formats))
84
87
 
85
88
  if not file_name:
86
89
  return
@@ -90,12 +93,7 @@ def export_observations_list_clicked(self):
90
93
  file_name = str(pl.Path(file_name)) + "." + output_format
91
94
  # check if file name with extension already exists
92
95
  if pl.Path(file_name).is_file():
93
- if (
94
- dialog.MessageDialog(
95
- cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
96
- )
97
- == cfg.CANCEL
98
- ):
96
+ if dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
99
97
  return
100
98
 
101
99
  if not project_functions.export_observations_list(self.pj, selected_observations, file_name, output_format):
@@ -107,7 +105,7 @@ def observations_list(self):
107
105
  show list of all observations of current project
108
106
  """
109
107
 
110
- logging.debug(f"observations list")
108
+ logging.debug("observations list")
111
109
 
112
110
  if self.playerType in cfg.VIEWERS:
113
111
  close_observation(self)
@@ -115,20 +113,25 @@ def observations_list(self):
115
113
  result, selected_obs = select_observations.select_observations2(self, cfg.SINGLE)
116
114
 
117
115
  if not selected_obs:
116
+ # activate main window
117
+ self.activateWindow()
118
118
  return
119
119
 
120
120
  if self.observationId:
121
-
122
121
  self.hide_data_files()
123
122
  response = dialog.MessageDialog(
124
123
  cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
125
124
  )
126
125
  if response == cfg.NO:
127
126
  self.show_data_files()
127
+ # activate main window
128
+ self.activateWindow()
129
+
128
130
  return ""
129
131
  else:
130
132
  close_observation(self)
131
133
 
134
+
132
135
  QtTest.QTest.qWait(1000)
133
136
 
134
137
  if result == cfg.OPEN:
@@ -144,10 +147,12 @@ def observations_list(self):
144
147
  QMessageBox.warning(
145
148
  self,
146
149
  cfg.programName,
147
- (f"The observation <b>{self.observationId}</b> is running!<br>" "Close it before editing."),
150
+ (f"The observation <b>{self.observationId}</b> is running!<br>Close it before editing."),
148
151
  )
149
152
 
150
- logging.debug(f"end observations list")
153
+ logging.debug("end observations list")
154
+ # activate main window
155
+ self.activateWindow()
151
156
 
152
157
 
153
158
  def open_observation(self, mode: str) -> str:
@@ -159,11 +164,10 @@ def open_observation(self, mode: str) -> str:
159
164
  "view" to view observation
160
165
  """
161
166
 
162
- logging.debug(f"open observation")
167
+ logging.debug("open observation")
163
168
 
164
169
  # check if current observation must be closed to open a new one
165
170
  if self.observationId:
166
-
167
171
  self.hide_data_files()
168
172
  response = dialog.MessageDialog(
169
173
  cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
@@ -195,18 +199,17 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
195
199
  "view" to view observation
196
200
  """
197
201
 
198
- logging.debug(f"load observation")
202
+ logging.debug("load observation")
199
203
 
200
204
  if obs_id not in self.pj[cfg.OBSERVATIONS]:
201
205
  return "Error: Observation not found"
202
206
 
203
- if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] not in [cfg.IMAGES, cfg.LIVE, cfg.MEDIA]:
207
+ if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] not in (cfg.IMAGES, cfg.LIVE, cfg.MEDIA):
204
208
  return f"Error: Observation type {self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]} not found"
205
209
 
206
210
  self.observationId = obs_id
207
211
 
208
212
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
209
-
210
213
  self.image_idx = 0
211
214
  self.images_list = []
212
215
 
@@ -219,7 +222,6 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
219
222
  self.dwEvents.setVisible(True)
220
223
 
221
224
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
222
-
223
225
  if mode == cfg.OBS_START:
224
226
  initialize_new_live_observation(self)
225
227
 
@@ -228,12 +230,12 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
228
230
  self.dwEvents.setVisible(True)
229
231
 
230
232
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
231
-
232
233
  if mode == cfg.OBS_START:
233
234
  if not initialize_new_media_observation(self):
234
- self.observationId = ""
235
- self.twEvents.setRowCount(0)
236
- menu_options.update_menu(self)
235
+ close_observation(self)
236
+ # self.observationId = ""
237
+ # self.twEvents.setRowCount(0)
238
+ # menu_options.update_menu(self)
237
239
  return "Error: loading observation problem"
238
240
 
239
241
  if mode == cfg.VIEW:
@@ -246,7 +248,7 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
246
248
  # title of dock widget “ ”
247
249
  self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation")
248
250
 
249
- logging.debug(f"end load observation")
251
+ logging.debug("end load observation")
250
252
  return ""
251
253
 
252
254
 
@@ -260,9 +262,7 @@ def edit_observation(self):
260
262
  # hide data plot
261
263
  self.hide_data_files()
262
264
  if (
263
- dialog.MessageDialog(
264
- cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
265
- )
265
+ dialog.MessageDialog(cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO))
266
266
  == cfg.NO
267
267
  ):
268
268
  # restore plots
@@ -271,9 +271,7 @@ def edit_observation(self):
271
271
  else:
272
272
  close_observation(self)
273
273
 
274
- _, selected_observations = select_observations.select_observations2(
275
- self, cfg.EDIT, windows_title="Edit observation"
276
- )
274
+ _, selected_observations = select_observations.select_observations2(self, cfg.EDIT, windows_title="Edit observation")
277
275
 
278
276
  if selected_observations:
279
277
  new_observation(self, mode=cfg.EDIT, obsId=selected_observations[0])
@@ -284,9 +282,7 @@ def remove_observations(self):
284
282
  remove observations from project file
285
283
  """
286
284
 
287
- _, selected_observations = select_observations.select_observations2(
288
- self, cfg.MULTIPLE, windows_title="Remove observations"
289
- )
285
+ _, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE, windows_title="Remove observations")
290
286
  if not selected_observations:
291
287
  return
292
288
 
@@ -329,11 +325,7 @@ def coding_time(observations: dict, observations_list: list) -> Tuple[Optional[d
329
325
  observation = observations[obs_id]
330
326
  if observation[cfg.EVENTS]:
331
327
  # check if events contain a NA timestamp
332
- if [
333
- event[cfg.EVENT_TIME_FIELD_IDX]
334
- for event in observation[cfg.EVENTS]
335
- if event[cfg.EVENT_TIME_FIELD_IDX].is_nan()
336
- ]:
328
+ if [event[cfg.EVENT_TIME_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_TIME_FIELD_IDX].is_nan()]:
337
329
  return dec("NaN"), dec("NaN"), dec("NaN")
338
330
  start_coding_list.append(observation[cfg.EVENTS][0][cfg.EVENT_TIME_FIELD_IDX])
339
331
  end_coding_list.append(observation[cfg.EVENTS][-1][cfg.EVENT_TIME_FIELD_IDX])
@@ -364,6 +356,47 @@ def coding_time(observations: dict, observations_list: list) -> Tuple[Optional[d
364
356
  return start_coding, end_coding, coding_duration
365
357
 
366
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
+
367
400
  def observation_total_length(observation: dict) -> dec:
368
401
  """
369
402
  Observation media duration (if any)
@@ -394,7 +427,7 @@ def observation_total_length(observation: dict) -> dec:
394
427
  last_event = obs_length = max(observation[cfg.EVENTS])[cfg.TW_OBS_FIELD[cfg.IMAGES]["time"]]
395
428
  obs_length = last_event - first_event
396
429
  except Exception:
397
- logging.critical(f"Length of observation from images not available")
430
+ logging.critical("Length of observation from images not available")
398
431
  obs_length = dec(-2)
399
432
  else:
400
433
  obs_length = dec(0)
@@ -510,10 +543,7 @@ def observation_length(pj: dict, selected_observations: list) -> tuple:
510
543
  if (
511
544
  dialog.MessageDialog(
512
545
  cfg.programName,
513
- (
514
- f"The observation length is not available (<b>{obs_id}</b>).<br>"
515
- "Use last event time as observation length?"
516
- ),
546
+ (f"The observation length is not available (<b>{obs_id}</b>).<br>Use last event time as observation length?"),
517
547
  (cfg.YES, cfg.NO),
518
548
  )
519
549
  == cfg.YES
@@ -545,7 +575,7 @@ def observation_length(pj: dict, selected_observations: list) -> tuple:
545
575
  return (max_obs_length, selectedObsTotalMediaLength)
546
576
 
547
577
 
548
- def new_observation(self, mode=cfg.NEW, obsId=""):
578
+ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
549
579
  """
550
580
  define a new observation or edit an existing observation
551
581
 
@@ -553,18 +583,18 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
553
583
  mode (str): NEW or EDIT
554
584
  obsId (str): observation Id to be edited
555
585
 
586
+ Retruns:
587
+ None
588
+
556
589
  """
557
590
  # check if current observation must be closed to create a new one
558
591
  if mode == cfg.NEW and self.observationId:
559
592
  # hide data plot
560
593
  self.hide_data_files()
561
594
  if (
562
- dialog.MessageDialog(
563
- cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
564
- )
595
+ dialog.MessageDialog(cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO))
565
596
  == cfg.NO
566
597
  ):
567
-
568
598
  # show data plot
569
599
  self.show_data_files()
570
600
  return
@@ -572,9 +602,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
572
602
  close_observation(self)
573
603
 
574
604
  observationWindow = observation.Observation(
575
- tmp_dir=self.ffmpeg_cache_dir
576
- if (self.ffmpeg_cache_dir and pl.Path(self.ffmpeg_cache_dir).is_dir())
577
- else tempfile.gettempdir(),
605
+ tmp_dir=self.ffmpeg_cache_dir if (self.ffmpeg_cache_dir and pl.Path(self.ffmpeg_cache_dir).is_dir()) else tempfile.gettempdir(),
578
606
  project_path=self.projectFileName,
579
607
  converters=self.pj.get(cfg.CONVERTERS, {}),
580
608
  time_format=self.timeFormat,
@@ -582,21 +610,19 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
582
610
 
583
611
  observationWindow.pj = dict(self.pj)
584
612
  observationWindow.sw_observation_type.setCurrentIndex(0) # no observation type
585
- # observationWindow.sw_observation_type.setVisible(False)
586
613
  observationWindow.mode = mode
587
614
  observationWindow.mem_obs_id = obsId
588
615
  observationWindow.chunk_length = self.chunk_length
589
616
  observationWindow.dteDate.setDateTime(QDateTime.currentDateTime())
617
+ # observationWindow.de_date_offset.setDateTime(QDateTime.currentDateTime())
590
618
  observationWindow.ffmpeg_bin = self.ffmpeg_bin
591
619
  observationWindow.project_file_name = self.projectFileName
592
620
  observationWindow.rb_no_time.setChecked(True)
593
621
 
594
622
  # add independent variables
595
623
  if cfg.INDEPENDENT_VARIABLES in self.pj:
596
-
597
624
  observationWindow.twIndepVariables.setRowCount(0)
598
625
  for i in util.sorted_keys(self.pj[cfg.INDEPENDENT_VARIABLES]):
599
-
600
626
  observationWindow.twIndepVariables.setRowCount(observationWindow.twIndepVariables.rowCount() + 1)
601
627
 
602
628
  # label
@@ -631,22 +657,18 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
631
657
  comboBox = QComboBox()
632
658
  comboBox.addItems(self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(","))
633
659
  if txt in self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(","):
634
- comboBox.setCurrentIndex(
635
- self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(",").index(txt)
636
- )
637
- observationWindow.twIndepVariables.setCellWidget(
638
- observationWindow.twIndepVariables.rowCount() - 1, 2, comboBox
639
- )
660
+ comboBox.setCurrentIndex(self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(",").index(txt))
661
+ observationWindow.twIndepVariables.setCellWidget(observationWindow.twIndepVariables.rowCount() - 1, 2, comboBox)
640
662
 
641
663
  elif self.pj[cfg.INDEPENDENT_VARIABLES][i]["type"] == cfg.TIMESTAMP:
642
664
  cal = QDateTimeEdit()
643
- cal.setDisplayFormat("yyyy-MM-dd hh:mm:ss")
665
+ cal.setDisplayFormat("yyyy-MM-dd hh:mm:ss.zzz")
644
666
  cal.setCalendarPopup(True)
645
- if txt:
646
- cal.setDateTime(QDateTime.fromString(txt, "yyyy-MM-ddThh:mm:ss"))
647
- observationWindow.twIndepVariables.setCellWidget(
648
- observationWindow.twIndepVariables.rowCount() - 1, 2, cal
649
- )
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)
650
672
  else:
651
673
  item.setText(txt)
652
674
  observationWindow.twIndepVariables.setItem(observationWindow.twIndepVariables.rowCount() - 1, 2, item)
@@ -655,28 +677,34 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
655
677
 
656
678
  # adapt time offset for current time format
657
679
  if self.timeFormat == cfg.S:
658
- observationWindow.obs_time_offset.set_format_s()
680
+ observationWindow.obs_time_offset.rb_seconds.setChecked(True)
659
681
  if self.timeFormat == cfg.HHMMSS:
660
- observationWindow.obs_time_offset.set_format_hhmmss()
682
+ # observationWindow.obs_time_offset.set_format_hhmmss()
683
+ observationWindow.obs_time_offset.rb_time.setChecked(True)
661
684
 
662
- if mode == cfg.EDIT:
685
+ observationWindow.obs_time_offset.set_time(0)
663
686
 
687
+ if mode == cfg.EDIT:
664
688
  observationWindow.setWindowTitle(f'Edit observation "{obsId}"')
665
- mem_obs_id = obsId
689
+ """mem_obs_id = obsId"""
666
690
  observationWindow.leObservationId.setText(obsId)
667
691
 
668
692
  # check date format for old versions of BORIS app
669
693
  try:
670
694
  time.strptime(self.pj[cfg.OBSERVATIONS][obsId]["date"], "%Y-%m-%d %H:%M")
671
- self.pj[cfg.OBSERVATIONS][obsId]["date"] = (
672
- self.pj[cfg.OBSERVATIONS][obsId]["date"].replace(" ", "T") + ":00"
673
- )
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")
674
697
  except ValueError:
675
698
  pass
676
699
 
677
- observationWindow.dteDate.setDateTime(
678
- QDateTime.fromString(self.pj[cfg.OBSERVATIONS][obsId]["date"], "yyyy-MM-ddThh:mm:ss")
679
- )
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
+
680
708
  observationWindow.teDescription.setPlainText(self.pj[cfg.OBSERVATIONS][obsId][cfg.DESCRIPTION])
681
709
 
682
710
  try:
@@ -695,17 +723,20 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
695
723
  logging.info("No Video/Audio information")
696
724
 
697
725
  # offset
698
- observationWindow.obs_time_offset.set_time(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_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])
699
733
 
700
734
  if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.MEDIA:
701
735
  observationWindow.rb_media_files.setChecked(True)
702
736
 
703
737
  observationWindow.twVideo1.setRowCount(0)
704
738
  for player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE]:
705
- if (
706
- player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE]
707
- and self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]
708
- ):
739
+ if player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE] and self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]:
709
740
  for mediaFile in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]:
710
741
  observationWindow.twVideo1.setRowCount(observationWindow.twVideo1.rowCount() + 1)
711
742
 
@@ -714,19 +745,15 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
714
745
  combobox.setCurrentIndex(int(player) - 1)
715
746
  observationWindow.twVideo1.setCellWidget(observationWindow.twVideo1.rowCount() - 1, 0, combobox)
716
747
 
717
- # set offset
748
+ # set media file offset
718
749
  try:
719
750
  observationWindow.twVideo1.setItem(
720
751
  observationWindow.twVideo1.rowCount() - 1,
721
752
  1,
722
- QTableWidgetItem(
723
- str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]["offset"][player])
724
- ),
753
+ QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]["offset"][player])),
725
754
  )
726
755
  except Exception:
727
- observationWindow.twVideo1.setItem(
728
- observationWindow.twVideo1.rowCount() - 1, 1, QTableWidgetItem("0.0")
729
- )
756
+ observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 1, QTableWidgetItem("0.0"))
730
757
 
731
758
  item = QTableWidgetItem(mediaFile)
732
759
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
@@ -735,16 +762,12 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
735
762
  # duration and FPS
736
763
  try:
737
764
  item = QTableWidgetItem(
738
- util.seconds2time(
739
- self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile]
740
- )
765
+ util.seconds2time(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile])
741
766
  )
742
767
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
743
768
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 3, item)
744
769
 
745
- item = QTableWidgetItem(
746
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]:.2f}"
747
- )
770
+ item = QTableWidgetItem(f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]:.2f}")
748
771
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
749
772
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 4, item)
750
773
  except Exception:
@@ -752,36 +775,41 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
752
775
 
753
776
  # has_video has_audio
754
777
  try:
755
- item = QTableWidgetItem(
756
- str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_VIDEO][mediaFile])
757
- )
778
+ item = QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_VIDEO][mediaFile]))
758
779
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
759
780
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 5, item)
760
781
 
761
- item = QTableWidgetItem(
762
- str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_AUDIO][mediaFile])
763
- )
782
+ item = QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_AUDIO][mediaFile]))
764
783
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
765
784
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 6, item)
766
785
  except Exception:
767
786
  pass
768
787
 
788
+ observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(observationWindow.twVideo1.rowCount() > 0)
769
789
  # spectrogram
770
790
  observationWindow.cbVisualizeSpectrogram.setEnabled(True)
771
- observationWindow.cbVisualizeSpectrogram.setChecked(
772
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_SPECTROGRAM, False)
773
- )
774
-
791
+ observationWindow.cbVisualizeSpectrogram.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_SPECTROGRAM, False))
775
792
  # waveform
776
793
  observationWindow.cb_visualize_waveform.setEnabled(True)
777
- observationWindow.cb_visualize_waveform.setChecked(
778
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_WAVEFORM, False)
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)
779
803
  )
780
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
+
781
810
  # plot data
782
811
  if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][obsId]:
783
812
  if self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA]:
784
-
785
813
  observationWindow.tw_data_files.setRowCount(0)
786
814
  for idx2 in util.sorted_keys(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA]):
787
815
  observationWindow.tw_data_files.setRowCount(observationWindow.tw_data_files.rowCount() + 1)
@@ -791,9 +819,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
791
819
  combobox.addItems(cfg.DATA_PLOT_STYLES)
792
820
  combobox.setCurrentIndex(
793
821
  cfg.DATA_PLOT_STYLES.index(
794
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
795
- cfg.DATA_PLOT_FIELDS[idx3]
796
- ]
822
+ self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]
797
823
  )
798
824
  )
799
825
 
@@ -807,9 +833,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
807
833
  combobox2.addItems(["False", "True"])
808
834
  combobox2.setCurrentIndex(
809
835
  ["False", "True"].index(
810
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
811
- cfg.DATA_PLOT_FIELDS[idx3]
812
- ]
836
+ self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]
813
837
  )
814
838
  )
815
839
 
@@ -824,11 +848,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
824
848
  observationWindow.tw_data_files.rowCount() - 1,
825
849
  idx3,
826
850
  QTableWidgetItem(
827
- str(
828
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
829
- cfg.DATA_PLOT_FIELDS[idx3]
830
- ]
831
- )
851
+ str(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]])
832
852
  ),
833
853
  )
834
854
 
@@ -836,24 +856,18 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
836
856
  observationWindow.tw_data_files.setItem(
837
857
  observationWindow.tw_data_files.rowCount() - 1,
838
858
  idx3,
839
- QTableWidgetItem(
840
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
841
- cfg.DATA_PLOT_FIELDS[idx3]
842
- ]
843
- ),
859
+ QTableWidgetItem(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]),
844
860
  )
845
861
 
846
862
  if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.IMAGES:
847
863
  observationWindow.rb_images.setChecked(True)
848
- observationWindow.lw_images_directory.addItems(
849
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DIRECTORIES_LIST, [])
850
- )
864
+ observationWindow.lw_images_directory.addItems(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DIRECTORIES_LIST, []))
851
865
  observationWindow.rb_use_exif.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.USE_EXIF_DATE, False))
852
866
  if self.pj[cfg.OBSERVATIONS][obsId].get(cfg.TIME_LAPSE, 0):
853
867
  observationWindow.rb_time_lapse.setChecked(True)
854
868
  observationWindow.sb_time_lapse.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.TIME_LAPSE, 0))
855
869
 
856
- if self.pj[cfg.OBSERVATIONS][obsId]["type"] in [cfg.LIVE]:
870
+ if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.LIVE:
857
871
  observationWindow.rb_live.setChecked(True)
858
872
  # sampling time
859
873
  observationWindow.sbScanSampling.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.SCAN_SAMPLING_TIME, 0))
@@ -863,39 +877,33 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
863
877
  or self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False)
864
878
  )
865
879
  # day/epoch time
866
- observationWindow.rb_day_time.setChecked(
867
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_TIME, False)
868
- )
869
- observationWindow.rb_epoch_time.setChecked(
870
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False)
871
- )
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))
872
882
 
873
883
  # observation time interval
874
884
  observationWindow.cb_observation_time_interval.setEnabled(True)
875
885
  if self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0]) != [0, 0]:
876
886
  observationWindow.cb_observation_time_interval.setChecked(True)
877
- observationWindow.observation_time_interval = self.pj[cfg.OBSERVATIONS][obsId].get(
878
- cfg.OBSERVATION_TIME_INTERVAL, [0, 0]
879
- )
887
+ observationWindow.observation_time_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
880
888
  observationWindow.cb_observation_time_interval.setText(
881
889
  (
882
890
  "Limit observation to a time interval: "
883
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][0]} - "
884
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][1]}"
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]}"
885
893
  )
886
894
  )
887
895
 
888
- # disabled due to problem when video goes back
889
- # if CLOSE_BEHAVIORS_BETWEEN_VIDEOS in self.pj[OBSERVATIONS][obsId]:
890
- # observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setChecked(self.pj[OBSERVATIONS][obsId][CLOSE_BEHAVIORS_BETWEEN_VIDEOS])
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
+ )
891
900
 
892
- rv = observationWindow.exec_()
901
+ rv = observationWindow.exec()
893
902
 
894
903
  # save geometry
895
904
  gui_utilities.save_geometry(observationWindow, "new observation")
896
905
 
897
906
  if rv:
898
-
899
907
  self.project_changed()
900
908
 
901
909
  new_obs_id = observationWindow.leObservationId.text().strip()
@@ -914,14 +922,14 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
914
922
 
915
923
  # check if id changed
916
924
  if mode == cfg.EDIT and new_obs_id != obsId:
917
-
918
925
  logging.info(f"observation id {obsId} changed in {new_obs_id}")
919
926
 
920
927
  self.pj[cfg.OBSERVATIONS][new_obs_id] = dict(self.pj[cfg.OBSERVATIONS][obsId])
921
928
  del self.pj[cfg.OBSERVATIONS][obsId]
922
929
 
923
930
  # observation date
924
- self.pj[cfg.OBSERVATIONS][new_obs_id]["date"] = observationWindow.dteDate.dateTime().toString(Qt.ISODate)
931
+ self.pj[cfg.OBSERVATIONS][new_obs_id]["date"] = observationWindow.dteDate.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
932
+ # observation description
925
933
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DESCRIPTION] = observationWindow.teDescription.toPlainText()
926
934
 
927
935
  # observation type: read project type from radio buttons
@@ -935,43 +943,53 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
935
943
  # independent variables for observation
936
944
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES] = {}
937
945
  for r in range(observationWindow.twIndepVariables.rowCount()):
938
-
939
946
  # set dictionary as label (col 0) => value (col 2)
940
947
  if observationWindow.twIndepVariables.item(r, 1).text() == cfg.SET_OF_VALUES:
941
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][
942
- observationWindow.twIndepVariables.item(r, 0).text()
943
- ] = observationWindow.twIndepVariables.cellWidget(r, 2).currentText()
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
+ )
944
951
  elif observationWindow.twIndepVariables.item(r, 1).text() == cfg.TIMESTAMP:
945
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][
946
- observationWindow.twIndepVariables.item(r, 0).text()
947
- ] = (observationWindow.twIndepVariables.cellWidget(r, 2).dateTime().toString(Qt.ISODate))
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
+ )
948
955
  else:
949
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][
950
- observationWindow.twIndepVariables.item(r, 0).text()
951
- ] = observationWindow.twIndepVariables.item(r, 2).text()
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
+ )
952
959
 
953
960
  # observation time offset
954
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] = observationWindow.obs_time_offset.get_time()
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
955
971
 
956
972
  if observationWindow.cb_observation_time_interval.isChecked():
957
- self.pj[cfg.OBSERVATIONS][new_obs_id][
958
- cfg.OBSERVATION_TIME_INTERVAL
959
- ] = observationWindow.observation_time_interval
973
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.OBSERVATION_TIME_INTERVAL] = observationWindow.observation_time_interval
960
974
 
961
975
  self.display_statusbar_info(new_obs_id)
962
976
 
963
977
  # visualize spectrogram
964
- self.pj[cfg.OBSERVATIONS][new_obs_id][
965
- cfg.VISUALIZE_SPECTROGRAM
966
- ] = observationWindow.cbVisualizeSpectrogram.isChecked()
967
- # visualize spectrogram
968
- self.pj[cfg.OBSERVATIONS][new_obs_id][
969
- cfg.VISUALIZE_WAVEFORM
970
- ] = observationWindow.cb_visualize_waveform.isChecked()
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
+
971
991
  # time interval for observation
972
- self.pj[cfg.OBSERVATIONS][new_obs_id][
973
- cfg.OBSERVATION_TIME_INTERVAL
974
- ] = observationWindow.observation_time_interval
992
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.OBSERVATION_TIME_INTERVAL] = observationWindow.observation_time_interval
975
993
 
976
994
  # plot data
977
995
  if observationWindow.tw_data_files.rowCount():
@@ -980,30 +998,27 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
980
998
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)] = {}
981
999
  for idx2 in cfg.DATA_PLOT_FIELDS:
982
1000
  if idx2 in [cfg.PLOT_DATA_PLOTCOLOR_IDX, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX]:
983
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
984
- cfg.DATA_PLOT_FIELDS[idx2]
985
- ] = observationWindow.tw_data_files.cellWidget(row, idx2).currentText()
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
+ )
986
1004
 
987
1005
  elif idx2 == cfg.PLOT_DATA_CONVERTERS_IDX:
988
1006
  if observationWindow.tw_data_files.item(row, idx2).text():
989
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
990
- cfg.DATA_PLOT_FIELDS[idx2]
991
- ] = eval(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
+ )
992
1010
  else:
993
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
994
- cfg.DATA_PLOT_FIELDS[idx2]
995
- ] = {}
1011
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = {}
996
1012
 
997
1013
  else:
998
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
999
- cfg.DATA_PLOT_FIELDS[idx2]
1000
- ] = observationWindow.tw_data_files.item(row, idx2).text()
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
+ )
1001
1017
 
1002
1018
  # Close current behaviors between video
1003
- # disabled due to problem when video goes back
1004
- # self.pj[OBSERVATIONS][new_obs_id][CLOSE_BEHAVIORS_BETWEEN_VIDEOS] =
1005
- # observationWindow.cbCloseCurrentBehaviorsBetweenVideo.isChecked()
1006
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS] = False
1019
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS] = (
1020
+ observationWindow.cbCloseCurrentBehaviorsBetweenVideo.isChecked()
1021
+ )
1007
1022
 
1008
1023
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.LIVE:
1009
1024
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SCAN_SAMPLING_TIME] = observationWindow.sbScanSampling.value()
@@ -1017,10 +1032,23 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1017
1032
  # images dir
1018
1033
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.IMAGES:
1019
1034
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DIRECTORIES_LIST] = [
1020
- observationWindow.lw_images_directory.item(i).text()
1021
- for i in range(observationWindow.lw_images_directory.count())
1035
+ observationWindow.lw_images_directory.item(i).text() for i in range(observationWindow.lw_images_directory.count())
1022
1036
  ]
1037
+
1038
+ # check if exif data must be used
1023
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
1024
1052
  if observationWindow.rb_time_lapse.isChecked():
1025
1053
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_LAPSE] = observationWindow.sb_time_lapse.value()
1026
1054
  else:
@@ -1031,17 +1059,19 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1031
1059
 
1032
1060
  # media
1033
1061
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.MEDIA:
1034
-
1035
1062
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO] = {
1036
1063
  cfg.LENGTH: observationWindow.mediaDurations,
1037
1064
  cfg.FPS: observationWindow.mediaFPS,
1038
1065
  }
1039
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
+
1040
1070
  try:
1041
1071
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.HAS_VIDEO] = observationWindow.mediaHasVideo
1042
1072
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.HAS_AUDIO] = observationWindow.mediaHasAudio
1043
1073
  except Exception:
1044
- logging.info("error with media_info information")
1074
+ logging.warning("error with media_info information")
1045
1075
 
1046
1076
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]["offset"] = {}
1047
1077
 
@@ -1051,9 +1081,9 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1051
1081
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][str(i + 1)] = []
1052
1082
 
1053
1083
  for row in range(observationWindow.twVideo1.rowCount()):
1054
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][
1055
- observationWindow.twVideo1.cellWidget(row, 0).currentText()
1056
- ].append(observationWindow.twVideo1.item(row, 2).text())
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
+ )
1057
1087
  # store offset for media player
1058
1088
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]["offset"][
1059
1089
  observationWindow.twVideo1.cellWidget(row, 0).currentText()
@@ -1075,11 +1105,11 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1075
1105
 
1076
1106
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
1077
1107
  self.playerType = cfg.MEDIA
1078
- # load events in table widget
1079
- initialize_new_media_observation(self)
1108
+ if not initialize_new_media_observation(self):
1109
+ close_observation(self)
1110
+ return "Observation not launched"
1080
1111
 
1081
1112
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
1082
- # QMessageBox.critical(self, cfg.programName, "Observation from images directory is not yet implemented")
1083
1113
  initialize_new_images_observation(self)
1084
1114
 
1085
1115
  self.load_tw_events(self.observationId)
@@ -1091,10 +1121,10 @@ def close_observation(self):
1091
1121
  close current observation
1092
1122
  """
1093
1123
 
1094
- logging.info(f"Close observation {self.playerType}")
1124
+ logging.info(f"Close observation (player type: {self.playerType})")
1125
+
1126
+ # check observation state events
1095
1127
 
1096
- logging.info(f"Check state events")
1097
- # check observation events
1098
1128
  flag_ok, msg = project_functions.check_state_events_obs(
1099
1129
  self.observationId,
1100
1130
  self.pj[cfg.ETHOGRAM],
@@ -1103,58 +1133,35 @@ def close_observation(self):
1103
1133
  )
1104
1134
 
1105
1135
  if not flag_ok:
1106
-
1107
1136
  out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
1108
- results = dialog.Results_dialog()
1137
+ results = dialog.Results_dialog_exit_code()
1109
1138
  results.setWindowTitle(f"{cfg.programName} - Check selected observations")
1110
1139
  results.ptText.setReadOnly(True)
1111
1140
  results.ptText.appendHtml(out)
1112
- results.pbSave.setVisible(False)
1113
- results.pbCancel.setText("Close observation")
1114
- results.pbCancel.setVisible(True)
1115
- results.pbOK.setText("Fix unpaired state events")
1116
-
1117
- if results.exec_(): # fix events
1118
-
1119
- w = dialog.Ask_time(self.timeFormat)
1120
- w.setWindowTitle("Fix UNPAIRED state events")
1121
- w.label.setText("Fix UNPAIRED events at time")
1122
-
1123
- if w.exec_():
1124
- fix_at_time = w.time_widget.get_time()
1125
- events_to_add = project_functions.fix_unpaired_state_events(
1126
- self.pj[cfg.ETHOGRAM],
1127
- self.pj[cfg.OBSERVATIONS][self.observationId],
1128
- fix_at_time - dec("0.001"),
1129
- )
1130
- if events_to_add:
1131
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
1132
- self.project_changed()
1133
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
1134
-
1135
- self.load_tw_events(self.observationId)
1136
- item = self.twEvents.item(
1137
- [
1138
- i
1139
- for i, t in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
1140
- if t[0] == fix_at_time
1141
- ][0],
1142
- 0,
1143
- )
1144
- self.twEvents.scrollToItem(item)
1145
- return
1146
- else:
1147
- return
1148
1141
 
1149
- logging.info(f"Check state events done")
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)
1150
1154
 
1151
1155
  self.saved_state = self.saveState()
1152
1156
 
1153
1157
  if self.playerType == cfg.MEDIA:
1154
-
1155
- logging.info(f"Stop plot timer")
1158
+ self.media_scan_sampling_mem = []
1159
+ logging.info("Stop plot timer")
1156
1160
  self.plot_timer.stop()
1157
1161
 
1162
+ if self.MPV_IPC_MODE:
1163
+ self.main_window_activation_timer.stop()
1164
+
1158
1165
  for i, player in enumerate(self.dw_player):
1159
1166
  if (
1160
1167
  str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
@@ -1163,6 +1170,16 @@ def close_observation(self):
1163
1170
  logging.info(f"Stop player #{i + 1}")
1164
1171
  player.player.stop()
1165
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
+
1166
1183
  self.verticalLayout_3.removeWidget(self.video_slider)
1167
1184
 
1168
1185
  if self.video_slider is not None:
@@ -1177,21 +1194,22 @@ def close_observation(self):
1177
1194
  self.liveObservationStarted = False
1178
1195
  self.liveStartTime = None
1179
1196
 
1180
- if (
1181
- cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId]
1182
- and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]
1183
- ):
1197
+ if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId] and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
1184
1198
  for x in self.ext_data_timer_list:
1185
1199
  x.stop()
1186
1200
  for pd in self.plot_data:
1187
1201
  self.plot_data[pd].close_plot()
1188
1202
 
1189
- logging.info(f"close tool window")
1203
+ logging.info("close tool window")
1190
1204
 
1191
1205
  self.close_tool_windows()
1192
1206
 
1193
1207
  self.observationId = ""
1194
1208
 
1209
+ # delete undo queue
1210
+ self.undo_queue = deque()
1211
+ self.undo_description = deque()
1212
+
1195
1213
  if self.playerType in (cfg.MEDIA, cfg.IMAGES):
1196
1214
  """
1197
1215
  for idx, _ in enumerate(self.dw_player):
@@ -1202,10 +1220,11 @@ def close_observation(self):
1202
1220
  """
1203
1221
 
1204
1222
  for dw in self.dw_player:
1205
-
1206
- logging.info(f"remove dock widget")
1207
-
1223
+ logging.info("remove dock widget")
1224
+ dw.player.log_handler = None
1208
1225
  self.removeDockWidget(dw)
1226
+
1227
+ del dw
1209
1228
  # sip.delete(dw)
1210
1229
  # dw = None
1211
1230
 
@@ -1217,10 +1236,12 @@ def close_observation(self):
1217
1236
 
1218
1237
  self.w_obs_info.setVisible(False)
1219
1238
 
1220
- self.twEvents.setRowCount(0)
1239
+ # self.twEvents.setRowCount(0)
1221
1240
 
1222
1241
  self.lb_current_media_time.clear()
1223
1242
  self.lb_player_status.clear()
1243
+ self.lb_video_info.clear()
1244
+ self.lb_zoom_level.clear()
1224
1245
 
1225
1246
  self.currentSubject = ""
1226
1247
  self.lbFocalSubject.setText(cfg.NO_FOCAL_SUBJECT)
@@ -1229,7 +1250,7 @@ def close_observation(self):
1229
1250
  for i in range(self.twSubjects.rowCount()):
1230
1251
  self.twSubjects.item(i, len(cfg.subjectsFields)).setText("")
1231
1252
 
1232
- for w in [self.lbTimeOffset, self.lbSpeed, self.lb_obs_time_interval]:
1253
+ for w in (self.lbTimeOffset, self.lb_obs_time_interval):
1233
1254
  w.clear()
1234
1255
  self.play_rate, self.playerType = 1, ""
1235
1256
 
@@ -1238,6 +1259,70 @@ def close_observation(self):
1238
1259
  logging.info(f"Observation {self.playerType} closed")
1239
1260
 
1240
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
+
1241
1326
  def initialize_new_media_observation(self) -> bool:
1242
1327
  """
1243
1328
  initialize new observation from media file(s)
@@ -1245,12 +1330,10 @@ def initialize_new_media_observation(self) -> bool:
1245
1330
 
1246
1331
  logging.debug("function: initialize new observation for media file(s)")
1247
1332
 
1248
- for dw in [self.dwEthogram, self.dwSubjects, self.dwEvents]:
1333
+ for dw in (self.dwEthogram, self.dwSubjects, self.dwEvents):
1249
1334
  dw.setVisible(True)
1250
1335
 
1251
- ok, msg = project_functions.check_if_media_available(
1252
- self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName
1253
- )
1336
+ ok, msg = project_functions.check_if_media_available(self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName)
1254
1337
 
1255
1338
  if not ok:
1256
1339
  QMessageBox.critical(
@@ -1278,6 +1361,8 @@ def initialize_new_media_observation(self) -> bool:
1278
1361
  font = QFont()
1279
1362
  font.setPointSize(15)
1280
1363
  self.lb_current_media_time.setFont(font)
1364
+ self.lb_video_info.setFont(font)
1365
+ self.lb_zoom_level.setFont(font)
1281
1366
 
1282
1367
  # initialize video slider
1283
1368
  self.video_slider = QSlider(Qt.Horizontal, self)
@@ -1290,8 +1375,19 @@ def initialize_new_media_observation(self) -> bool:
1290
1375
  # add all media files to media lists
1291
1376
  self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)
1292
1377
  self.dw_player = []
1293
- # create dock widgets for players
1294
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
1295
1391
  for i in range(cfg.N_PLAYER):
1296
1392
  n_player = str(i + 1)
1297
1393
  if (
@@ -1300,17 +1396,424 @@ def initialize_new_media_observation(self) -> bool:
1300
1396
  ):
1301
1397
  continue
1302
1398
 
1399
+ # Not pretty but the unique solution I have found to capture the click signal for each player
1400
+
1303
1401
  if i == 0: # first player
1304
- p = player_dock_widget.DW_player(i, self)
1305
- self.dw_player.append(p)
1402
+ p0 = player_dock_widget.DW_player(0, self)
1306
1403
 
1307
- @p.player.property_observer("time-pos")
1308
- def time_observer(_name, value):
1309
- if value is not None:
1310
- self.time_observer_signal.emit(value)
1404
+ if not self.MPV_IPC_MODE:
1311
1405
 
1312
- else:
1313
- self.dw_player.append(player_dock_widget.DW_player(i, self))
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)
1314
1817
 
1315
1818
  self.dw_player[-1].setFloating(False)
1316
1819
  self.dw_player[-1].setVisible(False)
@@ -1330,12 +1833,11 @@ def initialize_new_media_observation(self) -> bool:
1330
1833
  # for receiving event from volume slider
1331
1834
  self.dw_player[i].volume_slider_moved_signal.connect(self.set_volume)
1332
1835
 
1836
+ # for receiving event from mute toolbutton
1837
+ self.dw_player[i].mute_action_triggered_signal.connect(self.set_mute)
1838
+
1333
1839
  # for receiving resize event from dock widget
1334
1840
  self.dw_player[i].resize_signal.connect(self.resize_dw)
1335
- """
1336
- # for receiving event resize and clicked (Zoom - crop)
1337
- self.dw_player[i].view_signal.connect(self.signal_from_dw)
1338
- """
1339
1841
 
1340
1842
  # add durations list
1341
1843
  self.dw_player[i].media_durations = []
@@ -1344,8 +1846,21 @@ def initialize_new_media_observation(self) -> bool:
1344
1846
  # add fps list
1345
1847
  self.dw_player[i].fps = {}
1346
1848
 
1347
- for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
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
1348
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.start()
1861
+
1862
+
1863
+ for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
1349
1864
  logging.debug(f"media file: {mediaFile}")
1350
1865
 
1351
1866
  media_full_path = project_functions.full_path(mediaFile, self.projectFileName)
@@ -1354,13 +1869,10 @@ def initialize_new_media_observation(self) -> bool:
1354
1869
 
1355
1870
  # media duration
1356
1871
  try:
1357
- mediaLength = (
1358
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile] * 1000
1359
- )
1872
+ mediaLength = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile] * 1000
1360
1873
  mediaFPS = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]
1361
1874
  except Exception:
1362
-
1363
- logging.debug("media_info key not found")
1875
+ logging.debug("media_info key not found in project")
1364
1876
 
1365
1877
  r = util.accurate_media_analysis(self.ffmpeg_bin, media_full_path)
1366
1878
  if "error" not in r:
@@ -1383,58 +1895,77 @@ def initialize_new_media_observation(self) -> bool:
1383
1895
  self.project_changed()
1384
1896
 
1385
1897
  self.dw_player[i].media_durations.append(int(mediaLength))
1386
- self.dw_player[i].cumul_media_durations.append(
1387
- self.dw_player[i].cumul_media_durations[-1] + int(mediaLength)
1388
- )
1898
+ self.dw_player[i].cumul_media_durations.append(self.dw_player[i].cumul_media_durations[-1] + int(mediaLength))
1389
1899
 
1390
1900
  self.dw_player[i].fps[mediaFile] = mediaFPS
1391
1901
 
1902
+ # add media file to playlist
1392
1903
  self.dw_player[i].player.playlist_append(media_full_path)
1393
- # self.dw_player[i].player.loadfile(media_full_path)
1394
- # self.dw_player[i].player.pause = True
1395
1904
 
1396
- # check if BORIS is running on a Windows VM with WMIC COMPUTERSYSTEM GET SERIALNUMBER
1905
+ # add media file name to player window title
1906
+ self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({pl.Path(media_full_path).name})")
1907
+
1908
+ # media duration cumuled in seconds
1909
+ self.dw_player[i].cumul_media_durations_sec = [round(dec(x / 1000), 3) for x in self.dw_player[i].cumul_media_durations]
1910
+
1911
+ # check if BORIS is running on a Windows VM with the 'WMIC COMPUTERSYSTEM GET SERIALNUMBER' command
1397
1912
  # because "auto" or "auto-safe" crash in Windows VM
1398
1913
  # see https://superuser.com/questions/1128339/how-can-i-detect-if-im-within-a-vm-or-not
1399
1914
 
1400
- flag_vm = False
1401
- if sys.platform.startswith("win"):
1402
- p = subprocess.Popen(
1403
- ["WMIC", "BIOS", "GET", "SERIALNUMBER"],
1404
- stdout=subprocess.PIPE,
1405
- stderr=subprocess.PIPE,
1406
- shell=True,
1407
- )
1408
- out, _ = p.communicate()
1409
- flag_vm = b"SerialNumber \r\r\n0 " in out
1410
- logging.debug(f"Running on Windows VM: {flag_vm}")
1915
+ if not self.MPV_IPC_MODE:
1916
+ flag_vm = False
1917
+ if sys.platform.startswith("win"):
1918
+ p = subprocess.Popen(
1919
+ ["WMIC", "BIOS", "GET", "SERIALNUMBER"],
1920
+ stdout=subprocess.PIPE,
1921
+ stderr=subprocess.PIPE,
1922
+ shell=True,
1923
+ )
1924
+ out, _ = p.communicate()
1925
+ flag_vm = b"SerialNumber \r\r\n0 " in out
1926
+ logging.debug(f"Running on Windows VM: {flag_vm}")
1411
1927
 
1412
- if not flag_vm:
1413
- self.dw_player[i].player.hwdec = self.config_param.get(cfg.MPV_HWDEC, cfg.MPV_HWDEC_DEFAULT_VALUE)
1414
- else:
1415
- self.dw_player[i].player.hwdec = "no"
1928
+ if not flag_vm:
1929
+ self.dw_player[i].player.hwdec = self.config_param.get(cfg.MPV_HWDEC, cfg.MPV_HWDEC_DEFAULT_VALUE)
1930
+ else:
1931
+ self.dw_player[i].player.hwdec = cfg.MPV_HWDEC_NO
1932
+
1933
+ logging.debug(f"Player hwdec of player #{i} set to: {self.dw_player[i].player.hwdec}")
1934
+ self.config_param[cfg.MPV_HWDEC] = self.dw_player[i].player.hwdec
1416
1935
 
1417
1936
  self.dw_player[i].player.playlist_pos = 0
1418
1937
  self.dw_player[i].player.wait_until_playing()
1419
1938
  self.dw_player[i].player.pause = True
1420
- self.dw_player[i].player.wait_until_paused()
1939
+ time.sleep(0.2)
1940
+ # self.dw_player[i].player.wait_until_paused()
1421
1941
  self.dw_player[i].player.seek(0, "absolute")
1422
1942
  # do not close when playing finished
1423
1943
  self.dw_player[i].player.keep_open = True
1424
1944
  self.dw_player[i].player.keep_open_pause = False
1425
1945
 
1946
+ self.dw_player[i].player.image_display_duration = self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.IMAGE_DISPLAY_DURATION, 1)
1947
+
1426
1948
  # position media
1427
- if cfg.OBSERVATION_TIME_INTERVAL in self.pj[cfg.OBSERVATIONS][self.observationId]:
1428
- self.seek_mediaplayer(
1429
- int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.OBSERVATION_TIME_INTERVAL][0]), player=i
1430
- )
1949
+ self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]), player=i)
1431
1950
 
1432
- # restore zoom level
1951
+ # restore video zoom level
1433
1952
  if cfg.ZOOM_LEVEL in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1434
1953
  self.dw_player[i].player.video_zoom = log2(
1435
1954
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.ZOOM_LEVEL].get(n_player, 0)
1436
1955
  )
1437
1956
 
1957
+ # restore video pan
1958
+ if cfg.PAN_X in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1959
+ self.dw_player[i].player.video_pan_x = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.PAN_X].get(n_player, 0)
1960
+ if cfg.PAN_Y in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1961
+ self.dw_player[i].player.video_pan_y = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.PAN_Y].get(n_player, 0)
1962
+
1963
+ # restore rotation angle
1964
+ if cfg.ROTATION_ANGLE in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1965
+ self.dw_player[i].player.video_rotate = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.ROTATION_ANGLE].get(
1966
+ n_player, 0
1967
+ )
1968
+
1438
1969
  # restore subtitle visibility
1439
1970
  if cfg.DISPLAY_MEDIA_SUBTITLES in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1440
1971
  self.dw_player[i].player.sub_visibility = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][
@@ -1449,9 +1980,20 @@ def initialize_new_media_observation(self) -> bool:
1449
1980
 
1450
1981
  menu_options.update_menu(self)
1451
1982
 
1452
- self.time_observer_signal.connect(self.mpv_timer_out)
1983
+ if self.MPV_IPC_MODE:
1984
+ # activate timer
1985
+ self.ipc_mpv_timer = QTimer()
1986
+ self.ipc_mpv_timer.setInterval(500)
1987
+ self.ipc_mpv_timer.timeout.connect(self.mpv_timer_out)
1988
+
1989
+ else:
1990
+ self.ipc_mpv_timer = None
1991
+ self.time_observer_signal.connect(self.mpv_timer_out)
1992
+
1993
+ self.mpv_eof_reached_signal.connect(self.mpv_eof_reached)
1994
+ self.video_click_signal.connect(self.player_clicked)
1453
1995
 
1454
- self.actionPlay.setIcon(QIcon(":/play"))
1996
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
1455
1997
 
1456
1998
  self.display_statusbar_info(self.observationId)
1457
1999
 
@@ -1460,25 +2002,17 @@ def initialize_new_media_observation(self) -> bool:
1460
2002
  self.state_behaviors_codes = tuple(util.state_behavior_codes(self.pj[cfg.ETHOGRAM]))
1461
2003
 
1462
2004
  video_operations.display_play_rate(self)
1463
- """self.lbSpeed.setText(f"Player rate: <b>x{self.play_rate:.3f}</b>")"""
2005
+ video_operations.display_zoom_level(self)
1464
2006
 
1465
2007
  # spectrogram
1466
2008
  if (
1467
2009
  cfg.VISUALIZE_SPECTROGRAM in self.pj[cfg.OBSERVATIONS][self.observationId]
1468
2010
  and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.VISUALIZE_SPECTROGRAM]
1469
2011
  ):
1470
-
1471
- tmp_dir = (
1472
- self.ffmpeg_cache_dir
1473
- if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir)
1474
- else tempfile.gettempdir()
1475
- )
2012
+ tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
1476
2013
 
1477
2014
  wav_file_path = (
1478
- pl.Path(tmp_dir)
1479
- / pl.Path(
1480
- self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav"
1481
- ).name
2015
+ pl.Path(tmp_dir) / pl.Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
1482
2016
  )
1483
2017
 
1484
2018
  if not wav_file_path.is_file():
@@ -1491,18 +2025,10 @@ def initialize_new_media_observation(self) -> bool:
1491
2025
  cfg.VISUALIZE_WAVEFORM in self.pj[cfg.OBSERVATIONS][self.observationId]
1492
2026
  and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.VISUALIZE_WAVEFORM]
1493
2027
  ):
1494
-
1495
- tmp_dir = (
1496
- self.ffmpeg_cache_dir
1497
- if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir)
1498
- else tempfile.gettempdir()
1499
- )
2028
+ tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
1500
2029
 
1501
2030
  wav_file_path = (
1502
- pl.Path(tmp_dir)
1503
- / pl.Path(
1504
- self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav"
1505
- ).name
2031
+ pl.Path(tmp_dir) / pl.Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
1506
2032
  )
1507
2033
 
1508
2034
  if not wav_file_path.is_file():
@@ -1511,11 +2037,7 @@ def initialize_new_media_observation(self) -> bool:
1511
2037
  self.show_plot_widget("waveform", warning=False)
1512
2038
 
1513
2039
  # external data plot
1514
- if (
1515
- cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId]
1516
- and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]
1517
- ):
1518
-
2040
+ if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId] and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
1519
2041
  self.plot_data = {}
1520
2042
  self.ext_data_timer_list = []
1521
2043
  count = 0
@@ -1530,9 +2052,7 @@ def initialize_new_media_observation(self) -> bool:
1530
2052
  QMessageBox.critical(
1531
2053
  self,
1532
2054
  cfg.programName,
1533
- "Data file not found:\n{}".format(
1534
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]
1535
- ),
2055
+ "Data file not found:\n{}".format(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]),
1536
2056
  )
1537
2057
  data_ok = False
1538
2058
  # return False
@@ -1556,7 +2076,8 @@ def initialize_new_media_observation(self) -> bool:
1556
2076
  self,
1557
2077
  cfg.programName,
1558
2078
  (
1559
- f"Impossible to plot data from file {os.path.basename(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]['file_path'])}:\n"
2079
+ "Impossible to plot data from file "
2080
+ f"{os.path.basename(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]['file_path'])}:\n"
1560
2081
  f"{w1.error_msg}"
1561
2082
  ),
1562
2083
  )
@@ -1587,9 +2108,7 @@ def initialize_new_media_observation(self) -> bool:
1587
2108
  QMessageBox.critical(
1588
2109
  self,
1589
2110
  cfg.programName,
1590
- "Data file not found:\n{}".format(
1591
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]
1592
- ),
2111
+ "Data file not found:\n{}".format(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]),
1593
2112
  )
1594
2113
  data_ok = False
1595
2114
  # return False
@@ -1654,12 +2173,24 @@ def initialize_new_media_observation(self) -> bool:
1654
2173
  for player in self.dw_player:
1655
2174
  player.setVisible(True)
1656
2175
 
2176
+ self.load_tw_events(self.observationId)
2177
+
1657
2178
  # initial synchro
1658
- for n_player in range(1, len(self.dw_player)):
1659
- self.sync_time(n_player, 0)
2179
+ if not self.MPV_IPC_MODE:
2180
+ for n_player in range(1, len(self.dw_player)):
2181
+ self.sync_time(n_player, 0)
1660
2182
 
1661
2183
  self.mpv_timer_out(value=0.0)
1662
2184
 
2185
+ """
2186
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO].get(cfg.OVERLAY, {}):
2187
+ for i in range(cfg.N_PLAYER):
2188
+ # restore overlays
2189
+ if str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY]:
2190
+ self.overlays[i] = self.dw_player[i].player.create_image_overlay()
2191
+ self.resize_dw(i)
2192
+ """
2193
+
1663
2194
  return True
1664
2195
 
1665
2196
 
@@ -1682,6 +2213,7 @@ def initialize_new_live_observation(self):
1682
2213
 
1683
2214
  # button start enabled
1684
2215
  self.pb_live_obs.setEnabled(True)
2216
+
1685
2217
  self.w_live.setVisible(True)
1686
2218
  self.w_obs_info.setVisible(True)
1687
2219
 
@@ -1691,9 +2223,9 @@ def initialize_new_live_observation(self):
1691
2223
  self.pb_live_obs.setText("Start live observation")
1692
2224
 
1693
2225
  if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.START_FROM_CURRENT_TIME, False):
1694
- current_time = util.seconds_of_day(datetime.datetime.now())
2226
+ current_time = util.seconds_of_day(dt.datetime.now())
1695
2227
  elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False):
1696
- current_time = time.mktime(datetime.datetime.now().timetuple())
2228
+ current_time = time.mktime(dt.datetime.now().timetuple())
1697
2229
  else:
1698
2230
  current_time = 0
1699
2231
 
@@ -1712,6 +2244,8 @@ def initialize_new_live_observation(self):
1712
2244
  self.liveStartTime = None
1713
2245
  self.liveTimer.stop()
1714
2246
 
2247
+ self.load_tw_events(self.observationId)
2248
+
1715
2249
  self.get_events_current_row()
1716
2250
 
1717
2251
 
@@ -1720,16 +2254,14 @@ def initialize_new_images_observation(self):
1720
2254
  initialize a new observation from directories of images
1721
2255
  """
1722
2256
 
1723
- for dw in [self.dwEthogram, self.dwSubjects, self.dwEvents]:
2257
+ for dw in (self.dwEthogram, self.dwSubjects, self.dwEvents):
1724
2258
  dw.setVisible(True)
1725
2259
  # disable start live button
1726
2260
  self.pb_live_obs.setEnabled(False)
1727
2261
  self.w_live.setVisible(False)
1728
2262
 
1729
2263
  # check if directories are available
1730
- ok, msg = project_functions.check_directories_availability(
1731
- self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName
1732
- )
2264
+ ok, msg = project_functions.check_directories_availability(self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName)
1733
2265
 
1734
2266
  if not ok:
1735
2267
  QMessageBox.critical(
@@ -1749,7 +2281,8 @@ def initialize_new_images_observation(self):
1749
2281
  # count number of images in all directories
1750
2282
  tot_images_number = 0
1751
2283
  for dir_path in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
1752
- result = util.dir_images_number(dir_path)
2284
+ full_dir_path = project_functions.full_path(dir_path, self.projectFileName)
2285
+ result = util.dir_images_number(full_dir_path)
1753
2286
  tot_images_number += result.get("number of images", 0)
1754
2287
 
1755
2288
  if not tot_images_number:
@@ -1757,7 +2290,7 @@ def initialize_new_images_observation(self):
1757
2290
  self,
1758
2291
  cfg.programName,
1759
2292
  (
1760
- f"No images were found in directory(ies).<br><br>The observation will be opened in VIEW mode.<br>"
2293
+ "No images were found in directory(ies).<br><br>The observation will be opened in VIEW mode.<br>"
1761
2294
  "It will not be possible to log events.<br>"
1762
2295
  "Modify the directoriy path(s) to point existing directory "
1763
2296
  ),
@@ -1771,15 +2304,16 @@ def initialize_new_images_observation(self):
1771
2304
  # load image paths
1772
2305
  # directories user order is maintained
1773
2306
  # images are sorted inside each directory
1774
- self.images_list = []
2307
+ self.images_list: list = []
1775
2308
  for dir_path in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
2309
+ full_dir_path = project_functions.full_path(dir_path, self.projectFileName)
1776
2310
  for pattern in cfg.IMAGE_EXTENSIONS:
1777
2311
  self.images_list.extend(
1778
2312
  sorted(
1779
2313
  list(
1780
2314
  set(
1781
- [str(x) for x in pl.Path(dir_path).glob(pattern)]
1782
- + [str(x) for x in pl.Path(dir_path).glob(pattern.upper())]
2315
+ [str(x) for x in pl.Path(full_dir_path).glob(pattern)]
2316
+ + [str(x) for x in pl.Path(full_dir_path).glob(pattern.upper())]
1783
2317
  )
1784
2318
  )
1785
2319
  )
@@ -1834,11 +2368,6 @@ def initialize_new_images_observation(self):
1834
2368
  self.saved_state = self.saveState()
1835
2369
  self.restoreState(self.saved_state)
1836
2370
 
1837
- """
1838
- self.twEvents.setColumnCount(len(cfg.IMAGES_TW_EVENTS_FIELDS))
1839
- self.twEvents.setHorizontalHeaderLabels(cfg.IMAGES_TW_EVENTS_FIELDS)
1840
- """
1841
-
1842
2371
  self.extract_frame(self.dw_player[i])
1843
2372
  self.w_obs_info.setVisible(True)
1844
2373
 
@@ -1849,57 +2378,160 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
1849
2378
  """
1850
2379
  returns the media file name corresponding to the event (start time in case of state event)
1851
2380
 
2381
+ Args:
2382
+ observation (dict): observation
2383
+ timestamp (dec): time stamp
2384
+
1852
2385
  Returns:
1853
- str: name of media file containing the event
2386
+ str: path of media file containing the event
1854
2387
  """
2388
+ if observation.get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
2389
+ # media creation date/time was used for coding
2390
+ video_file_name = None
2391
+ for media_path in observation[cfg.MEDIA_INFO].get(cfg.MEDIA_CREATION_TIME, {}):
2392
+ start_media = observation[cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_path]
2393
+ duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_path]
2394
+ if start_media <= timestamp <= start_media + duration:
2395
+ video_file_name = media_path
2396
+ break
1855
2397
 
1856
- cumul_media_durations = [0]
1857
- for media_file in observation[cfg.FILE]["1"]:
1858
- media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
1859
- cumul_media_durations.append(cumul_media_durations[-1] + media_duration)
2398
+ else: # no media creation date
2399
+ cumul_media_durations: list = [dec(0)]
2400
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2401
+ try:
2402
+ media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
2403
+ # cut off media duration to 3 decimal places as that is how fine the player is
2404
+ media_duration = floor(media_duration * 10**3) / dec(10**3)
2405
+ cumul_media_durations.append(floor((cumul_media_durations[-1] + media_duration) * 10**3) / dec(10**3))
2406
+ except KeyError:
2407
+ return None
1860
2408
 
1861
- cumul_media_durations.remove(0)
2409
+ """
2410
+ cumul_media_durations: list = [dec(0)]
2411
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2412
+ try:
2413
+ media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2414
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2415
+ except KeyError:
2416
+ return None
2417
+ """
1862
2418
 
1863
- # test if timestamp is at end of last media
1864
- if timestamp == cumul_media_durations[-1]:
1865
- player_idx = len(observation[cfg.FILE]["1"]) - 1
1866
- else:
1867
- player_idx = -1
1868
- for idx, value in enumerate(cumul_media_durations):
1869
- start = 0 if idx == 0 else cumul_media_durations[idx - 1]
1870
- if start <= timestamp < value:
1871
- player_idx = idx
1872
- break
2419
+ cumul_media_durations.remove(dec(0))
1873
2420
 
1874
- if player_idx != -1:
1875
- video_file_name = observation[cfg.FILE]["1"][player_idx]
1876
- else:
1877
- video_file_name = None
2421
+ logging.debug(f"{cumul_media_durations=}")
2422
+
2423
+ # test if timestamp is at end of last media
2424
+ if timestamp == cumul_media_durations[-1]:
2425
+ player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2426
+ else:
2427
+ player_idx = None
2428
+ for idx, value in enumerate(cumul_media_durations):
2429
+ start = 0 if idx == 0 else cumul_media_durations[idx - 1]
2430
+ if start <= timestamp < value:
2431
+ player_idx = idx
2432
+ break
1878
2433
 
2434
+ video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx] if player_idx is not None else None
2435
+
2436
+ return video_file_name
2437
+
2438
+
2439
+ def create_observations(self):
1879
2440
  """
1880
- player_idx_list = [
1881
- idx for idx, x in enumerate(cumul_media_durations) if cumul_media_durations[idx - 1] <= timestamp < x
1882
- ]
2441
+ Create observations from a media file directory
2442
+ """
2443
+ # print(self.pj[cfg.OBSERVATIONS])
1883
2444
 
1884
- try:
1885
- print(f"{player_idx_list=}")
1886
- except:
1887
- pass
2445
+ dir_path = QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))
2446
+ if not dir_path:
2447
+ return
1888
2448
 
1889
- if len(player_idx_list):
1890
- player_idx = player_idx_list[0] - 1
2449
+ dlg = dialog.Input_dialog(
2450
+ label_caption="Set the following observation parameters",
2451
+ elements_list=[
2452
+ ("cb", "Recurse the subdirectories", False),
2453
+ ("cb", "Save the absolute media file path", True),
2454
+ ("cb", "Visualize spectrogram", False),
2455
+ ("cb", "Visualize waveform", False),
2456
+ ("cb", "Media creation date as offset", False),
2457
+ ("cb", "Close behaviors between videos", False),
2458
+ ("dsb", "Time offset (in seconds)", 0.0, 86400, 1, 0, 3),
2459
+ ("dsb", "Media scan sampling duration (in seconds)", 0.0, 86400, 1, 0, 3),
2460
+ ],
2461
+ title="Observation parameters",
2462
+ )
2463
+ if not dlg.exec_():
2464
+ return
1891
2465
 
1892
- try:
1893
- print(f"{player_idx=}")
1894
- except:
1895
- pass
2466
+ file_count: int = 0
1896
2467
 
1897
- video_file_name = observation[cfg.FILE]["1"][player_idx]
2468
+ if dlg.elements["Recurse the subdirectories"].isChecked():
2469
+ files_list = pl.Path(dir_path).rglob("*")
1898
2470
  else:
1899
- player_idx = -1
1900
- video_file_name = None
2471
+ files_list = pl.Path(dir_path).glob("*")
1901
2472
 
1902
- print()
1903
- """
2473
+ for file in files_list:
2474
+ if not file.is_file():
2475
+ continue
2476
+ r = util.accurate_media_analysis(ffmpeg_bin=self.ffmpeg_bin, file_name=file)
2477
+ if "error" not in r:
2478
+ if not r.get("frames_number", 0):
2479
+ continue
1904
2480
 
1905
- return video_file_name
2481
+ if dlg.elements["Save the absolute media file path"].isChecked():
2482
+ media_file = str(file)
2483
+ else:
2484
+ try:
2485
+ media_file = str(file.relative_to(pl.Path(self.projectFileName).parent))
2486
+ except ValueError:
2487
+ QMessageBox.critical(
2488
+ self,
2489
+ cfg.programName,
2490
+ (
2491
+ f"the media file <b>{file}</b> can not be relative to the project directory "
2492
+ f"(<b>{pl.Path(self.projectFileName).parent}</b>)"
2493
+ "<br><br>Aborting the creation of observations"
2494
+ ),
2495
+ )
2496
+ return
2497
+
2498
+ if media_file in self.pj[cfg.OBSERVATIONS]:
2499
+ QMessageBox.critical(
2500
+ self,
2501
+ cfg.programName,
2502
+ (f"The observation <b>{media_file}</b> already exists.<br><br>Aborting the creation of observations"),
2503
+ )
2504
+ return
2505
+
2506
+ self.pj[cfg.OBSERVATIONS][media_file] = {
2507
+ "file": {"1": [media_file], "2": [], "3": [], "4": [], "5": [], "6": [], "7": [], "8": []},
2508
+ "type": "MEDIA",
2509
+ "date": dt.datetime.now().replace(microsecond=0).isoformat(),
2510
+ "description": "",
2511
+ "time offset": dec(str(round(dlg.elements["Time offset (in seconds)"].value(), 3))),
2512
+ "events": [],
2513
+ "observation time interval": [0, 0],
2514
+ "independent_variables": {},
2515
+ "visualize_spectrogram": dlg.elements["Visualize spectrogram"].isChecked(),
2516
+ "visualize_waveform": dlg.elements["Visualize waveform"].isChecked(),
2517
+ "media_creation_date_as_offset": dlg.elements["Media creation date as offset"].isChecked(),
2518
+ "media_scan_sampling_duration": dec(str(round(dlg.elements["Media scan sampling duration (in seconds)"].value(), 3))),
2519
+ "image_display_duration": 1,
2520
+ "close_behaviors_between_videos": dlg.elements["Close behaviors between videos"].isChecked(),
2521
+ "media_info": {
2522
+ "length": {media_file: r["duration"]},
2523
+ "fps": {media_file: r["duration"]},
2524
+ "hasVideo": {media_file: r["has_video"]},
2525
+ "hasAudio": {media_file: r["has_audio"]},
2526
+ "offset": {"1": 0.0},
2527
+ },
2528
+ }
2529
+ file_count += 1
2530
+ self.project_changed()
2531
+
2532
+ if file_count:
2533
+ message: str = f"{file_count} observation(s) were created" if file_count > 1 else "One observation was created"
2534
+ else:
2535
+ message: str = f"No media file were found in {dir_path}"
2536
+
2537
+ QMessageBox.information(self, cfg.programName, message)