boris-behav-obs 9.4.1__py2.py3-none-any.whl → 9.5__py2.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.
@@ -8,7 +8,7 @@ import pandas as pd
8
8
 
9
9
  __version__ = "0.0.1"
10
10
  __version_date__ = "2025-04-10"
11
- __plugin_name__ = "Behavior latency"
11
+ __plugin_name__ = "Behavior latencyxxx"
12
12
  __author__ = "Olivier Friard - University of Torino - Italy"
13
13
 
14
14
 
boris/config.py CHANGED
@@ -531,6 +531,16 @@ NO_COLOR_CODING_PAD = "#777777"
531
531
  SPECTROGRAM_COLOR_MAPS = ["viridis", "inferno", "plasma", "magma", "gray", "YlOrRd"]
532
532
  SPECTROGRAM_DEFAULT_COLOR_MAP = "viridis"
533
533
  SPECTROGRAM_DEFAULT_TIME_INTERVAL = 10
534
+ SPECTROGRAM_WINDOW_TYPE = "SPECTROGRAM_WINDOW_TYPE"
535
+ SPECTROGRAM_DEFAULT_WINDOW_TYPE = "hanning"
536
+ SPECTROGRAM_NFFT = "SPECTROGRAM_NFFT"
537
+ SPECTROGRAM_DEFAULT_NFFT = "1024"
538
+ SPECTROGRAM_NOVERLAP = "SPECTROGRAM_NOVERLAP"
539
+ SPECTROGRAM_DEFAULT_NOVERLAP = 900
540
+ SPECTROGRAM_VMIN = "SPECTROGRAM_VMIN"
541
+ SPECTROGRAM_DEFAULT_VMIN = -100
542
+ SPECTROGRAM_VMAX = "SPECTROGRAM_VMAX"
543
+ SPECTROGRAM_DEFAULT_VMAX = -20
534
544
 
535
545
  # see matplotlib.colors.cnames.keys()
536
546
  # https://xkcd.com/color/rgb/
boris/connections.py CHANGED
@@ -193,10 +193,11 @@ def connections(self):
193
193
  self.actionAdd_image_overlay_on_video.triggered.connect(lambda: image_overlay.add_image_overlay(self))
194
194
  self.actionRemove_image_overlay.triggered.connect(lambda: image_overlay.remove_image_overlay(self))
195
195
 
196
+ self.actionMedia_file_information_2.triggered.connect(lambda: media_file.get_info(self))
196
197
  self.actionRecode_resize_video.triggered.connect(lambda: external_processes.ffmpeg_process(self, "reencode_resize"))
197
198
  self.actionRotate_video.triggered.connect(lambda: external_processes.ffmpeg_process(self, "rotate"))
198
199
  self.actionMerge_media_files.triggered.connect(lambda: external_processes.ffmpeg_process(self, "merge"))
199
- self.actionMedia_file_information_2.triggered.connect(lambda: media_file.get_info(self))
200
+ self.actionCreate_video_spectrogram.triggered.connect(lambda: external_processes.ffmpeg_process(self, "video_spectrogram"))
200
201
 
201
202
  self.actionCreate_transitions_flow_diagram.triggered.connect(transitions.transitions_dot_script)
202
203
  self.actionCreate_transitions_flow_diagram_2.triggered.connect(transitions.transitions_flow_diagram)
boris/core.py CHANGED
@@ -1034,6 +1034,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1034
1034
  self.spectro.interval = self.spectrogram_time_interval
1035
1035
  self.spectro.cursor_color = cfg.REALTIME_PLOT_CURSOR_COLOR
1036
1036
 
1037
+ self.spectro.config_param = self.config_param
1038
+
1037
1039
  # color palette
1038
1040
  try:
1039
1041
  self.spectro.spectro_color_map = matplotlib.pyplot.get_cmap(self.spectrogram_color_map)
boris/core_ui.py CHANGED
@@ -3,7 +3,7 @@
3
3
  ################################################################################
4
4
  ## Form generated from reading UI file 'core.ui'
5
5
  ##
6
- ## Created by: Qt User Interface Compiler version 6.8.0
6
+ ## Created by: Qt User Interface Compiler version 6.9.0
7
7
  ##
8
8
  ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
9
  ################################################################################
@@ -378,6 +378,8 @@ class Ui_MainWindow(object):
378
378
  self.actionAdd_frame_indexes.setObjectName(u"actionAdd_frame_indexes")
379
379
  self.action_load_plugins = QAction(MainWindow)
380
380
  self.action_load_plugins.setObjectName(u"action_load_plugins")
381
+ self.actionCreate_video_spectrogram = QAction(MainWindow)
382
+ self.actionCreate_video_spectrogram.setObjectName(u"actionCreate_video_spectrogram")
381
383
  self.centralwidget = QWidget(MainWindow)
382
384
  self.centralwidget.setObjectName(u"centralwidget")
383
385
  self.horizontalLayout_2 = QHBoxLayout(self.centralwidget)
@@ -484,7 +486,7 @@ class Ui_MainWindow(object):
484
486
  MainWindow.setCentralWidget(self.centralwidget)
485
487
  self.menubar = QMenuBar(MainWindow)
486
488
  self.menubar.setObjectName(u"menubar")
487
- self.menubar.setGeometry(QRect(0, 0, 1509, 22))
489
+ self.menubar.setGeometry(QRect(0, 0, 1509, 20))
488
490
  self.menuHelp = QMenu(self.menubar)
489
491
  self.menuHelp.setObjectName(u"menuHelp")
490
492
  self.menuFile = QMenu(self.menubar)
@@ -775,6 +777,7 @@ class Ui_MainWindow(object):
775
777
  self.menuMedia_file.addAction(self.actionRecode_resize_video)
776
778
  self.menuMedia_file.addAction(self.actionRotate_video)
777
779
  self.menuMedia_file.addAction(self.actionMerge_media_files)
780
+ self.menuMedia_file.addAction(self.actionCreate_video_spectrogram)
778
781
  self.toolBar.addAction(self.action_obs_list)
779
782
  self.toolBar.addAction(self.actionPlay)
780
783
  self.toolBar.addAction(self.actionReset)
@@ -1029,6 +1032,7 @@ class Ui_MainWindow(object):
1029
1032
  self.actionConfigure_tvevents_columns.setText(QCoreApplication.translate("MainWindow", u"Configure columns", None))
1030
1033
  self.actionAdd_frame_indexes.setText(QCoreApplication.translate("MainWindow", u"Add frame indexes", None))
1031
1034
  self.action_load_plugins.setText(QCoreApplication.translate("MainWindow", u"Load plugins", None))
1035
+ self.actionCreate_video_spectrogram.setText(QCoreApplication.translate("MainWindow", u"Create video spectrogram", None))
1032
1036
  self.lbLogoBoris.setText("")
1033
1037
  self.lbLogoUnito.setText("")
1034
1038
  self.lb_player_status.setText(QCoreApplication.translate("MainWindow", u"lb_player_status", None))
@@ -22,7 +22,7 @@ This file is part of BORIS.
22
22
 
23
23
  import os
24
24
  import tempfile
25
- import pathlib as pl
25
+ from pathlib import Path
26
26
  import logging
27
27
 
28
28
  from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
@@ -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")
@@ -153,7 +153,7 @@ def ffmpeg_process(self, action: str):
153
153
  file_list_lst = []
154
154
  for file_name in file_names:
155
155
  file_list_lst.append(f"file '{file_name}'")
156
- file_extensions.append(pl.Path(file_name).suffix)
156
+ file_extensions.append(Path(file_name).suffix)
157
157
  if len(set(file_extensions)) > 1:
158
158
  QMessageBox.critical(self, cfg.programName, "All media files must have the same format")
159
159
  return
@@ -162,13 +162,13 @@ def ffmpeg_process(self, action: str):
162
162
  output_file_name, _ = QFileDialog().getSaveFileName(self, "Output file name", "", "*")
163
163
  if output_file_name == "":
164
164
  return
165
- if pl.Path(output_file_name).suffix != file_extensions[0]:
165
+ if Path(output_file_name).suffix != file_extensions[0]:
166
166
  QMessageBox.warning(
167
167
  self,
168
168
  cfg.programName,
169
169
  (
170
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 {pl.Path(output_file_name).suffix} file."
171
+ f"(<b>{file_extensions[0]}</b>).<br>You selected a {Path(output_file_name).suffix} file."
172
172
  ),
173
173
  )
174
174
  else:
@@ -215,93 +215,118 @@ def ffmpeg_process(self, action: str):
215
215
  self.processes_widget.resize(700, 300)
216
216
 
217
217
  self.processes_widget.setWindowFlags(Qt.WindowStaysOnTopHint)
218
- if action == "reencode_resize":
219
- self.processes_widget.setWindowTitle("Re-encoding and resizing with FFmpeg")
220
- if action == "rotate":
221
- self.processes_widget.setWindowTitle("Rotating the video with FFmpeg")
222
- if action == "merge":
223
- self.processes_widget.setWindowTitle("Merging media files")
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")
224
227
 
225
228
  self.processes_widget.label.setText("This operation can be long. Be patient...\nIn the meanwhile you can continue to use BORIS\n\n")
226
229
  self.processes_widget.number_of_files = len(file_names)
227
230
  self.processes_widget.show()
228
231
 
229
- if action == "merge":
230
- # ffmpeg -f concat -safe 0 -i join_video.txt -c copy output_demuxer.mp4
231
- args = ["-hide_banner", "-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output_file_name]
232
- self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
233
- self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
234
- self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
235
- self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
236
- self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
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)))
237
241
 
238
- self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
242
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
239
243
 
240
- if action in ("reencode_resize", "rotate"):
241
- for file_name in sorted(file_names, reverse=True):
242
- 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}"))
243
248
  args = [
244
249
  "-hide_banner",
245
250
  "-y",
246
251
  "-i",
247
- f"{file_name}",
248
- "-vf",
249
- f"scale={horiz_resol}:-1",
250
- "-b:v",
251
- f"{video_quality * 1024 * 1024}",
252
- f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.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,
253
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)))
254
264
 
255
- if action == "rotate":
256
- # check bitrate
257
- r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
258
- if "error" not in r and r["bitrate"] is not None:
259
- current_bitrate = r["bitrate"]
260
- else:
261
- current_bitrate = 10_000_000
262
-
263
- if rotation_idx in (1, 2):
264
- args = [
265
- "-hide_banner",
266
- "-y",
267
- "-i",
268
- f"{file_name}",
269
- "-vf",
270
- f"transpose={rotation_idx}",
271
- "-codec:a",
272
- "copy",
273
- "-b:v",
274
- f"{current_bitrate}",
275
- f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
276
- ]
265
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
277
266
 
278
- if rotation_idx == 3: # 180
267
+ case "reencode_resize" | "rotate":
268
+ for file_name in sorted(file_names, reverse=True):
269
+ if action == "reencode_resize":
279
270
  args = [
280
271
  "-hide_banner",
281
272
  "-y",
282
273
  "-i",
283
274
  f"{file_name}",
284
275
  "-vf",
285
- "transpose=2,transpose=2",
286
- "-codec:a",
287
- "copy",
276
+ f"scale={horiz_resol}:-1",
288
277
  "-b:v",
289
- f"{current_bitrate}",
290
- f"{file_name}.rotated180.avi",
278
+ f"{video_quality * 1024 * 1024}",
279
+ f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi",
291
280
  ]
292
281
 
293
- logging.debug("Launch process")
294
- logging.debug(f"{self.ffmpeg_bin} {' '.join(args)}")
295
-
296
- self.processes.append([QProcess(self), [self.ffmpeg_bin, args, file_name]])
297
-
298
- # self.processes[-1][0].setProcessChannelMode(QProcess.SeparateChannels)
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)))
299
331
 
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])
@@ -19,10 +19,11 @@ Copyright 2012-2025 Olivier Friard
19
19
  MA 02110-1301, USA.
20
20
  """
21
21
 
22
- import json
23
22
  import datetime
24
- from pathlib import Path
23
+ import gzip
24
+ import json
25
25
  import pandas as pd
26
+ from pathlib import Path
26
27
 
27
28
  from PySide6.QtWidgets import (
28
29
  QMessageBox,
@@ -49,8 +50,14 @@ def load_observations_from_boris_project(self, project_file_path: str):
49
50
  )
50
51
  return
51
52
 
53
+ if project_file_path.endswith(".boris.gz"):
54
+ file_in = gzip.open(project_file_path, mode="rt", encoding="utf-8")
55
+ else:
56
+ file_in = open(project_file_path, "r")
57
+ file_content = file_in.read()
58
+
52
59
  try:
53
- fromProject = json.loads(open(project_file_path, "r").read())
60
+ fromProject = json.loads(file_content)
54
61
  except Exception:
55
62
  QMessageBox.critical(self, cfg.programName, "This project file seems corrupted")
56
63
  return
@@ -84,7 +91,7 @@ def load_observations_from_boris_project(self, project_file_path: str):
84
91
  if new_behav_set:
85
92
  diag_result = dialog.MessageDialog(
86
93
  cfg.programName,
87
- (f"Some coded behaviors in <b>{obs_id}</b> are " f"not defined in the ethogram:<br><b>{', '.join(new_behav_set)}</b>"),
94
+ (f"Some coded behaviors in <b>{obs_id}</b> are not defined in the ethogram:<br><b>{', '.join(new_behav_set)}</b>"),
88
95
  ["Interrupt import", "Skip observation", "Import observation"],
89
96
  )
90
97
  if diag_result == "Interrupt import":
@@ -103,7 +110,7 @@ def load_observations_from_boris_project(self, project_file_path: str):
103
110
  if new_subject_set and new_subject_set != {""}:
104
111
  diag_result = dialog.MessageDialog(
105
112
  cfg.programName,
106
- (f"Some coded subjects in <b>{obs_id}</b> are not defined in the project:<br>" f"<b>{', '.join(new_subject_set)}</b>"),
113
+ (f"Some coded subjects in <b>{obs_id}</b> are not defined in the project:<br><b>{', '.join(new_subject_set)}</b>"),
107
114
  ["Interrupt import", "Skip observation", "Import observation"],
108
115
  )
109
116
 
@@ -116,7 +123,7 @@ def load_observations_from_boris_project(self, project_file_path: str):
116
123
  if obs_id in self.pj[cfg.OBSERVATIONS].keys():
117
124
  diag_result = dialog.MessageDialog(
118
125
  cfg.programName,
119
- (f"The observation <b>{obs_id}</b>" "already exists in the current project.<br>"),
126
+ (f"The observation <b>{obs_id}</b>already exists in the current project.<br>"),
120
127
  ["Interrupt import", "Skip observation", "Rename observation"],
121
128
  )
122
129
  if diag_result == "Interrupt import":
@@ -141,18 +148,11 @@ def load_observations_from_spreadsheet(self, project_file_path: str):
141
148
  import observations from a spreadsheet file
142
149
  """
143
150
 
144
- if Path(project_file_path).suffix.upper() == ".XLSX":
151
+ if Path(project_file_path).suffix.lower() == ".xlsx":
145
152
  engine = "openpyxl"
146
- elif Path(project_file_path).suffix.upper() == ".ODS":
153
+ elif Path(project_file_path).suffix.lower() == ".ods":
147
154
  engine = "odf"
148
155
  else:
149
- QMessageBox.warning(
150
- None,
151
- cfg.programName,
152
- ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
153
- QMessageBox.Ok | QMessageBox.Default,
154
- QMessageBox.NoButton,
155
- )
156
156
  return
157
157
 
158
158
  try:
@@ -167,7 +167,7 @@ def load_observations_from_spreadsheet(self, project_file_path: str):
167
167
  )
168
168
  return
169
169
 
170
- expected_labels: list = ["time", "subject", "code", "modifier", "comment"]
170
+ expected_labels: list = ("time", "subject", "code", "modifier", "comment")
171
171
 
172
172
  df.columns = df.columns.str.upper()
173
173
 
@@ -210,16 +210,16 @@ def import_observations(self):
210
210
  """
211
211
 
212
212
  file_name, _ = QFileDialog().getOpenFileName(
213
- None, "Choose a file", "", "BORIS project files (*.boris);;Spreadsheet files (*.ods *.xlsx *);;All files (*)"
213
+ None, "Choose a file", "", "BORIS project files (*.boris *.boris.gz);;Spreadsheet files (*.ods *.xlsx *);;All files (*)"
214
214
  )
215
215
 
216
216
  if not file_name:
217
217
  return
218
218
 
219
- if Path(file_name).suffix == ".boris":
219
+ if file_name.endswith(".boris") or file_name.endswith(".boris.gz"):
220
220
  load_observations_from_boris_project(self, file_name)
221
221
 
222
- if Path(file_name).suffix in (".ods", ".xlsx"):
222
+ elif Path(file_name).suffix.lower() in (".ods", ".xlsx"):
223
223
  if not self.observationId:
224
224
  QMessageBox.warning(
225
225
  None,
@@ -231,3 +231,12 @@ def import_observations(self):
231
231
  return
232
232
 
233
233
  load_observations_from_spreadsheet(self, file_name)
234
+
235
+ else:
236
+ QMessageBox.warning(
237
+ None,
238
+ cfg.programName,
239
+ ("The type of file was not recognized. Must be a BORIS project or a Microsoft-Excel XLSX format or OpenDocument ODS"),
240
+ QMessageBox.Ok | QMessageBox.Default,
241
+ QMessageBox.NoButton,
242
+ )
@@ -19,7 +19,7 @@ Copyright 2012-2025 Olivier Friard
19
19
  MA 02110-1301, USA.
20
20
  """
21
21
 
22
- from math import log2
22
+ from math import log2, floor
23
23
  import os
24
24
  import logging
25
25
  import time
@@ -2426,6 +2426,17 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2426
2426
  str: name of media file containing the event
2427
2427
  """
2428
2428
 
2429
+ cumul_media_durations: list = [dec(0)]
2430
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2431
+ try:
2432
+ media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
2433
+ # cut off media duration to 3 decimal places as that is how fine the player is
2434
+ media_duration = floor(media_duration * 10**3) / dec(10**3)
2435
+ cumul_media_durations.append(floor((cumul_media_durations[-1] + media_duration) * 10**3) / dec(10**3))
2436
+ except KeyError:
2437
+ return None
2438
+
2439
+ """
2429
2440
  cumul_media_durations: list = [dec(0)]
2430
2441
  for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2431
2442
  try:
@@ -2433,9 +2444,12 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2433
2444
  cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2434
2445
  except KeyError:
2435
2446
  return None
2447
+ """
2436
2448
 
2437
2449
  cumul_media_durations.remove(dec(0))
2438
2450
 
2451
+ logging.debug(f"{cumul_media_durations=}")
2452
+
2439
2453
  # test if timestamp is at end of last media
2440
2454
  if timestamp == cumul_media_durations[-1]:
2441
2455
  player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
boris/plot_events.py CHANGED
@@ -25,7 +25,7 @@ import pathlib as pl
25
25
 
26
26
  import matplotlib
27
27
 
28
- matplotlib.use("Qt5Agg")
28
+ matplotlib.use("QtAgg")
29
29
 
30
30
  import matplotlib.dates
31
31
 
boris/plot_events_rt.py CHANGED
@@ -25,7 +25,7 @@ Plot events in real time
25
25
 
26
26
  import matplotlib
27
27
 
28
- matplotlib.use("Qt5Agg")
28
+ matplotlib.use("QtAgg")
29
29
 
30
30
  import numpy as np
31
31
  from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
@@ -24,10 +24,10 @@ Copyright 2012-2025 Olivier Friard
24
24
  import wave
25
25
  import matplotlib
26
26
 
27
- matplotlib.use("Qt5Agg")
27
+ matplotlib.use("QtAgg")
28
28
 
29
29
  import numpy as np
30
-
30
+ from scipy import signal
31
31
  from . import config as cfg
32
32
 
33
33
  from PySide6.QtWidgets import (
@@ -43,8 +43,6 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
43
43
  from matplotlib.figure import Figure
44
44
  import matplotlib.ticker as mticker
45
45
 
46
- # matplotlib.pyplot.switch_backend("Qt5Agg")
47
-
48
46
 
49
47
  class Plot_spectrogram_RT(QWidget):
50
48
  # send keypress event to mainwindow
@@ -115,7 +113,7 @@ class Plot_spectrogram_RT(QWidget):
115
113
  else:
116
114
  return False
117
115
 
118
- def get_wav_info(self, wav_file: str):
116
+ def get_wav_info(self, wav_file: str) -> tuple[np.array, int]:
119
117
  """
120
118
  read wav file and extract information
121
119
 
@@ -184,7 +182,7 @@ class Plot_spectrogram_RT(QWidget):
184
182
 
185
183
  return {"media_length": self.media_length, "frame_rate": self.frame_rate}
186
184
 
187
- def plot_spectro(self, current_time: float, force_plot: bool = False):
185
+ def plot_spectro(self, current_time: float, force_plot: bool = False) -> tuple[float, bool]:
188
186
  """
189
187
  plot sound spectrogram centered on the current time
190
188
 
@@ -200,15 +198,36 @@ class Plot_spectrogram_RT(QWidget):
200
198
 
201
199
  self.ax.clear()
202
200
 
201
+ window_type = "blackmanharris" # self.config_param.get(cfg.SPECTROGRAM_WINDOW_TYPE, cfg.SPECTROGRAM_DEFAULT_WINDOW_TYPE)
202
+ nfft = int(self.config_param.get(cfg.SPECTROGRAM_NFFT, cfg.SPECTROGRAM_DEFAULT_NFFT))
203
+ noverlap = self.config_param.get(cfg.SPECTROGRAM_NOVERLAP, cfg.SPECTROGRAM_DEFAULT_NOVERLAP)
204
+ vmin = self.config_param.get(cfg.SPECTROGRAM_VMIN, cfg.SPECTROGRAM_DEFAULT_VMIN)
205
+ vmax = self.config_param.get(cfg.SPECTROGRAM_VMAX, cfg.SPECTROGRAM_DEFAULT_VMAX)
206
+
203
207
  # start
204
208
  if current_time <= self.interval / 2:
205
209
  self.ax.specgram(
206
- self.sound_info[: int((self.interval) * self.frame_rate)],
210
+ self.sound_info[: int(self.interval * self.frame_rate)],
207
211
  mode="psd",
208
- # NFFT=1024,
212
+ NFFT=nfft,
209
213
  Fs=self.frame_rate,
210
- # noverlap=900,
214
+ noverlap=noverlap,
215
+ window=signal.get_window(window_type, nfft),
216
+ # matplotlib.mlab.window_hanning
217
+ # if window_type == "hanning"
218
+ # else matplotlib.mlab.window_hamming
219
+ # if window_type == "hamming"
220
+ # else matplotlib.mlab.window_blackmanharris
221
+ # if window_type == "blackmanharris"
222
+ # else matplotlib.mlab.window_hanning,
211
223
  cmap=self.spectro_color_map,
224
+ vmin=vmin,
225
+ vmax=vmax,
226
+ # mode="psd",
227
+ ## NFFT=1024,
228
+ # Fs=self.frame_rate,
229
+ ## noverlap=900,
230
+ # cmap=self.spectro_color_map,
212
231
  )
213
232
 
214
233
  self.ax.set_xlim(current_time - self.interval / 2, current_time + self.interval / 2)
@@ -222,10 +241,25 @@ class Plot_spectrogram_RT(QWidget):
222
241
  self.ax.specgram(
223
242
  self.sound_info[i:],
224
243
  mode="psd",
225
- # NFFT=1024,
244
+ NFFT=nfft,
226
245
  Fs=self.frame_rate,
227
- # noverlap=900,
246
+ noverlap=noverlap,
247
+ window=signal.get_window(window_type, nfft),
248
+ # matplotlib.mlab.window_hanning
249
+ # if window_type == "hanning"
250
+ # else matplotlib.mlab.window_hamming
251
+ # if window_type == "hamming"
252
+ # else matplotlib.mlab.window_blackmanharris
253
+ # if window_type == "blackmanharris"
254
+ # else matplotlib.mlab.window_hanning,
228
255
  cmap=self.spectro_color_map,
256
+ vmin=vmin,
257
+ vmax=vmax,
258
+ # mode="psd",
259
+ ## NFFT=1024,
260
+ # Fs=self.frame_rate,
261
+ ## noverlap=900,
262
+ # cmap=self.spectro_color_map,
229
263
  )
230
264
 
231
265
  lim1 = current_time - (self.media_length - self.interval / 2)
@@ -248,10 +282,25 @@ class Plot_spectrogram_RT(QWidget):
248
282
  )
249
283
  ],
250
284
  mode="psd",
251
- # NFFT=1024,
285
+ NFFT=nfft,
252
286
  Fs=self.frame_rate,
253
- # noverlap=900,
287
+ noverlap=noverlap,
288
+ window=signal.get_window(window_type, nfft),
289
+ # matplotlib.mlab.window_hanning
290
+ # if window_type == "hanning"
291
+ # else matplotlib.mlab.window_hamming
292
+ # if window_type == "hamming"
293
+ # else matplotlib.mlab.window_blackmanharris
294
+ # if window_type == "blackmanharris"
295
+ # else matplotlib.mlab.window_hanning,
254
296
  cmap=self.spectro_color_map,
297
+ vmin=vmin,
298
+ vmax=vmax,
299
+ # mode="psd",
300
+ ## NFFT=1024,
301
+ # Fs=self.frame_rate,
302
+ ## noverlap=900,
303
+ # cmap=self.spectro_color_map,
255
304
  )
256
305
 
257
306
  self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
boris/plot_waveform_rt.py CHANGED
@@ -25,7 +25,7 @@ import wave
25
25
  from . import config as cfg
26
26
  import matplotlib
27
27
 
28
- matplotlib.use("Qt5Agg")
28
+ matplotlib.use("QtAgg")
29
29
 
30
30
  import numpy as np
31
31
  from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel