boris-behav-obs 9.7.12__py3-none-any.whl → 9.8.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 (84) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +4 -3
  4. boris/add_modifier.py +1 -1
  5. boris/advanced_event_filtering.py +1 -1
  6. boris/analysis_plugins/export_to_feral.py +336 -0
  7. boris/analysis_plugins/irr_weighted_cohen_kappa.py +2 -2
  8. boris/behav_coding_map_creator.py +1 -1
  9. boris/behavior_binary_table.py +1 -1
  10. boris/behaviors_coding_map.py +1 -1
  11. boris/boris_cli.py +1 -1
  12. boris/cmd_arguments.py +1 -1
  13. boris/coding_pad.py +1 -1
  14. boris/config.py +15 -3
  15. boris/config_file.py +18 -19
  16. boris/connections.py +12 -13
  17. boris/converters.py +1 -1
  18. boris/converters_ui.py +2 -3
  19. boris/cooccurence.py +1 -1
  20. boris/core.py +168 -166
  21. boris/core_qrc.py +1830 -1967
  22. boris/core_ui.py +1 -1
  23. boris/db_functions.py +5 -14
  24. boris/dialog.py +24 -24
  25. boris/edit_event.py +1 -1
  26. boris/event_operations.py +1 -1
  27. boris/events_cursor.py +1 -1
  28. boris/events_snapshots.py +133 -78
  29. boris/exclusion_matrix.py +1 -1
  30. boris/export_events.py +49 -43
  31. boris/export_observation.py +1 -1
  32. boris/external_processes.py +1 -1
  33. boris/geometric_measurement.py +1 -1
  34. boris/gui_utilities.py +1 -1
  35. boris/image_overlay.py +1 -1
  36. boris/import_observations.py +1 -1
  37. boris/ipc_mpv.py +1 -1
  38. boris/irr.py +1 -1
  39. boris/latency.py +1 -1
  40. boris/measurement_widget.py +1 -1
  41. boris/media_file.py +1 -1
  42. boris/menu_options.py +14 -12
  43. boris/modifier_coding_map_creator.py +1 -1
  44. boris/modifiers_coding_map.py +1 -1
  45. boris/observation.py +13 -14
  46. boris/observation_operations.py +1 -1
  47. boris/observations_list.py +1 -1
  48. boris/otx_parser.py +1 -1
  49. boris/param_panel.py +1 -1
  50. boris/player_dock_widget.py +1 -1
  51. boris/plot_data_module.py +1 -1
  52. boris/plot_events.py +1 -1
  53. boris/plot_events_rt.py +1 -1
  54. boris/plot_spectrogram_rt.py +42 -73
  55. boris/plot_waveform_rt.py +1 -1
  56. boris/plugins.py +1 -1
  57. boris/preferences.py +35 -4
  58. boris/preferences_ui.py +48 -18
  59. boris/project.py +1 -1
  60. boris/project_functions.py +19 -22
  61. boris/project_import_export.py +1 -1
  62. boris/select_modifiers.py +1 -1
  63. boris/select_observations.py +22 -23
  64. boris/select_subj_behav.py +4 -4
  65. boris/state_events.py +1 -1
  66. boris/subjects_pad.py +1 -1
  67. boris/synthetic_time_budget.py +1 -1
  68. boris/time_budget_functions.py +1 -1
  69. boris/time_budget_widget.py +1 -1
  70. boris/transitions.py +1 -1
  71. boris/utilities.py +1 -1
  72. boris/version.py +3 -3
  73. boris/video_equalizer.py +1 -1
  74. boris/video_operations.py +1 -1
  75. boris/view_df.py +28 -4
  76. boris/write_event.py +1 -1
  77. {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/METADATA +2 -2
  78. boris_behav_obs-9.8.2.dist-info/RECORD +110 -0
  79. {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/WHEEL +1 -1
  80. boris/analysis_plugins/_export_to_feral.py +0 -225
  81. boris_behav_obs-9.7.12.dist-info/RECORD +0 -110
  82. {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/entry_points.txt +0 -0
  83. {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/licenses/LICENSE.TXT +0 -0
  84. {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/top_level.txt +0 -0
boris/core_ui.py CHANGED
@@ -912,7 +912,7 @@ class Ui_MainWindow(object):
912
912
  self.actionEdit_selected_events.setText(QCoreApplication.translate("MainWindow", u"Edit selected event(s)", None))
913
913
  self.actionShow_spectrogram.setText(QCoreApplication.translate("MainWindow", u"Show the sound spectrogram", None))
914
914
  self.actionExport_events_as_Praat_TextGrid.setText(QCoreApplication.translate("MainWindow", u"as Praat TextGrid", None))
915
- self.actionExtract_events_from_media_files.setText(QCoreApplication.translate("MainWindow", u"Extract sequences from media files", None))
915
+ self.actionExtract_events_from_media_files.setText(QCoreApplication.translate("MainWindow", u"Extract clips from media files", None))
916
916
  self.action_geometric_measurements.setText(QCoreApplication.translate("MainWindow", u"Geometric measurement", None))
917
917
  self.actionFrame_forward.setText(QCoreApplication.translate("MainWindow", u"Frame forward", None))
918
918
  self.actionFrame_backward.setText(QCoreApplication.translate("MainWindow", u"frame backward", None))
boris/db_functions.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
 
7
7
  This program is free software; you can redistribute it and/or modify
@@ -21,12 +21,12 @@ Copyright 2012-2025 Olivier Friard
21
21
 
22
22
  """
23
23
 
24
- import sqlite3
25
24
  import logging
25
+ import sqlite3
26
26
  from typing import Optional, Tuple
27
+
27
28
  from . import config as cfg
28
- from . import project_functions
29
- from . import event_operations
29
+ from . import event_operations, project_functions
30
30
 
31
31
 
32
32
  def load_events_in_db(
@@ -39,6 +39,7 @@ def load_events_in_db(
39
39
  """
40
40
  populate a memory sqlite database with events from selected_observations,
41
41
  selected_subjects and selected_behaviors
42
+ include modifiers
42
43
 
43
44
  Args:
44
45
  pj (dict): project dictionary
@@ -59,16 +60,6 @@ def load_events_in_db(
59
60
  if cfg.STATE in pj[cfg.ETHOGRAM][x][cfg.TYPE].upper() and pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] in selected_behaviors
60
61
  ]
61
62
 
62
- # selected behaviors defined as point event
63
- """
64
- point_behaviors_codes = [
65
- pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE]
66
- for x in pj[cfg.ETHOGRAM]
67
- if cfg.POINT in pj[cfg.ETHOGRAM][x][cfg.TYPE].upper()
68
- and pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] in selected_behaviors
69
- ]
70
- """
71
-
72
63
  db = sqlite3.connect(":memory:", isolation_level=None)
73
64
 
74
65
  """
boris/dialog.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -21,23 +21,28 @@ This file is part of BORIS.
21
21
  """
22
22
 
23
23
  import datetime as dt
24
- from decimal import Decimal as dec
25
24
  import logging
26
25
  import math
27
26
  import pathlib as pl
28
27
  import platform
29
28
  import sys
30
29
  import traceback
30
+ from decimal import Decimal as dec
31
31
  from typing import Union
32
32
 
33
- from PySide6.QtCore import Qt, Signal, qVersion, QRect, QTime, QDateTime, QSize
33
+ from PySide6.QtCore import QDateTime, QRect, QSize, Qt, QTime, Signal, qVersion
34
+ from PySide6.QtGui import QFont, QTextCursor
34
35
  from PySide6.QtWidgets import (
35
- QApplication,
36
36
  QAbstractItemView,
37
+ QAbstractSpinBox,
38
+ QApplication,
37
39
  QCheckBox,
38
40
  QComboBox,
41
+ QDateTimeEdit,
39
42
  QDialog,
43
+ QDoubleSpinBox,
40
44
  QFileDialog,
45
+ QFrame,
41
46
  QHBoxLayout,
42
47
  QLabel,
43
48
  QLineEdit,
@@ -46,26 +51,21 @@ from PySide6.QtWidgets import (
46
51
  QMessageBox,
47
52
  QPlainTextEdit,
48
53
  QPushButton,
54
+ QRadioButton,
49
55
  QSizePolicy,
50
56
  QSpacerItem,
51
57
  QSpinBox,
52
- QDoubleSpinBox,
58
+ QStackedWidget,
53
59
  QTableView,
54
60
  QTableWidget,
61
+ QTimeEdit,
55
62
  QVBoxLayout,
56
63
  QWidget,
57
- QDateTimeEdit,
58
- QTimeEdit,
59
- QAbstractSpinBox,
60
- QRadioButton,
61
- QStackedWidget,
62
- QFrame,
63
64
  )
64
- from PySide6.QtGui import QFont, QTextCursor
65
65
 
66
66
  from . import config as cfg
67
- from . import version
68
67
  from . import utilities as util
68
+ from . import version
69
69
 
70
70
 
71
71
  def MessageDialog(title: str, text: str, buttons: tuple) -> str:
@@ -538,8 +538,8 @@ class Video_overlay_dialog(QDialog):
538
538
  None,
539
539
  cfg.programName,
540
540
  "Select a file containing a PNG image",
541
- QMessageBox.Ok | QMessageBox.Default,
542
- QMessageBox.NoButton,
541
+ QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Default,
542
+ QMessageBox.StandardButton.NoButton,
543
543
  )
544
544
  return
545
545
 
@@ -548,8 +548,8 @@ class Video_overlay_dialog(QDialog):
548
548
  None,
549
549
  cfg.programName,
550
550
  "The overlay position must be in x,y format",
551
- QMessageBox.Ok | QMessageBox.Default,
552
- QMessageBox.NoButton,
551
+ QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Default,
552
+ QMessageBox.StandardButton.NoButton,
553
553
  )
554
554
  return
555
555
  if self.le_overlay_position.text():
@@ -560,8 +560,8 @@ class Video_overlay_dialog(QDialog):
560
560
  None,
561
561
  cfg.programName,
562
562
  "The overlay position must be in x,y format",
563
- QMessageBox.Ok | QMessageBox.Default,
564
- QMessageBox.NoButton,
563
+ QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Default,
564
+ QMessageBox.StandardButton.NoButton,
565
565
  )
566
566
  return
567
567
  self.accept()
@@ -590,18 +590,18 @@ class Input_dialog(QDialog):
590
590
 
591
591
  self.elements: dict = {}
592
592
  for element in elements_list:
593
- if element[0] == "cb": # checkbox
593
+ if element[0] == cfg.CHECKBOX:
594
594
  self.elements[element[1]] = QCheckBox(element[1])
595
595
  self.elements[element[1]].setChecked(element[2])
596
596
  hbox.addWidget(self.elements[element[1]])
597
597
 
598
- if element[0] == "le": # line edit
598
+ if element[0] == cfg.LINE_EDIT:
599
599
  lb = QLabel(element[1])
600
600
  hbox.addWidget(lb)
601
601
  self.elements[element[1]] = QLineEdit()
602
602
  hbox.addWidget(self.elements[element[1]])
603
603
 
604
- if element[0] == "sb": # spinbox
604
+ if element[0] == cfg.SPINBOX:
605
605
  # 1 - Label
606
606
  # 2 - minimum value
607
607
  # 3 - maximum value
@@ -616,7 +616,7 @@ class Input_dialog(QDialog):
616
616
  self.elements[element[1]].setValue(element[5])
617
617
  hbox.addWidget(self.elements[element[1]])
618
618
 
619
- if element[0] == "dsb": # doubleSpinbox
619
+ if element[0] == cfg.DOUBLE_SPINBOX:
620
620
  # 1 - Label
621
621
  # 2 - minimum value
622
622
  # 3 - maximum value
@@ -633,7 +633,7 @@ class Input_dialog(QDialog):
633
633
  self.elements[element[1]].setDecimals(element[6])
634
634
  hbox.addWidget(self.elements[element[1]])
635
635
 
636
- if element[0] == "il": # items list
636
+ if element[0] == cfg.ITEMS_LIST:
637
637
  # 1 - Label
638
638
  # 2 - Values (tuple of tuple: 0 - value; 1 - "", "selected")
639
639
  lb = QLabel(element[1])
boris/edit_event.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
boris/event_operations.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
 
7
7
  This program is free software; you can redistribute it and/or modify
boris/events_cursor.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
 
7
7
  This program is free software; you can redistribute it and/or modify
boris/events_snapshots.py CHANGED
@@ -1,23 +1,22 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
+ This file is part of BORIS.
6
7
 
7
- This program is free software; you can redistribute it and/or modify
8
+ BORIS is free software; you can redistribute it and/or modify
8
9
  it under the terms of the GNU General Public License as published by
9
- the Free Software Foundation; either version 2 of the License, or
10
- (at your option) any later version.
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
11
12
 
12
- This program is distributed in the hope that it will be useful,
13
+ BORIS is distributed in the hope that it will be useful,
13
14
  but WITHOUT ANY WARRANTY; without even the implied warranty of
14
15
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
16
  GNU General Public License for more details.
16
17
 
17
18
  You should have received a copy of the GNU General Public License
18
- along with this program; if not, write to the Free Software
19
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
- MA 02110-1301, USA.
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
21
20
 
22
21
  """
23
22
 
@@ -27,17 +26,18 @@ import pathlib as pl
27
26
  import subprocess
28
27
  from decimal import Decimal as dec
29
28
 
30
- from PySide6.QtWidgets import QApplication, QFileDialog, QInputDialog, QMessageBox
29
+ from PySide6.QtCore import QProcess
30
+ from PySide6.QtWidgets import QApplication, QFileDialog
31
31
 
32
32
  from . import config as cfg
33
33
  from . import db_functions, dialog, project_functions, select_observations, select_subj_behav
34
34
  from . import utilities as util
35
35
 
36
36
 
37
- def events_snapshots(self):
37
+ def extract_media_snapshots(self):
38
38
  """
39
39
  create snapshots corresponding to coded events
40
- if observations are from media file and media files have video
40
+ Observations must be from media file and media files must have video
41
41
  """
42
42
 
43
43
  _, selected_observations = select_observations.select_observations2(
@@ -47,7 +47,7 @@ def events_snapshots(self):
47
47
  return
48
48
 
49
49
  # check if obs are MEDIA
50
- live_images_obs_list = []
50
+ live_images_obs_list: list = []
51
51
  for obs_id in selected_observations:
52
52
  if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in [cfg.LIVE, cfg.IMAGES]:
53
53
  live_images_obs_list.append(obs_id)
@@ -79,7 +79,7 @@ def events_snapshots(self):
79
79
  selected_observations,
80
80
  start_coding=dec("NaN"),
81
81
  end_coding=dec("NaN"),
82
- show_include_modifiers=False,
82
+ show_include_modifiers=True,
83
83
  show_exclude_non_coded_behaviors=False,
84
84
  n_observations=len(selected_observations),
85
85
  )
@@ -93,8 +93,16 @@ def events_snapshots(self):
93
93
  ib = dialog.Input_dialog(
94
94
  label_caption="Choose parameters",
95
95
  elements_list=[
96
- ("dsb", "Time interval around the events (in seconds)", 0.0, 86400, 1, 0, 3),
97
- ("il", "Bitmap format", (("JPG - small size / low quality", ""), ("PNG - big size / high quality", ""))),
96
+ (cfg.DOUBLE_SPINBOX, "Time interval around the events (in seconds)", 0.0, 86400, 1, 0, 3),
97
+ (
98
+ cfg.ITEMS_LIST,
99
+ "Bitmap format",
100
+ (
101
+ ("JPG - small size / low quality", ""),
102
+ ("PNG - big size / high quality", ""),
103
+ # ("WEBP - small size / high quality", "")
104
+ ),
105
+ ),
98
106
  ],
99
107
  title="Extract frames",
100
108
  )
@@ -105,6 +113,8 @@ def events_snapshots(self):
105
113
  frame_bitmap_format = "jpg"
106
114
  elif "PNG" in ib.elements["Bitmap format"].currentText():
107
115
  frame_bitmap_format = "png"
116
+ # elif "WEBP" in ib.elements["Bitmap format"].currentText():
117
+ # frame_bitmap_format = "webp"
108
118
  else:
109
119
  return
110
120
 
@@ -113,7 +123,7 @@ def events_snapshots(self):
113
123
  self,
114
124
  "Choose a directory to extract events",
115
125
  os.path.expanduser("~"),
116
- options=QFileDialog.ShowDirsOnly,
126
+ options=QFileDialog.Option.ShowDirsOnly,
117
127
  )
118
128
  if not export_dir:
119
129
  return
@@ -137,10 +147,13 @@ def events_snapshots(self):
137
147
  for subject in parameters[cfg.SELECTED_SUBJECTS]:
138
148
  for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
139
149
  cursor.execute(
140
- "SELECT occurence FROM events WHERE observation = ? AND subject = ? AND code = ?",
150
+ "SELECT occurence, modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
141
151
  (obs_id, subject, behavior),
142
152
  )
143
- rows = [{"occurence": util.float2decimal(r["occurence"])} for r in cursor.fetchall()]
153
+
154
+ rows = tuple(
155
+ {"occurence": util.float2decimal(r["occurence"]), "modifiers": r[cfg.MODIFIERS]} for r in cursor.fetchall()
156
+ )
144
157
 
145
158
  behavior_state = project_functions.event_type(behavior, self.pj[cfg.ETHOGRAM])
146
159
 
@@ -165,11 +178,11 @@ def events_snapshots(self):
165
178
  "The following media file does not have video.<br>"
166
179
  f"{self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]}"
167
180
  ),
168
- [cfg.OK, "Abort"],
181
+ (cfg.OK, cfg.ABORT),
169
182
  )
170
183
  if response == cfg.OK:
171
184
  continue
172
- if response == "Abort":
185
+ if response == cfg.ABORT:
173
186
  return
174
187
 
175
188
  # check FPS
@@ -194,11 +207,11 @@ def events_snapshots(self):
194
207
  "The FPS was not found for the following media file:<br>"
195
208
  f"{self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]}"
196
209
  ),
197
- [cfg.OK, "Abort"],
210
+ (cfg.OK, cfg.ABORT),
198
211
  )
199
212
  if response == cfg.OK:
200
213
  continue
201
- if response == "Abort":
214
+ if response == cfg.ABORT:
202
215
  return
203
216
 
204
217
  global_start = dec("0.000") if row["occurence"] < time_interval else round(row["occurence"] - time_interval, 3)
@@ -238,11 +251,11 @@ def events_snapshots(self):
238
251
  "At the moment it no possible to extract frames "
239
252
  "for this type of event.<br>"
240
253
  ),
241
- [cfg.OK, "Abort"],
254
+ (cfg.OK, cfg.ABORT),
242
255
  )
243
256
  if response == cfg.OK:
244
257
  continue
245
- if response == "Abort":
258
+ if response == cfg.ABORT:
246
259
  return
247
260
 
248
261
  # globalStop = round(rows[idx + 1]["occurence"] + time_interval, 3)
@@ -279,17 +292,23 @@ def events_snapshots(self):
279
292
  else:
280
293
  continue
281
294
 
282
- ffmpeg_command = (
283
- f'"{self.ffmpeg_bin}" '
284
- f"-ss {start:.3f} "
285
- f'-i "{media_path}" '
286
- f"-vframes {vframes} "
287
- f'"{export_dir}{os.sep}'
288
- f"{util.safeFileName(obs_id).replace(' ', '-')}"
289
- f"_PLAYER{nplayer}"
290
- f"_{util.safeFileName(subject).replace(' ', '-')}"
291
- f"_{util.safeFileName(behavior).replace(' ', '-')}"
292
- f'_{global_start:.3f}_%08d.{frame_bitmap_format}"'
295
+ ffmpeg_command = "".join(
296
+ [
297
+ f'"{self.ffmpeg_bin}" ',
298
+ f'-i "{media_path}" ',
299
+ f"-ss {start:.3f} ",
300
+ f"-vframes {vframes} ",
301
+ f'"{export_dir}{os.sep}',
302
+ f"{util.safeFileName(obs_id).replace(' ', '-')}",
303
+ f"_PLAYER{nplayer}",
304
+ f"_{util.safeFileName(subject).replace(' ', '-')}",
305
+ f"_{util.safeFileName(behavior).replace(' ', '-')}",
306
+ f"_{global_start:.3f}_%08d",
307
+ f"_{util.safeFileName(row[cfg.MODIFIERS].replace('|', '+')).replace(' ', '-')}"
308
+ if parameters[cfg.INCLUDE_MODIFIERS] and row[cfg.MODIFIERS]
309
+ else "",
310
+ f'.{frame_bitmap_format}"',
311
+ ]
293
312
  )
294
313
 
295
314
  logging.debug(f"ffmpeg command: {ffmpeg_command}")
@@ -300,26 +319,29 @@ def events_snapshots(self):
300
319
  self.statusbar.showMessage(f"Frames extracted in {export_dir}", 0)
301
320
 
302
321
 
303
- def extract_events(self):
322
+ def extract_media_clips(self):
304
323
  """
305
- extract sub-sequences from media files corresponding to coded events with FFmpeg
306
- in case of point event, from -n to +n seconds are extracted (n is asked to user)
324
+ extract with FFmpeg sub-sequences from media files corresponding to coded events
325
+ In case of point event, from -n to +n seconds are extracted (n is asked to user)
307
326
  """
308
327
 
328
+ # def on_finished(self, exit_code, exit_status):
329
+ # self.statusbar.showMessage("Media sequences extracted", 0)
330
+
309
331
  _, selected_observations = select_observations.select_observations2(
310
332
  self, cfg.MULTIPLE, windows_title="Select observations for extracting events"
311
333
  )
312
334
  if not selected_observations:
313
335
  return
314
336
 
315
- # check if obs are MEDIA
316
- live_images_obs_list = []
337
+ # check if obs are from media files
338
+ live_images_obs_list: list = []
317
339
  for obs_id in selected_observations:
318
- if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in [cfg.LIVE, cfg.IMAGES]:
340
+ if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
319
341
  live_images_obs_list.append(obs_id)
320
342
 
321
343
  if live_images_obs_list:
322
- out = "The following observations are live observations or observation from pictures and will be removed from analysis<br><br>"
344
+ out = "The following observations are live observations or observation from pictures and will be removed<br><br>"
323
345
  out += "<br>".join(live_images_obs_list)
324
346
  results = dialog.Results_dialog()
325
347
  results.setWindowTitle(cfg.programName)
@@ -345,39 +367,59 @@ def extract_events(self):
345
367
  selected_observations,
346
368
  start_coding=dec("NaN"),
347
369
  end_coding=dec("NaN"),
348
- show_include_modifiers=False,
370
+ show_include_modifiers=True,
349
371
  show_exclude_non_coded_behaviors=False,
350
372
  )
351
373
  if parameters == {}:
352
374
  return
353
375
 
354
376
  if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
355
- QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
356
377
  return
357
378
 
358
- # Ask for time interval around the event
359
- while True:
360
- text, ok = QInputDialog.getDouble(self, "Time interval around the events", "Time (in seconds):", 0.0, 0.0, 86400, 1)
361
- if not ok:
362
- return
363
- try:
364
- timeOffset = util.float2decimal(text)
365
- break
366
- except Exception:
367
- QMessageBox.warning(self, cfg.programName, f"<b>{text}</b> is not recognized as time")
368
-
369
- # ask for video / audio extraction
370
- items_to_extract, ok = QInputDialog.getItem(
371
- self, "Tracks to extract", "Tracks", ("Video and audio", "Only video", "Only audio"), 0, False
379
+ ib = dialog.Input_dialog(
380
+ label_caption="Choose parameters",
381
+ elements_list=[
382
+ (cfg.DOUBLE_SPINBOX, "Time interval around the events (in seconds)", 0.0, 86400, 1, 0, 3),
383
+ (
384
+ cfg.ITEMS_LIST,
385
+ "Tracks to extract",
386
+ (
387
+ ("Video and audio", ""),
388
+ ("Only video", ""),
389
+ ("Only audio", ""),
390
+ ),
391
+ ),
392
+ ],
393
+ title="Extract clips",
372
394
  )
373
- if not ok:
395
+ if not ib.exec_():
374
396
  return
375
397
 
398
+ timeOffset = util.float2decimal(ib.elements["Time interval around the events (in seconds)"].value())
399
+ items_to_extract = ib.elements["Tracks to extract"].currentText()
400
+
401
+ # Ask for time interval around the event
402
+ # while True:
403
+ # text, ok = QInputDialog.getDouble(self, "Time interval around the events", "Time (in seconds):", 0.0, 0.0, 86400, 1)
404
+ # if not ok:
405
+ # return
406
+ # try:
407
+ # timeOffset = util.float2decimal(text)
408
+ # break
409
+ # except Exception:
410
+ # QMessageBox.warning(self, cfg.programName, f"<b>{text}</b> is not recognized as time")
411
+ ## ask for video / audio extraction
412
+ # items_to_extract, ok = QInputDialog.getItem(
413
+ # self, "Tracks to extract", "Tracks", ("Video and audio", "Only video", "Only audio"), 0, False
414
+ # )
415
+ # if not ok:
416
+ # return
417
+
376
418
  export_dir = QFileDialog.getExistingDirectory(
377
419
  self,
378
420
  "Choose a directory to extract events",
379
421
  os.path.expanduser("~"),
380
- options=QFileDialog.ShowDirsOnly,
422
+ options=QFileDialog.Option.ShowDirsOnly,
381
423
  )
382
424
  if not export_dir:
383
425
  return
@@ -393,7 +435,7 @@ def extract_events(self):
393
435
  self.statusBar().showMessage("Extracting sequences from media files")
394
436
  QApplication.processEvents()
395
437
 
396
- ffmpeg_extract_command: str = '"{ffmpeg_bin}" -ss {start} -i "{input_}" -y -t {duration} {codecs} '
438
+ ffmpeg_extract_command: str = '"{ffmpeg_bin}" -i "{input_}" -ss {start} -y -t {duration} {codecs} '
397
439
  mem_command: str = ""
398
440
  for obs_id in selected_observations:
399
441
  for nplayer in self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
@@ -407,11 +449,12 @@ def extract_events(self):
407
449
  for subject in parameters[cfg.SELECTED_SUBJECTS]:
408
450
  for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
409
451
  cursor.execute(
410
- "SELECT occurence FROM events WHERE observation = ? AND subject = ? AND code = ?",
452
+ "SELECT occurence, modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
411
453
  (obs_id, subject, behavior),
412
454
  )
413
- rows = [{"occurence": util.float2decimal(r["occurence"])} for r in cursor.fetchall()]
414
-
455
+ rows = tuple(
456
+ {"occurence": util.float2decimal(r["occurence"]), "modifiers": r[cfg.MODIFIERS]} for r in cursor.fetchall()
457
+ )
415
458
  behavior_state = project_functions.event_type(behavior, self.pj[cfg.ETHOGRAM])
416
459
  if behavior_state in cfg.STATE_EVENT_TYPES and len(rows) % 2: # unpaired events
417
460
  continue
@@ -433,7 +476,7 @@ def extract_events(self):
433
476
  dialog.MessageDialog(
434
477
  cfg.programName,
435
478
  f"The media file {self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]} does not have a video stream",
436
- ["Continue", "Abort"],
479
+ ("Continue", "Abort"),
437
480
  )
438
481
  == "Abort"
439
482
  ):
@@ -461,9 +504,9 @@ def extract_events(self):
461
504
  dialog.MessageDialog(
462
505
  cfg.programName,
463
506
  f"The media file {self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]} does not have an audio stream",
464
- ["Continue", "Abort"],
507
+ ("Continue", cfg.ABORT),
465
508
  )
466
- == "Abort"
509
+ == cfg.ABORT
467
510
  ):
468
511
  return
469
512
  else:
@@ -509,11 +552,11 @@ def extract_events(self):
509
552
  "The event extends on 2 successive video. "
510
553
  " At the moment it is not possible to extract this type of event.<br>"
511
554
  ),
512
- [cfg.OK, "Abort"],
555
+ (cfg.OK, cfg.ABORT),
513
556
  )
514
557
  if response == cfg.OK:
515
558
  continue
516
- if response == "Abort":
559
+ if response == cfg.ABORT:
517
560
  return
518
561
 
519
562
  globalStart = dec("0.000") if row["occurence"] < timeOffset else round(row["occurence"] - timeOffset, 3)
@@ -550,22 +593,27 @@ def extract_events(self):
550
593
  continue
551
594
 
552
595
  new_file_name = pl.Path(export_dir) / pl.Path(
553
- (
554
- f"{util.safeFileName(obs_id).replace(' ', '-')}_"
555
- f"PLAYER{nplayer}_"
556
- f"{util.safeFileName(subject).replace(' ', '-')}_"
557
- f"{util.safeFileName(behavior)}_"
558
- f"{globalStart}-{globalStop}"
559
- f"{new_extension}"
596
+ "".join(
597
+ [
598
+ f"{util.safeFileName(obs_id).replace(' ', '-')}_",
599
+ f"PLAYER{nplayer}_",
600
+ f"{util.safeFileName(subject).replace(' ', '-')}_",
601
+ f"{util.safeFileName(behavior)}_",
602
+ f"{globalStart}-{globalStop}",
603
+ f"_{util.safeFileName(row[cfg.MODIFIERS].replace('|', '+')).replace(' ', '-')}"
604
+ if parameters[cfg.INCLUDE_MODIFIERS] and row[cfg.MODIFIERS]
605
+ else "",
606
+ f"{new_extension}",
607
+ ]
560
608
  )
561
- ) # .with_suffix(new_extension)
609
+ )
562
610
 
563
611
  if new_file_name.is_file():
564
612
  if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL):
565
613
  mem_command = dialog.MessageDialog(
566
614
  cfg.programName,
567
615
  f"The file <b>{new_file_name}</b> already exists.",
568
- [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
616
+ (cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL),
569
617
  )
570
618
  if mem_command == cfg.CANCEL:
571
619
  return
@@ -585,6 +633,13 @@ def extract_events(self):
585
633
 
586
634
  logging.debug(f'ffmpeg command: {ffmpeg_command} "{new_file_name}"')
587
635
 
636
+ # run ffmpeg command non blocking UI
637
+ # self.process = QProcess(self)
638
+ # self.process.readyReadStandardOutput.connect(self.on_stdout)
639
+ # self.process.readyReadStandardError.connect(self.on_stderr)
640
+ # self.process.finished.connect(on_finished)
641
+ # self.process.start(ffmpeg_command, [str(new_file_name)])
642
+
588
643
  p = subprocess.Popen(
589
644
  f'{ffmpeg_command} "{new_file_name}"',
590
645
  stdout=subprocess.PIPE,
boris/exclusion_matrix.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7