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,13 +20,13 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
-
24
23
  import os
25
24
  import tempfile
26
- import pathlib as pl
25
+ from pathlib import Path
26
+ import logging
27
27
 
28
- from PyQt5.QtWidgets import QFileDialog, QMessageBox, QInputDialog
29
- from PyQt5.QtCore import (
28
+ from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
29
+ from PySide6.QtCore import (
30
30
  Qt,
31
31
  QProcess,
32
32
  )
@@ -41,9 +41,9 @@ def ffmpeg_process(self, action: str):
41
41
  launch ffmpeg process with QProcess
42
42
 
43
43
  Args:
44
- action (str): "reencode_resize, rotate, merge
44
+ action (str): "reencode_resize, rotate, merge, video_spectrogram
45
45
  """
46
- if action not in ("reencode_resize", "rotate", "merge"):
46
+ if action not in ("reencode_resize", "rotate", "merge", "video_spectrogram"):
47
47
  return
48
48
 
49
49
  def readStdOutput(idx):
@@ -61,7 +61,7 @@ def ffmpeg_process(self, action: str):
61
61
  # self.processes_widget.lwi.clear()
62
62
  std_out = self.processes[idx - 1][0].readAllStandardOutput().data().decode("utf-8")
63
63
  if std_out:
64
- self.processes_widget.lwi.addItems((f"{pl.Path(self.processes[idx - 1][1][2]).name}: {std_out}",))
64
+ self.processes_widget.lwi.addItems((f"{Path(self.processes[idx - 1][1][2]).name}: {std_out}",))
65
65
 
66
66
  """
67
67
  std_err = self.processes[idx - 1][0].readAllStandardError().data().decode("utf-8")
@@ -83,9 +83,7 @@ def ffmpeg_process(self, action: str):
83
83
  self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
84
84
  else:
85
85
  self.processes_widget.label.setText(
86
- (
87
- f"Done: {self.processes_widget.number_of_files - len(self.processes)} of {self.processes_widget.number_of_files}"
88
- )
86
+ (f"Done: {self.processes_widget.number_of_files - len(self.processes)} of {self.processes_widget.number_of_files}")
89
87
  )
90
88
  """
91
89
  self.processes_widget.hide()
@@ -102,36 +100,37 @@ def ffmpeg_process(self, action: str):
102
100
  else:
103
101
  msg = f"Select one or more video files to {action.replace('_', ' and ')}"
104
102
  file_type = "Video files (*)"
105
- fn = QFileDialog().getOpenFileNames(self, msg, "", file_type)
106
- fileNames = fn[0] if type(fn) is tuple else fn
103
+ file_names, _ = QFileDialog().getOpenFileNames(self, msg, "", file_type)
107
104
 
108
- if not fileNames:
105
+ if not file_names:
109
106
  return
110
107
 
111
108
  if action == "reencode_resize":
112
- current_bitrate = 200_000
109
+ current_bitrate = 10_000_000 # default 10 Mb/s
113
110
  current_resolution = 1024
114
111
 
115
- r = util.accurate_media_analysis(self.ffmpeg_bin, fileNames[0])
112
+ r = util.accurate_media_analysis(self.ffmpeg_bin, file_names[0])
116
113
  if "error" in r:
117
- QMessageBox.warning(self, cfg.programName, f"{fileNames[0]}. {r['error']}")
114
+ QMessageBox.warning(self, cfg.programName, f"{file_names[0]}. {r['error']}")
118
115
  elif r["has_video"]:
119
116
  current_bitrate = r.get("bitrate", None)
120
117
  if current_bitrate is None:
121
118
  current_bitrate = -1
119
+ else:
120
+ current_bitrate = round(current_bitrate / 1024 / 1024) # Convert to Mb/s
122
121
  current_resolution = int(r["resolution"].split("x")[0]) if r["resolution"] is not None else None
123
122
 
124
123
  ib = dialog.Input_dialog(
125
124
  "Set the parameters for re-encoding / resizing",
126
125
  [
127
126
  ("sb", "Horizontal resolution (in pixel)", 352, 3840, 100, current_resolution),
128
- ("sb", "Video quality (bitrate)", 50_000, 100_000_000, 50_000, current_bitrate),
127
+ ("sb", "Video quality (bitrate Mb/s)", 1, 1000, 1, current_bitrate),
129
128
  ],
130
129
  )
131
130
  if not ib.exec_():
132
131
  return
133
132
 
134
- if len(fileNames) > 1:
133
+ if len(file_names) > 1:
135
134
  if (
136
135
  dialog.MessageDialog(
137
136
  cfg.programName,
@@ -143,18 +142,18 @@ def ffmpeg_process(self, action: str):
143
142
  return
144
143
 
145
144
  horiz_resol = ib.elements["Horizontal resolution (in pixel)"].value()
146
- video_quality = ib.elements["Video quality (bitrate)"].value()
145
+ video_quality = ib.elements["Video quality (bitrate Mb/s)"].value()
147
146
 
148
147
  if action == "merge":
149
- if len(fileNames) == 1:
148
+ if len(file_names) == 1:
150
149
  QMessageBox.critical(self, cfg.programName, "Select more than one file")
151
150
  return
152
151
 
153
152
  file_extensions = [] # check extension of 1st media file
154
153
  file_list_lst = []
155
- for file_name in fileNames:
154
+ for file_name in file_names:
156
155
  file_list_lst.append(f"file '{file_name}'")
157
- file_extensions.append(pl.Path(file_name).suffix)
156
+ file_extensions.append(Path(file_name).suffix)
158
157
  if len(set(file_extensions)) > 1:
159
158
  QMessageBox.critical(self, cfg.programName, "All media files must have the same format")
160
159
  return
@@ -163,11 +162,14 @@ def ffmpeg_process(self, action: str):
163
162
  output_file_name, _ = QFileDialog().getSaveFileName(self, "Output file name", "", "*")
164
163
  if output_file_name == "":
165
164
  return
166
- if pl.Path(output_file_name).suffix != file_extensions[0]:
165
+ if Path(output_file_name).suffix != file_extensions[0]:
167
166
  QMessageBox.warning(
168
167
  self,
169
168
  cfg.programName,
170
- f"The extension of output file must be the same than the extension of input files (<b>{file_extensions[0]}</b>).<br>You selected a {pl.Path(output_file_name).suffix} file.",
169
+ (
170
+ "The extension of output file must be the same than the extension of input files "
171
+ f"(<b>{file_extensions[0]}</b>).<br>You selected a {Path(output_file_name).suffix} file."
172
+ ),
171
173
  )
172
174
  else:
173
175
  break
@@ -190,10 +192,9 @@ def ffmpeg_process(self, action: str):
190
192
  # check if processed files already exist
191
193
  if action in ("reencode_resize", "rotate"):
192
194
  files_list = []
193
- for file_name in fileNames:
194
-
195
+ for file_name in file_names:
195
196
  if action == "reencode_resize":
196
- fn = f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}k.avi"
197
+ fn = f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi"
197
198
 
198
199
  if action == "rotate":
199
200
  fn = f"{file_name}.rotated{['', '90', '-90', '180'][rotation_idx]}.avi"
@@ -214,94 +215,118 @@ def ffmpeg_process(self, action: str):
214
215
  self.processes_widget.resize(700, 300)
215
216
 
216
217
  self.processes_widget.setWindowFlags(Qt.WindowStaysOnTopHint)
217
- if action == "reencode_resize":
218
- self.processes_widget.setWindowTitle("Re-encoding and resizing with FFmpeg")
219
- if action == "rotate":
220
- self.processes_widget.setWindowTitle("Rotating the video with FFmpeg")
221
- if action == "merge":
222
- self.processes_widget.setWindowTitle("Merging media files")
223
-
224
- self.processes_widget.label.setText(
225
- "This operation can be long. Be patient...\nIn the meanwhile you can continue to use BORIS\n\n"
226
- )
227
- self.processes_widget.number_of_files = len(fileNames)
218
+ match action:
219
+ case "reencode_resize":
220
+ self.processes_widget.setWindowTitle("Re-encoding and resizing with FFmpeg")
221
+ case "rotate":
222
+ self.processes_widget.setWindowTitle("Rotating the video with FFmpeg")
223
+ case "merge":
224
+ self.processes_widget.setWindowTitle("Merging media files")
225
+ case "video_spectrogram":
226
+ self.processes_widget.setWindowTitle("Creating a video spectrogram")
227
+
228
+ self.processes_widget.label.setText("This operation can be long. Be patient...\nIn the meanwhile you can continue to use BORIS\n\n")
229
+ self.processes_widget.number_of_files = len(file_names)
228
230
  self.processes_widget.show()
229
231
 
230
- if action == "merge":
231
- # ffmpeg -f concat -safe 0 -i join_video.txt -c copy output_demuxer.mp4
232
- args = ["-hide_banner", "-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output_file_name]
233
- self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
234
- self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
235
- self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
236
- self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
237
- self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
238
-
239
- self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
232
+ match action:
233
+ case "merge":
234
+ # ffmpeg -f concat -safe 0 -i join_video.txt -c copy output.mp4
235
+ args = ["-hide_banner", "-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output_file_name]
236
+ self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
237
+ self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
238
+ self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
239
+ self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
240
+ self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
240
241
 
241
- if action in ("reencode_resize", "rotate"):
242
- for file_name in sorted(fileNames, reverse=True):
242
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
243
243
 
244
- if action == "reencode_resize":
244
+ case "video_spectrogram":
245
+ # ffmpeg -i video.mp4 -filter_complex showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt -y -acodec copy output.mp4
246
+ for file_name in sorted(file_names, reverse=True):
247
+ output_file_name = str(Path(file_name).with_suffix(f".spectrogram{Path(file_name).suffix}"))
245
248
  args = [
246
249
  "-hide_banner",
247
250
  "-y",
248
251
  "-i",
249
- f"{file_name}",
250
- "-vf",
251
- f"scale={horiz_resol}:-1",
252
- "-b:v",
253
- f"{video_quality}",
254
- f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}k.avi",
252
+ file_name,
253
+ "-filter_complex",
254
+ "showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt",
255
+ "-acodec",
256
+ "copy",
257
+ output_file_name,
255
258
  ]
259
+ self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
260
+ self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
261
+ self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
262
+ self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
263
+ self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
256
264
 
257
- if action == "rotate":
258
-
259
- # check bitrate
260
- r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
261
- if "error" not in r and r["bitrate"] is not None:
262
- video_quality = r["bitrate"]
263
- else:
264
- video_quality = 200_000
265
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
265
266
 
266
- if rotation_idx in [1, 2]:
267
+ case "reencode_resize" | "rotate":
268
+ for file_name in sorted(file_names, reverse=True):
269
+ if action == "reencode_resize":
267
270
  args = [
268
271
  "-hide_banner",
269
272
  "-y",
270
273
  "-i",
271
274
  f"{file_name}",
272
275
  "-vf",
273
- f"transpose={rotation_idx}",
274
- "-codec:a",
275
- "copy",
276
+ f"scale={horiz_resol}:-1",
276
277
  "-b:v",
277
- f"{video_quality}",
278
- f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
278
+ f"{video_quality * 1024 * 1024}",
279
+ f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi",
279
280
  ]
280
281
 
281
- if rotation_idx == 3: # 180
282
- args = [
283
- "-hide_banner",
284
- "-y",
285
- "-i",
286
- f"{file_name}",
287
- "-vf",
288
- "transpose=2,transpose=2",
289
- "-codec:a",
290
- "copy",
291
- "-b:v",
292
- f"{video_quality}",
293
- f"{file_name}.rotated180.avi",
294
- ]
282
+ if action == "rotate":
283
+ # check bitrate
284
+ r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
285
+ if "error" not in r and r["bitrate"] is not None:
286
+ current_bitrate = r["bitrate"]
287
+ else:
288
+ current_bitrate = 10_000_000
289
+
290
+ if rotation_idx in (1, 2):
291
+ args = [
292
+ "-hide_banner",
293
+ "-y",
294
+ "-i",
295
+ f"{file_name}",
296
+ "-vf",
297
+ f"transpose={rotation_idx}",
298
+ "-codec:a",
299
+ "copy",
300
+ "-b:v",
301
+ f"{current_bitrate}",
302
+ f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
303
+ ]
304
+
305
+ if rotation_idx == 3: # 180
306
+ args = [
307
+ "-hide_banner",
308
+ "-y",
309
+ "-i",
310
+ f"{file_name}",
311
+ "-vf",
312
+ "transpose=2,transpose=2",
313
+ "-codec:a",
314
+ "copy",
315
+ "-b:v",
316
+ f"{current_bitrate}",
317
+ f"{file_name}.rotated180.avi",
318
+ ]
319
+
320
+ logging.debug("Launch process")
321
+ logging.debug(f"{self.ffmpeg_bin} {' '.join(args)}")
322
+
323
+ self.processes.append([QProcess(self), [self.ffmpeg_bin, args, file_name]])
324
+
325
+ ## FFmpeg output the work in progress on stderr
326
+ self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
327
+ self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
328
+ # self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
329
+
330
+ self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
295
331
 
296
- self.processes.append([QProcess(self), [self.ffmpeg_bin, args, file_name]])
297
-
298
- # self.processes[-1][0].setProcessChannelMode(QProcess.SeparateChannels)
299
-
300
- ## FFmpeg output the work in progress on stderr
301
- self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
302
- self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
303
- # self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
304
-
305
- self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
306
-
307
- self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
332
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])