boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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.
Files changed (126) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +266 -144
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_export_to_feral.py +225 -0
  9. boris/analysis_plugins/_latency.py +59 -0
  10. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  11. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  13. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  14. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  15. boris/analysis_plugins/number_of_occurences.py +22 -0
  16. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  17. boris/analysis_plugins/time_budget.py +61 -0
  18. boris/behav_coding_map_creator.py +235 -236
  19. boris/behavior_binary_table.py +33 -50
  20. boris/behaviors_coding_map.py +17 -18
  21. boris/boris_cli.py +6 -25
  22. boris/cmd_arguments.py +12 -1
  23. boris/coding_pad.py +19 -36
  24. boris/config.py +109 -50
  25. boris/config_file.py +58 -67
  26. boris/connections.py +105 -58
  27. boris/converters.py +13 -37
  28. boris/converters_ui.py +187 -110
  29. boris/cooccurence.py +250 -0
  30. boris/core.py +2174 -1303
  31. boris/core_qrc.py +15892 -10829
  32. boris/core_ui.py +941 -806
  33. boris/db_functions.py +17 -42
  34. boris/dev.py +27 -7
  35. boris/dialog.py +461 -242
  36. boris/duration_widget.py +9 -14
  37. boris/edit_event.py +61 -31
  38. boris/edit_event_ui.py +208 -97
  39. boris/event_operations.py +405 -281
  40. boris/events_cursor.py +25 -17
  41. boris/events_snapshots.py +36 -82
  42. boris/exclusion_matrix.py +4 -9
  43. boris/export_events.py +180 -203
  44. boris/export_observation.py +60 -73
  45. boris/external_processes.py +123 -98
  46. boris/geometric_measurement.py +427 -218
  47. boris/gui_utilities.py +91 -14
  48. boris/image_overlay.py +4 -4
  49. boris/import_observations.py +190 -98
  50. boris/ipc_mpv.py +325 -0
  51. boris/irr.py +20 -57
  52. boris/latency.py +31 -24
  53. boris/measurement_widget.py +14 -18
  54. boris/media_file.py +17 -19
  55. boris/menu_options.py +16 -6
  56. boris/modifier_coding_map_creator.py +1013 -0
  57. boris/modifiers_coding_map.py +7 -9
  58. boris/mpv2.py +128 -35
  59. boris/observation.py +501 -211
  60. boris/observation_operations.py +1037 -393
  61. boris/observation_ui.py +573 -363
  62. boris/observations_list.py +51 -58
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +45 -59
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +91 -56
  67. boris/plot_data_module.py +20 -53
  68. boris/plot_events.py +56 -153
  69. boris/plot_events_rt.py +16 -30
  70. boris/plot_spectrogram_rt.py +83 -56
  71. boris/plot_waveform_rt.py +27 -49
  72. boris/plugins.py +468 -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 +307 -123
  80. boris/preferences_ui.py +686 -227
  81. boris/project.py +294 -271
  82. boris/project_functions.py +626 -537
  83. boris/project_import_export.py +204 -213
  84. boris/project_ui.py +673 -441
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +62 -90
  88. boris/select_observations.py +19 -197
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +51 -33
  91. boris/subjects_pad.py +7 -9
  92. boris/synthetic_time_budget.py +42 -26
  93. boris/time_budget_functions.py +169 -169
  94. boris/time_budget_widget.py +77 -89
  95. boris/transitions.py +41 -41
  96. boris/utilities.py +594 -226
  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 +86 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +240 -136
  104. boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
  106. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.12.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -37
  112. boris/core.ui +0 -1571
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -982
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1074
  120. boris/vlc_local.py +0 -90
  121. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  122. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  123. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  124. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  125. {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
  126. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.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 file is part of BORIS.
7
7
 
@@ -20,40 +20,31 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- # add misc directory to search path for mpv-1.dll
24
- import os
25
23
  import sys
26
24
  import logging
27
- import pathlib as pl
28
- import datetime as dt
25
+ import functools
29
26
 
30
- os.environ["PATH"] = os.path.dirname(__file__) + os.sep + "misc" + os.pathsep + os.environ["PATH"]
31
-
32
- try:
27
+ if (sys.platform.startswith("win") or sys.platform.startswith("linux")) and ("-i" not in sys.argv) and ("--ipc" not in sys.argv):
33
28
  from . import mpv2 as mpv
34
-
35
- # check if MPV API v. 1
36
- # is v. 1 use the old version of mpv.py
37
- try:
38
- if "libmpv.so.1" in mpv.sofile:
39
- from . import mpv as mpv
40
- except AttributeError:
41
- if "mpv-1.dll" in mpv.dll:
42
- from . import mpv as mpv
43
-
44
- except RuntimeError: # libmpv found but version too old
45
- from . import mpv as mpv
46
-
47
- except OSError: # libmpv not found
48
- msg = "LIBMPV library not found!\n"
49
- logging.critical(msg)
50
- sys.exit()
51
-
52
-
53
- from PyQt5.QtWidgets import QLabel, QDockWidget, QWidget, QHBoxLayout, QSlider, QSizePolicy, QStackedWidget
54
- from PyQt5.QtCore import pyqtSignal, QEvent, Qt
55
-
56
- import logging
29
+ else:
30
+ import ipc_mpv
31
+ import config as cfg
32
+ import gui_utilities
33
+
34
+
35
+ from PySide6.QtWidgets import (
36
+ QLabel,
37
+ QDockWidget,
38
+ QWidget,
39
+ QHBoxLayout,
40
+ QVBoxLayout,
41
+ QSlider,
42
+ QSizePolicy,
43
+ QStackedWidget,
44
+ QToolButton,
45
+ )
46
+ from PySide6.QtCore import Signal, QEvent, Qt
47
+ from PySide6.QtGui import QIcon, QAction
57
48
 
58
49
 
59
50
  class Clickable_label(QLabel):
@@ -62,7 +53,7 @@ class Clickable_label(QLabel):
62
53
  Label emits a signal when clicked
63
54
  """
64
55
 
65
- mouse_pressed_signal = pyqtSignal(int, QEvent)
56
+ mouse_pressed_signal = Signal(int, QEvent)
66
57
 
67
58
  def __init__(self, id_, parent=None):
68
59
  QLabel.__init__(self, parent)
@@ -72,22 +63,40 @@ class Clickable_label(QLabel):
72
63
  """
73
64
  label clicked
74
65
  """
75
- """logging.debug(f"mousepress event: label {self.id_} clicked")"""
66
+ logging.debug(f"mousepress event: label {self.id_} clicked {event.pos()}")
67
+
68
+ """
69
+ super().mousePressEvent(event)
70
+ x, y = event.pos().x(), event.pos().y()
71
+ draw_on_pixmap(self, x, y) # Example usage
72
+ """
76
73
 
77
74
  self.mouse_pressed_signal.emit(self.id_, event)
78
75
 
79
76
 
77
+ def mpv_logger(player_id, loglevel, component, message):
78
+ """
79
+ redirect MPV log messages to general logging system
80
+ """
81
+
82
+ logging.debug(f"MPV player #{player_id}: [{loglevel}] {component}: {message}")
83
+
84
+
80
85
  class DW_player(QDockWidget):
86
+ """
87
+ Define the player class
88
+ """
81
89
 
82
- key_pressed_signal = pyqtSignal(QEvent)
83
- volume_slider_moved_signal = pyqtSignal(int, int)
84
- view_signal = pyqtSignal(int, str, int)
85
- resize_signal = pyqtSignal(int)
90
+ key_pressed_signal = Signal(QEvent)
91
+ volume_slider_moved_signal = Signal(int, int)
92
+ mute_action_triggered_signal = Signal(int)
93
+ view_signal = Signal(int, str, int)
94
+ resize_signal = Signal(int)
86
95
 
87
96
  def __init__(self, id_, parent=None):
88
97
  super().__init__(parent)
98
+ # print("ipc", parent.MPV_IPC_MODE)
89
99
  self.id_ = id_
90
- self.zoomed = False
91
100
  self.setWindowTitle(f"Player #{id_ + 1}")
92
101
  self.setObjectName(f"player{id_ + 1}")
93
102
 
@@ -96,23 +105,46 @@ class DW_player(QDockWidget):
96
105
 
97
106
  self.videoframe = QWidget(self)
98
107
 
99
- self.player = mpv.MPV(
100
- wid=str(int(self.videoframe.winId())),
101
- # vo='x11', # You may not need this
102
- log_handler=None,
103
- loglevel="debug",
104
- )
105
-
106
- self.player.screenshot_format = "png"
108
+ if parent.MPV_IPC_MODE:
109
+ self.player = ipc_mpv.IPC_MPV(socket_path=f"{cfg.MPV_SOCKET}{self.id_}")
110
+ else:
111
+ self.player = mpv.MPV(
112
+ wid=str(int(self.videoframe.winId())),
113
+ vo="x11" if sys.platform.startswith("linux") else "",
114
+ log_handler=functools.partial(mpv_logger, self.id_),
115
+ loglevel="debug",
116
+ )
117
+
118
+ self.player.screenshot_format = "png"
107
119
  self.hlayout.addWidget(self.videoframe)
108
120
 
121
+ # layout volume slider and mute button
122
+ self.vlayout = QVBoxLayout()
123
+
124
+ # volume slider
109
125
  self.volume_slider = QSlider(Qt.Vertical, self)
110
126
  self.volume_slider.setFocusPolicy(Qt.NoFocus)
111
127
  self.volume_slider.setMaximum(100)
112
128
  self.volume_slider.setValue(50)
113
129
  self.volume_slider.sliderMoved.connect(self.volume_slider_moved)
114
130
 
115
- self.hlayout.addWidget(self.volume_slider)
131
+ self.vlayout.addWidget(self.volume_slider)
132
+
133
+ # mute button
134
+ self.mute_button = QToolButton()
135
+ self.mute_button.setFocusPolicy(Qt.NoFocus)
136
+ self.mute_button.setAutoRaise(True)
137
+ self.mute_action = QAction()
138
+
139
+ theme_mode = gui_utilities.theme_mode()
140
+
141
+ self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
142
+ self.mute_action.triggered.connect(self.mute_action_triggered)
143
+ self.mute_button.setDefaultAction(self.mute_action)
144
+
145
+ self.vlayout.addWidget(self.mute_button)
146
+
147
+ self.hlayout.addLayout(self.vlayout)
116
148
 
117
149
  self.stack1.setLayout(self.hlayout)
118
150
 
@@ -141,21 +173,24 @@ class DW_player(QDockWidget):
141
173
  """
142
174
  self.volume_slider_moved_signal.emit(self.id_, self.volume_slider.value())
143
175
 
144
- def keyPressEvent(self, event):
176
+ def mute_action_triggered(self):
145
177
  """
146
- emit signal when key pressed on dock widget
178
+ emit signal when mute action is triggered
147
179
  """
148
- self.key_pressed_signal.emit(event)
180
+ theme_mode = gui_utilities.theme_mode()
181
+ if self.player.mute:
182
+ self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
183
+ else:
184
+ self.mute_action.setIcon(QIcon(f":/volume_off_{theme_mode}"))
185
+ self.mute_action_triggered_signal.emit(self.id_)
149
186
 
150
- '''
151
- def view_signal_triggered(self, msg, button):
187
+ def keyPressEvent(self, event):
152
188
  """
153
- transmit signal received by video frame
189
+ emit signal when key pressed on dock widget
154
190
  """
155
- self.view_signal.emit(self.id_, msg, button)
156
- '''
191
+ self.key_pressed_signal.emit(event)
157
192
 
158
- def resizeEvent(self, dummy):
193
+ def resizeEvent(self, _):
159
194
  """
160
195
  emits signal when dockwidget resized
161
196
  """
boris/plot_data_module.py CHANGED
@@ -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
 
7
7
  This program is free software; you can redistribute it and/or modify
@@ -29,8 +29,8 @@ from decimal import Decimal as dec
29
29
  import numpy as np
30
30
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
31
31
  from matplotlib.figure import Figure
32
- from PyQt5.QtCore import pyqtSignal, QEvent, QThread, QObject, pyqtSlot, QTimer
33
- from PyQt5.QtWidgets import (
32
+ from PySide6.QtCore import Signal, QEvent, QThread, QObject, Slot, QTimer
33
+ from PySide6.QtWidgets import (
34
34
  QSizePolicy,
35
35
  QWidget,
36
36
  QPushButton,
@@ -56,11 +56,10 @@ class MyMplCanvas(FigureCanvas):
56
56
 
57
57
 
58
58
  class Plot_data(QWidget):
59
-
60
- send_fig = pyqtSignal(float)
59
+ send_fig = Signal(float)
61
60
 
62
61
  # send keypress event to mainwindow
63
- sendEvent = pyqtSignal(QEvent)
62
+ sendEvent = Signal(QEvent)
64
63
 
65
64
  def __init__(
66
65
  self,
@@ -76,7 +75,6 @@ class Plot_data(QWidget):
76
75
  column_converter,
77
76
  log_level="",
78
77
  ):
79
-
80
78
  super().__init__()
81
79
 
82
80
  self.installEventFilter(self)
@@ -103,17 +101,13 @@ class Plot_data(QWidget):
103
101
  self.hlayout1.addWidget(QLabel("Zoom"))
104
102
  self.hlayout1.addWidget(self.button_plus)
105
103
  self.hlayout1.addWidget(self.button_minus)
106
- self.hlayout1.addItem(
107
- QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
108
- )
104
+ self.hlayout1.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
109
105
 
110
106
  self.hlayout2 = QHBoxLayout()
111
107
  self.hlayout2.addWidget(QLabel("Value"))
112
108
  self.lb_value = QLabel("")
113
109
  self.hlayout2.addWidget(self.lb_value)
114
- self.hlayout2.addItem(
115
- QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
116
- )
110
+ self.hlayout2.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
117
111
 
118
112
  self.layout.addLayout(self.hlayout1)
119
113
  self.layout.addLayout(self.hlayout2)
@@ -140,6 +134,8 @@ class Plot_data(QWidget):
140
134
  column_converter=column_converter,
141
135
  )
142
136
 
137
+ print(f"{error_msg=}")
138
+
143
139
  if not result:
144
140
  self.error_msg = error_msg
145
141
  return
@@ -186,7 +182,6 @@ class Plot_data(QWidget):
186
182
 
187
183
  # check if sampling rate is not constant
188
184
  if len(diff) != 1:
189
-
190
185
  """logging.debug("len diff != 1")"""
191
186
 
192
187
  min_time_step = min(diff)
@@ -197,9 +192,7 @@ class Plot_data(QWidget):
197
192
  if min_time_step > 1:
198
193
  min_time_step = 1
199
194
 
200
- x2 = np.arange(
201
- min_time_value, max_time_value + min_time_step, min_time_step
202
- )
195
+ x2 = np.arange(min_time_value, max_time_value + min_time_step, min_time_step)
203
196
  data = np.array((x2, np.interp(x2, data[:, 0], data[:, 1]))).T
204
197
  del x2
205
198
 
@@ -294,10 +287,7 @@ class Plot_data(QWidget):
294
287
  self.close()
295
288
 
296
289
  # Slot receives data and plots it
297
- def plot(
298
- self, x, y, position_data, position_start, min_value, max_value, position_end
299
- ):
300
-
290
+ def plot(self, x, y, position_data, position_start, min_value, max_value, position_end):
301
291
  # print current value
302
292
  try:
303
293
  if x[0] == 0:
@@ -314,9 +304,7 @@ class Plot_data(QWidget):
314
304
  self.myplot.axes.set_ylabel(self.y_label, rotation=90, labelpad=10)
315
305
  self.myplot.axes.set_ylim((min_value, max_value))
316
306
  self.myplot.axes.plot(x, y, self.plot_style)
317
- self.myplot.axes.axvline(
318
- x=position_data, color=cfg.REALTIME_PLOT_CURSOR_COLOR, linestyle="-"
319
- )
307
+ self.myplot.axes.axvline(x=position_data, color=cfg.REALTIME_PLOT_CURSOR_COLOR, linestyle="-")
320
308
 
321
309
  self.myplot.draw()
322
310
  except Exception:
@@ -324,7 +312,7 @@ class Plot_data(QWidget):
324
312
 
325
313
 
326
314
  class Plotter(QObject):
327
- return_fig = pyqtSignal(
315
+ return_fig = Signal(
328
316
  np.ndarray, # x array
329
317
  np.ndarray, # y array
330
318
  float, # position_data
@@ -334,14 +322,11 @@ class Plotter(QObject):
334
322
  float, # position end
335
323
  )
336
324
 
337
- @pyqtSlot(float)
325
+ @Slot(float)
338
326
  def replot(self, current_time): # time_ in s
339
-
340
327
  logging.debug("current_time: {}".format(current_time))
341
328
 
342
- current_discrete_time = round(
343
- round(current_time / self.min_time_step) * self.min_time_step, 2
344
- )
329
+ current_discrete_time = round(round(current_time / self.min_time_step) * self.min_time_step, 2)
345
330
 
346
331
  logging.debug("current_discrete_time: {}".format(current_discrete_time))
347
332
  logging.debug("self.interval: {}".format(self.interval))
@@ -349,17 +334,13 @@ class Plotter(QObject):
349
334
  freq_interval = int(round(self.interval / self.min_time_step))
350
335
 
351
336
  if self.min_time_value <= current_discrete_time <= self.max_time_value:
352
-
353
- logging.debug(
354
- "self.min_time_value <= current_discrete_time <= self.max_time_value"
355
- )
337
+ logging.debug("self.min_time_value <= current_discrete_time <= self.max_time_value")
356
338
 
357
339
  idx = np.where(self.data[:, 0] == current_discrete_time)[0]
358
340
  if not len(idx):
359
341
  idx = np.where(abs(self.data[:, 0] - current_discrete_time) <= 0.02)[0]
360
342
 
361
343
  if len(idx):
362
-
363
344
  position_data = idx[0]
364
345
 
365
346
  logging.debug(f"position data: {position_data}")
@@ -396,28 +377,17 @@ class Plotter(QObject):
396
377
  d = np.array([np.nan] * int(self.interval / self.min_time_step)).T
397
378
 
398
379
  elif current_time > self.max_time_value:
380
+ logging.debug(f"self.interval/self.min_time_step/2: {self.interval / self.min_time_step / 2}")
399
381
 
400
- logging.debug(
401
- f"self.interval/self.min_time_step/2: {self.interval / self.min_time_step / 2}"
402
- )
403
-
404
- dim_footer = int(
405
- round(
406
- (current_time - self.max_time_value) / self.min_time_step
407
- + self.interval / self.min_time_step / 2
408
- )
409
- )
382
+ dim_footer = int(round((current_time - self.max_time_value) / self.min_time_step + self.interval / self.min_time_step / 2))
410
383
 
411
384
  footer = np.array([np.nan] * dim_footer).T
412
385
  logging.debug(f"len footer: {len(footer)}")
413
386
 
414
- a = (
415
- self.interval / 2 - (current_time - self.max_time_value)
416
- ) / self.min_time_step
387
+ a = (self.interval / 2 - (current_time - self.max_time_value)) / self.min_time_step
417
388
  logging.debug(f"a: {a}")
418
389
 
419
390
  if a >= 0:
420
-
421
391
  logging.debug("a>=0")
422
392
 
423
393
  st = int(round(len(self.data) - a))
@@ -443,7 +413,6 @@ class Plotter(QObject):
443
413
  logging.debug(f"len d a<0: {len(d)}")
444
414
 
445
415
  elif current_time < self.min_time_value:
446
-
447
416
  x = (self.min_time_value - current_time) / self.min_time_step
448
417
  dim_header = int(round(self.interval / self.min_time_step / 2 + x))
449
418
  header = np.array([np.nan] * dim_header).T
@@ -453,9 +422,7 @@ class Plotter(QObject):
453
422
  if b >= 0:
454
423
  d = np.append(header, self.data[0:b][:, 1], axis=0)
455
424
  if len(d) < freq_interval:
456
- d = np.append(
457
- d, np.array([np.nan] * int(freq_interval - len(d))).T, axis=0
458
- )
425
+ d = np.append(d, np.array([np.nan] * int(freq_interval - len(d))).T, axis=0)
459
426
 
460
427
  else:
461
428
  d = np.array([np.nan] * int(self.interval / self.min_time_step)).T