boris-behav-obs 9.7.7__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 (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  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 +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  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.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,332 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ import os
24
+ import tempfile
25
+ from pathlib import Path
26
+ import logging
27
+
28
+ from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
29
+ from PySide6.QtCore import (
30
+ Qt,
31
+ QProcess,
32
+ )
33
+
34
+ from . import config as cfg
35
+ from . import dialog
36
+ from . import utilities as util
37
+
38
+
39
+ def ffmpeg_process(self, action: str):
40
+ """
41
+ launch ffmpeg process with QProcess
42
+
43
+ Args:
44
+ action (str): "reencode_resize, rotate, merge, video_spectrogram
45
+ """
46
+ if action not in ("reencode_resize", "rotate", "merge", "video_spectrogram"):
47
+ return
48
+
49
+ def readStdOutput(idx):
50
+ """
51
+ read stdout and stderr form qprocess and display them
52
+ """
53
+ self.processes_widget.label.setText(
54
+ (
55
+ "This operation can be long. Be patient...\n"
56
+ "In the meanwhile you can continue to use BORIS\n\n"
57
+ f"Done: {self.processes_widget.number_of_files - len(self.processes)} of {self.processes_widget.number_of_files}"
58
+ )
59
+ )
60
+
61
+ # self.processes_widget.lwi.clear()
62
+ std_out = self.processes[idx - 1][0].readAllStandardOutput().data().decode("utf-8")
63
+ if std_out:
64
+ self.processes_widget.lwi.addItems((f"{Path(self.processes[idx - 1][1][2]).name}: {std_out}",))
65
+
66
+ """
67
+ std_err = self.processes[idx - 1][0].readAllStandardError().data().decode("utf-8")
68
+ if std_err:
69
+ self.processes_widget.lwi.addItems((f"{pl.Path(self.processes[idx - 1][1][2]).name}: ERROR: {std_err}",))
70
+ self.flag_ffmpeg_error = True
71
+ """
72
+
73
+ self.processes_widget.lwi.scrollToBottom()
74
+
75
+ def qprocess_finished(idx):
76
+ """
77
+ function triggered when process finished
78
+ """
79
+ if self.processes:
80
+ del self.processes[idx - 1]
81
+ if self.processes:
82
+ # start new process
83
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
84
+ else:
85
+ self.processes_widget.label.setText(
86
+ (f"Done: {self.processes_widget.number_of_files - len(self.processes)} of {self.processes_widget.number_of_files}")
87
+ )
88
+ """
89
+ self.processes_widget.hide()
90
+ del self.processes_widget
91
+ """
92
+
93
+ if self.processes:
94
+ QMessageBox.warning(self, cfg.programName, "BORIS is already running some job.")
95
+ return
96
+
97
+ if action == "merge":
98
+ msg = "Select two or more media files to merge"
99
+ file_type = "Media files (*)"
100
+ else:
101
+ msg = f"Select one or more video files to {action.replace('_', ' and ')}"
102
+ file_type = "Video files (*)"
103
+ file_names, _ = QFileDialog().getOpenFileNames(self, msg, "", file_type)
104
+
105
+ if not file_names:
106
+ return
107
+
108
+ if action == "reencode_resize":
109
+ current_bitrate = 10_000_000 # default 10 Mb/s
110
+ current_resolution = 1024
111
+
112
+ r = util.accurate_media_analysis(self.ffmpeg_bin, file_names[0])
113
+ if "error" in r:
114
+ QMessageBox.warning(self, cfg.programName, f"{file_names[0]}. {r['error']}")
115
+ elif r["has_video"]:
116
+ current_bitrate = r.get("bitrate", None)
117
+ if current_bitrate is None:
118
+ current_bitrate = -1
119
+ else:
120
+ current_bitrate = round(current_bitrate / 1024 / 1024) # Convert to Mb/s
121
+ current_resolution = int(r["resolution"].split("x")[0]) if r["resolution"] is not None else None
122
+
123
+ ib = dialog.Input_dialog(
124
+ "Set the parameters for re-encoding / resizing",
125
+ [
126
+ ("sb", "Horizontal resolution (in pixel)", 352, 3840, 100, current_resolution),
127
+ ("sb", "Video quality (bitrate Mb/s)", 1, 1000, 1, current_bitrate),
128
+ ],
129
+ )
130
+ if not ib.exec_():
131
+ return
132
+
133
+ if len(file_names) > 1:
134
+ if (
135
+ dialog.MessageDialog(
136
+ cfg.programName,
137
+ "All the selected video files will be re-encoded / resized with these parameters",
138
+ [cfg.OK, cfg.CANCEL],
139
+ )
140
+ == cfg.CANCEL
141
+ ):
142
+ return
143
+
144
+ horiz_resol = ib.elements["Horizontal resolution (in pixel)"].value()
145
+ video_quality = ib.elements["Video quality (bitrate Mb/s)"].value()
146
+
147
+ if action == "merge":
148
+ if len(file_names) == 1:
149
+ QMessageBox.critical(self, cfg.programName, "Select more than one file")
150
+ return
151
+
152
+ file_extensions = [] # check extension of 1st media file
153
+ file_list_lst = []
154
+ for file_name in file_names:
155
+ file_list_lst.append(f"file '{file_name}'")
156
+ file_extensions.append(Path(file_name).suffix)
157
+ if len(set(file_extensions)) > 1:
158
+ QMessageBox.critical(self, cfg.programName, "All media files must have the same format")
159
+ return
160
+
161
+ while True:
162
+ output_file_name, _ = QFileDialog().getSaveFileName(self, "Output file name", "", "*")
163
+ if output_file_name == "":
164
+ return
165
+ if Path(output_file_name).suffix != file_extensions[0]:
166
+ QMessageBox.warning(
167
+ self,
168
+ cfg.programName,
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
+ ),
173
+ )
174
+ else:
175
+ break
176
+
177
+ # temp file for list of media file to merge
178
+ with tempfile.NamedTemporaryFile() as tmp:
179
+ file_list = tmp.name
180
+ with open(file_list, "w") as f_out:
181
+ f_out.write("\n".join(file_list_lst))
182
+
183
+ if action == "rotate":
184
+ rotation_items = ("Rotate 90 clockwise", "Rotate 90 counter clockwise", "rotate 180")
185
+
186
+ rotation, ok = QInputDialog.getItem(self, "Rotate media file(s)", "Type of rotation", rotation_items, 0, False)
187
+
188
+ if not ok:
189
+ return
190
+ rotation_idx = rotation_items.index(rotation) + 1
191
+
192
+ # check if processed files already exist
193
+ if action in ("reencode_resize", "rotate"):
194
+ files_list = []
195
+ for file_name in file_names:
196
+ if action == "reencode_resize":
197
+ fn = f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi"
198
+
199
+ if action == "rotate":
200
+ fn = f"{file_name}.rotated{['', '90', '-90', '180'][rotation_idx]}.avi"
201
+
202
+ if os.path.isfile(fn):
203
+ files_list.append(fn)
204
+
205
+ if files_list:
206
+ response = dialog.MessageDialog(
207
+ cfg.programName,
208
+ "Some file(s) already exist.\n\n" + "\n".join(files_list),
209
+ [cfg.OVERWRITE_ALL, cfg.CANCEL],
210
+ )
211
+ if response == cfg.CANCEL:
212
+ return
213
+
214
+ self.processes_widget = dialog.Info_widget()
215
+ self.processes_widget.resize(700, 300)
216
+
217
+ self.processes_widget.setWindowFlags(Qt.WindowStaysOnTopHint)
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)
230
+ self.processes_widget.show()
231
+
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)))
241
+
242
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
243
+
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}"))
248
+ args = [
249
+ "-hide_banner",
250
+ "-y",
251
+ "-i",
252
+ file_name,
253
+ "-filter_complex",
254
+ "showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt",
255
+ "-acodec",
256
+ "copy",
257
+ output_file_name,
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)))
264
+
265
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
266
+
267
+ case "reencode_resize" | "rotate":
268
+ for file_name in sorted(file_names, reverse=True):
269
+ if action == "reencode_resize":
270
+ args = [
271
+ "-hide_banner",
272
+ "-y",
273
+ "-i",
274
+ f"{file_name}",
275
+ "-vf",
276
+ f"scale={horiz_resol}:-1",
277
+ "-b:v",
278
+ f"{video_quality * 1024 * 1024}",
279
+ f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi",
280
+ ]
281
+
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)))
331
+
332
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])