boris-behav-obs 8.12__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 (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -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 +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  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 +408 -293
  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 +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  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 +325 -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 +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 +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  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 +25 -33
  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 +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  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 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  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 +627 -236
  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 +95 -29
  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.12.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/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.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 file is part of BORIS.
7
7
 
@@ -20,44 +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
- try:
37
- if "libmpv.so.1" in mpv.sofile:
38
- from . import mpv as mpv
39
- except AttributeError:
40
- if "mpv-1.dll" in mpv.dll:
41
- from . import mpv as mpv
42
-
43
- except RuntimeError: # libmpv found but version too old
44
- from . import mpv as mpv
45
-
46
- except OSError: # libmpv not found
47
- msg = "LIBMPV library not found!\n"
48
- logging.critical(msg)
49
- # append to boris.log file
50
- with open(pl.Path("~").expanduser() / "boris.log", "a") as f_out:
51
- f_out.write(f"{dt.datetime.now():%Y-%m-%d %H:%M}\n")
52
- f_out.write(msg)
53
- f_out.write("-" * 80 + "\n")
54
- sys.exit()
55
-
56
-
57
- from PyQt5.QtWidgets import QLabel, QDockWidget, QWidget, QHBoxLayout, QSlider, QSizePolicy, QStackedWidget
58
- from PyQt5.QtCore import pyqtSignal, QEvent, Qt
59
-
60
- 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
61
48
 
62
49
 
63
50
  class Clickable_label(QLabel):
@@ -66,7 +53,7 @@ class Clickable_label(QLabel):
66
53
  Label emits a signal when clicked
67
54
  """
68
55
 
69
- mouse_pressed_signal = pyqtSignal(int, QEvent)
56
+ mouse_pressed_signal = Signal(int, QEvent)
70
57
 
71
58
  def __init__(self, id_, parent=None):
72
59
  QLabel.__init__(self, parent)
@@ -76,23 +63,40 @@ class Clickable_label(QLabel):
76
63
  """
77
64
  label clicked
78
65
  """
66
+ logging.debug(f"mousepress event: label {self.id_} clicked {event.pos()}")
79
67
 
80
- logging.debug(f"mousepress event: label {self.id_} clicked")
68
+ """
69
+ super().mousePressEvent(event)
70
+ x, y = event.pos().x(), event.pos().y()
71
+ draw_on_pixmap(self, x, y) # Example usage
72
+ """
81
73
 
82
74
  self.mouse_pressed_signal.emit(self.id_, event)
83
75
 
84
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
+
85
85
  class DW_player(QDockWidget):
86
+ """
87
+ Define the player class
88
+ """
86
89
 
87
- key_pressed_signal = pyqtSignal(QEvent)
88
- volume_slider_moved_signal = pyqtSignal(int, int)
89
- view_signal = pyqtSignal(int, str, int)
90
- 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)
91
95
 
92
96
  def __init__(self, id_, parent=None):
93
97
  super().__init__(parent)
98
+ # print("ipc", parent.MPV_IPC_MODE)
94
99
  self.id_ = id_
95
- self.zoomed = False
96
100
  self.setWindowTitle(f"Player #{id_ + 1}")
97
101
  self.setObjectName(f"player{id_ + 1}")
98
102
 
@@ -101,23 +105,46 @@ class DW_player(QDockWidget):
101
105
 
102
106
  self.videoframe = QWidget(self)
103
107
 
104
- self.player = mpv.MPV(
105
- wid=str(int(self.videoframe.winId())),
106
- # vo='x11', # You may not need this
107
- log_handler=None,
108
- loglevel="debug",
109
- )
110
-
111
- 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"
112
119
  self.hlayout.addWidget(self.videoframe)
113
120
 
121
+ # layout volume slider and mute button
122
+ self.vlayout = QVBoxLayout()
123
+
124
+ # volume slider
114
125
  self.volume_slider = QSlider(Qt.Vertical, self)
115
126
  self.volume_slider.setFocusPolicy(Qt.NoFocus)
116
127
  self.volume_slider.setMaximum(100)
117
128
  self.volume_slider.setValue(50)
118
129
  self.volume_slider.sliderMoved.connect(self.volume_slider_moved)
119
130
 
120
- 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)
121
148
 
122
149
  self.stack1.setLayout(self.hlayout)
123
150
 
@@ -146,21 +173,24 @@ class DW_player(QDockWidget):
146
173
  """
147
174
  self.volume_slider_moved_signal.emit(self.id_, self.volume_slider.value())
148
175
 
149
- def keyPressEvent(self, event):
176
+ def mute_action_triggered(self):
150
177
  """
151
- emit signal when key pressed on dock widget
178
+ emit signal when mute action is triggered
152
179
  """
153
- 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_)
154
186
 
155
- '''
156
- def view_signal_triggered(self, msg, button):
187
+ def keyPressEvent(self, event):
157
188
  """
158
- transmit signal received by video frame
189
+ emit signal when key pressed on dock widget
159
190
  """
160
- self.view_signal.emit(self.id_, msg, button)
161
- '''
191
+ self.key_pressed_signal.emit(event)
162
192
 
163
- def resizeEvent(self, dummy):
193
+ def resizeEvent(self, _):
164
194
  """
165
195
  emits signal when dockwidget resized
166
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,
@@ -42,6 +42,7 @@ from PyQt5.QtWidgets import (
42
42
  )
43
43
 
44
44
  from . import utilities as util
45
+ from . import config as cfg
45
46
 
46
47
 
47
48
  class MyMplCanvas(FigureCanvas):
@@ -55,11 +56,10 @@ class MyMplCanvas(FigureCanvas):
55
56
 
56
57
 
57
58
  class Plot_data(QWidget):
58
-
59
- send_fig = pyqtSignal(float)
59
+ send_fig = Signal(float)
60
60
 
61
61
  # send keypress event to mainwindow
62
- sendEvent = pyqtSignal(QEvent)
62
+ sendEvent = Signal(QEvent)
63
63
 
64
64
  def __init__(
65
65
  self,
@@ -75,7 +75,6 @@ class Plot_data(QWidget):
75
75
  column_converter,
76
76
  log_level="",
77
77
  ):
78
-
79
78
  super().__init__()
80
79
 
81
80
  self.installEventFilter(self)
@@ -135,6 +134,8 @@ class Plot_data(QWidget):
135
134
  column_converter=column_converter,
136
135
  )
137
136
 
137
+ print(f"{error_msg=}")
138
+
138
139
  if not result:
139
140
  self.error_msg = error_msg
140
141
  return
@@ -181,7 +182,6 @@ class Plot_data(QWidget):
181
182
 
182
183
  # check if sampling rate is not constant
183
184
  if len(diff) != 1:
184
-
185
185
  """logging.debug("len diff != 1")"""
186
186
 
187
187
  min_time_step = min(diff)
@@ -288,19 +288,13 @@ class Plot_data(QWidget):
288
288
 
289
289
  # Slot receives data and plots it
290
290
  def plot(self, x, y, position_data, position_start, min_value, max_value, position_end):
291
-
292
- """
293
- logging.debug(f"len x (plot): {len(x)}")
294
- logging.debug(f"len y (plot): {len(y)}")
295
- """
296
-
297
291
  # print current value
298
292
  try:
299
293
  if x[0] == 0:
300
294
  self.lb_value.setText(str(round(y[position_data], 3)))
301
295
  else:
302
296
  self.lb_value.setText(str(round(y[len(y) // 2], 3)))
303
- except:
297
+ except Exception:
304
298
  self.lb_value.setText("Read error")
305
299
 
306
300
  try:
@@ -310,16 +304,15 @@ class Plot_data(QWidget):
310
304
  self.myplot.axes.set_ylabel(self.y_label, rotation=90, labelpad=10)
311
305
  self.myplot.axes.set_ylim((min_value, max_value))
312
306
  self.myplot.axes.plot(x, y, self.plot_style)
313
- self.myplot.axes.axvline(x=position_data, color="red", linestyle="-")
307
+ self.myplot.axes.axvline(x=position_data, color=cfg.REALTIME_PLOT_CURSOR_COLOR, linestyle="-")
314
308
 
315
309
  self.myplot.draw()
316
- except:
310
+ except Exception:
317
311
  logging.debug(f"error in plotting external data: {sys.exc_info()[1]}")
318
- pass # only for protection against crash
319
312
 
320
313
 
321
314
  class Plotter(QObject):
322
- return_fig = pyqtSignal(
315
+ return_fig = Signal(
323
316
  np.ndarray, # x array
324
317
  np.ndarray, # y array
325
318
  float, # position_data
@@ -329,9 +322,8 @@ class Plotter(QObject):
329
322
  float, # position end
330
323
  )
331
324
 
332
- @pyqtSlot(float)
325
+ @Slot(float)
333
326
  def replot(self, current_time): # time_ in s
334
-
335
327
  logging.debug("current_time: {}".format(current_time))
336
328
 
337
329
  current_discrete_time = round(round(current_time / self.min_time_step) * self.min_time_step, 2)
@@ -342,7 +334,6 @@ class Plotter(QObject):
342
334
  freq_interval = int(round(self.interval / self.min_time_step))
343
335
 
344
336
  if self.min_time_value <= current_discrete_time <= self.max_time_value:
345
-
346
337
  logging.debug("self.min_time_value <= current_discrete_time <= self.max_time_value")
347
338
 
348
339
  idx = np.where(self.data[:, 0] == current_discrete_time)[0]
@@ -350,7 +341,6 @@ class Plotter(QObject):
350
341
  idx = np.where(abs(self.data[:, 0] - current_discrete_time) <= 0.02)[0]
351
342
 
352
343
  if len(idx):
353
-
354
344
  position_data = idx[0]
355
345
 
356
346
  logging.debug(f"position data: {position_data}")
@@ -387,14 +377,9 @@ class Plotter(QObject):
387
377
  d = np.array([np.nan] * int(self.interval / self.min_time_step)).T
388
378
 
389
379
  elif current_time > self.max_time_value:
390
-
391
380
  logging.debug(f"self.interval/self.min_time_step/2: {self.interval / self.min_time_step / 2}")
392
381
 
393
- dim_footer = int(
394
- round(
395
- (current_time - self.max_time_value) / self.min_time_step + self.interval / self.min_time_step / 2
396
- )
397
- )
382
+ dim_footer = int(round((current_time - self.max_time_value) / self.min_time_step + self.interval / self.min_time_step / 2))
398
383
 
399
384
  footer = np.array([np.nan] * dim_footer).T
400
385
  logging.debug(f"len footer: {len(footer)}")
@@ -403,7 +388,6 @@ class Plotter(QObject):
403
388
  logging.debug(f"a: {a}")
404
389
 
405
390
  if a >= 0:
406
-
407
391
  logging.debug("a>=0")
408
392
 
409
393
  st = int(round(len(self.data) - a))
@@ -429,7 +413,6 @@ class Plotter(QObject):
429
413
  logging.debug(f"len d a<0: {len(d)}")
430
414
 
431
415
  elif current_time < self.min_time_value:
432
-
433
416
  x = (self.min_time_value - current_time) / self.min_time_step
434
417
  dim_header = int(round(self.interval / self.min_time_step / 2 + x))
435
418
  header = np.array([np.nan] * dim_header).T
@@ -449,7 +432,11 @@ class Plotter(QObject):
449
432
 
450
433
  logging.debug(f"self.min_time_step: {self.min_time_step}")
451
434
 
452
- x = np.arange(current_time - self.interval // 2, current_time + self.interval // 2, self.min_time_step)
435
+ x = np.arange(
436
+ current_time - self.interval // 2,
437
+ current_time + self.interval // 2,
438
+ self.min_time_step,
439
+ )
453
440
 
454
441
  logging.debug(f"len x 1: {len(x)}")
455
442
 
@@ -494,7 +481,12 @@ if __name__ == "__main__":
494
481
  "convert_time_ecg": {
495
482
  "name": "convert_time_ecg",
496
483
  "description": "convert '%d/%m/%Y %H:%M:%S.%f' in seconds from epoch",
497
- "code": '\nimport datetime\nepoch = datetime.datetime.utcfromtimestamp(0)\ndatetime_format = "%d/%m/%Y %H:%M:%S.%f"\n\nOUTPUT = (datetime.datetime.strptime(INPUT, datetime_format) - epoch).total_seconds()\n',
484
+ "code": (
485
+ "\nimport datetime\n"
486
+ "epoch = datetime.datetime.utcfromtimestamp(0)\n"
487
+ 'datetime_format = "%d/%m/%Y %H:%M:%S.%f"\n\n'
488
+ "OUTPUT = (datetime.datetime.strptime(INPUT, datetime_format) - epoch).total_seconds()\n"
489
+ ),
498
490
  },
499
491
  "hhmmss_2_seconds": {
500
492
  "name": "hhmmss_2_seconds",