boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.2__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 (125) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +24 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +235 -131
  6. boris/advanced_event_filtering.py +23 -29
  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 +16 -34
  23. boris/config.py +108 -49
  24. boris/config_file.py +58 -67
  25. boris/connections.py +105 -58
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2106 -1277
  30. boris/core_qrc.py +15892 -10829
  31. boris/core_ui.py +941 -806
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +134 -0
  34. boris/dialog.py +461 -242
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +405 -281
  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 +180 -203
  43. boris/export_observation.py +60 -73
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +427 -218
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +304 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +16 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv2.py +127 -36
  58. boris/observation.py +493 -210
  59. boris/observation_operations.py +1010 -391
  60. boris/observation_ui.py +573 -363
  61. boris/observations_list.py +51 -58
  62. boris/otx_parser.py +74 -68
  63. boris/param_panel.py +45 -59
  64. boris/param_panel_ui.py +254 -138
  65. boris/player_dock_widget.py +91 -56
  66. boris/plot_data_module.py +18 -53
  67. boris/plot_events.py +56 -153
  68. boris/plot_events_rt.py +16 -30
  69. boris/plot_spectrogram_rt.py +80 -56
  70. boris/plot_waveform_rt.py +23 -48
  71. boris/plugins.py +431 -0
  72. boris/portion/__init__.py +18 -8
  73. boris/portion/const.py +35 -18
  74. boris/portion/dict.py +5 -5
  75. boris/portion/func.py +2 -2
  76. boris/portion/interval.py +21 -41
  77. boris/portion/io.py +41 -32
  78. boris/preferences.py +304 -123
  79. boris/preferences_ui.py +684 -227
  80. boris/project.py +293 -270
  81. boris/project_functions.py +618 -537
  82. boris/project_import_export.py +204 -213
  83. boris/project_ui.py +673 -441
  84. boris/qrc_boris.py +6 -3
  85. boris/qrc_boris5.py +6 -3
  86. boris/select_modifiers.py +62 -90
  87. boris/select_observations.py +19 -197
  88. boris/select_subj_behav.py +67 -39
  89. boris/state_events.py +51 -33
  90. boris/subjects_pad.py +6 -8
  91. boris/synthetic_time_budget.py +25 -17
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +71 -86
  94. boris/transitions.py +41 -41
  95. boris/utilities.py +562 -222
  96. boris/version.py +3 -3
  97. boris/video_equalizer.py +16 -14
  98. boris/video_equalizer_ui.py +199 -130
  99. boris/video_operations.py +78 -28
  100. boris/view_df.py +104 -0
  101. boris/view_df_ui.py +75 -0
  102. boris/write_event.py +240 -136
  103. boris_behav_obs-9.7.2.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.2.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.2.dist-info/entry_points.txt +2 -0
  107. boris/README.TXT +0 -22
  108. boris/add_modifier.ui +0 -323
  109. boris/converters.ui +0 -289
  110. boris/core.qrc +0 -37
  111. boris/core.ui +0 -1571
  112. boris/edit_event.ui +0 -233
  113. boris/icons/logo_eye.ico +0 -0
  114. boris/map_creator.py +0 -982
  115. boris/observation.ui +0 -814
  116. boris/param_panel.ui +0 -379
  117. boris/preferences.ui +0 -537
  118. boris/project.ui +0 -1074
  119. boris/vlc_local.py +0 -90
  120. boris_behav_obs-8.16.6.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
  123. boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.2.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.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)
@@ -186,7 +180,6 @@ class Plot_data(QWidget):
186
180
 
187
181
  # check if sampling rate is not constant
188
182
  if len(diff) != 1:
189
-
190
183
  """logging.debug("len diff != 1")"""
191
184
 
192
185
  min_time_step = min(diff)
@@ -197,9 +190,7 @@ class Plot_data(QWidget):
197
190
  if min_time_step > 1:
198
191
  min_time_step = 1
199
192
 
200
- x2 = np.arange(
201
- min_time_value, max_time_value + min_time_step, min_time_step
202
- )
193
+ x2 = np.arange(min_time_value, max_time_value + min_time_step, min_time_step)
203
194
  data = np.array((x2, np.interp(x2, data[:, 0], data[:, 1]))).T
204
195
  del x2
205
196
 
@@ -294,10 +285,7 @@ class Plot_data(QWidget):
294
285
  self.close()
295
286
 
296
287
  # 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
-
288
+ def plot(self, x, y, position_data, position_start, min_value, max_value, position_end):
301
289
  # print current value
302
290
  try:
303
291
  if x[0] == 0:
@@ -314,9 +302,7 @@ class Plot_data(QWidget):
314
302
  self.myplot.axes.set_ylabel(self.y_label, rotation=90, labelpad=10)
315
303
  self.myplot.axes.set_ylim((min_value, max_value))
316
304
  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
- )
305
+ self.myplot.axes.axvline(x=position_data, color=cfg.REALTIME_PLOT_CURSOR_COLOR, linestyle="-")
320
306
 
321
307
  self.myplot.draw()
322
308
  except Exception:
@@ -324,7 +310,7 @@ class Plot_data(QWidget):
324
310
 
325
311
 
326
312
  class Plotter(QObject):
327
- return_fig = pyqtSignal(
313
+ return_fig = Signal(
328
314
  np.ndarray, # x array
329
315
  np.ndarray, # y array
330
316
  float, # position_data
@@ -334,14 +320,11 @@ class Plotter(QObject):
334
320
  float, # position end
335
321
  )
336
322
 
337
- @pyqtSlot(float)
323
+ @Slot(float)
338
324
  def replot(self, current_time): # time_ in s
339
-
340
325
  logging.debug("current_time: {}".format(current_time))
341
326
 
342
- current_discrete_time = round(
343
- round(current_time / self.min_time_step) * self.min_time_step, 2
344
- )
327
+ current_discrete_time = round(round(current_time / self.min_time_step) * self.min_time_step, 2)
345
328
 
346
329
  logging.debug("current_discrete_time: {}".format(current_discrete_time))
347
330
  logging.debug("self.interval: {}".format(self.interval))
@@ -349,17 +332,13 @@ class Plotter(QObject):
349
332
  freq_interval = int(round(self.interval / self.min_time_step))
350
333
 
351
334
  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
- )
335
+ logging.debug("self.min_time_value <= current_discrete_time <= self.max_time_value")
356
336
 
357
337
  idx = np.where(self.data[:, 0] == current_discrete_time)[0]
358
338
  if not len(idx):
359
339
  idx = np.where(abs(self.data[:, 0] - current_discrete_time) <= 0.02)[0]
360
340
 
361
341
  if len(idx):
362
-
363
342
  position_data = idx[0]
364
343
 
365
344
  logging.debug(f"position data: {position_data}")
@@ -396,28 +375,17 @@ class Plotter(QObject):
396
375
  d = np.array([np.nan] * int(self.interval / self.min_time_step)).T
397
376
 
398
377
  elif current_time > self.max_time_value:
378
+ logging.debug(f"self.interval/self.min_time_step/2: {self.interval / self.min_time_step / 2}")
399
379
 
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
- )
380
+ dim_footer = int(round((current_time - self.max_time_value) / self.min_time_step + self.interval / self.min_time_step / 2))
410
381
 
411
382
  footer = np.array([np.nan] * dim_footer).T
412
383
  logging.debug(f"len footer: {len(footer)}")
413
384
 
414
- a = (
415
- self.interval / 2 - (current_time - self.max_time_value)
416
- ) / self.min_time_step
385
+ a = (self.interval / 2 - (current_time - self.max_time_value)) / self.min_time_step
417
386
  logging.debug(f"a: {a}")
418
387
 
419
388
  if a >= 0:
420
-
421
389
  logging.debug("a>=0")
422
390
 
423
391
  st = int(round(len(self.data) - a))
@@ -443,7 +411,6 @@ class Plotter(QObject):
443
411
  logging.debug(f"len d a<0: {len(d)}")
444
412
 
445
413
  elif current_time < self.min_time_value:
446
-
447
414
  x = (self.min_time_value - current_time) / self.min_time_step
448
415
  dim_header = int(round(self.interval / self.min_time_step / 2 + x))
449
416
  header = np.array([np.nan] * dim_header).T
@@ -453,9 +420,7 @@ class Plotter(QObject):
453
420
  if b >= 0:
454
421
  d = np.append(header, self.data[0:b][:, 1], axis=0)
455
422
  if len(d) < freq_interval:
456
- d = np.append(
457
- d, np.array([np.nan] * int(freq_interval - len(d))).T, axis=0
458
- )
423
+ d = np.append(d, np.array([np.nan] * int(freq_interval - len(d))).T, axis=0)
459
424
 
460
425
  else:
461
426
  d = np.array([np.nan] * int(self.interval / self.min_time_step)).T