boris-behav-obs 9.4.1__py2.py3-none-any.whl → 9.5.2__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
 
@@ -0,0 +1,22 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ number of occurences of behaviors
5
+ """
6
+
7
+ import pandas as pd
8
+
9
+ __version__ = "0.0.1"
10
+ __version_date__ = "2025-06-13"
11
+ __plugin_name__ = "List of dataframe columns"
12
+ __author__ = "Olivier Friard - University of Torino - Italy"
13
+
14
+
15
+ def run(df: pd.DataFrame) -> pd.DataFrame:
16
+ """
17
+ List the columns present in the dataframe
18
+ """
19
+
20
+ df_results = pd.DataFrame(df.columns, columns=["column name"])
21
+
22
+ return df_results
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))
@@ -691,11 +691,6 @@ def export_aggregated_events(pj: dict, parameters: dict, obsId: str, force_numbe
691
691
  # obs description
692
692
  obs_description = util.eol2space(observation[cfg.DESCRIPTION])
693
693
 
694
- """
695
- obs_length = observation_operations.observation_total_length(pj[cfg.OBSERVATIONS][obsId])
696
- logging.debug(f"obs_length: {obs_length}")
697
- """
698
-
699
694
  _, _, connector = db_functions.load_aggregated_events_in_db(
700
695
  pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS]
701
696
  )
@@ -798,6 +793,7 @@ def export_aggregated_events(pj: dict, parameters: dict, obsId: str, force_numbe
798
793
  if observation[cfg.TYPE] == cfg.MEDIA:
799
794
  observation_type = "Media file"
800
795
 
796
+ # get the media file name of the start of event
801
797
  media_file_name = observation_operations.event2media_file_name(observation, row["start"])
802
798
  if media_file_name is None:
803
799
  media_file_name = "Not found"
@@ -842,7 +838,7 @@ def export_aggregated_events(pj: dict, parameters: dict, obsId: str, force_numbe
842
838
  observation["date"].replace("T", " "),
843
839
  obs_description,
844
840
  observation_type,
845
- media_file_str,
841
+ media_file_str, # list of media used in observation
846
842
  pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET],
847
843
  f"{coding_duration:.3f}" if not coding_duration.is_nan() else cfg.NA,
848
844
  media_durations_str,
@@ -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
@@ -1055,8 +1055,6 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
1055
1055
  }
1056
1056
 
1057
1057
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_CREATION_DATE_AS_OFFSET]:
1058
- print("\n", observationWindow.media_creation_time, "\n")
1059
-
1060
1058
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME] = observationWindow.media_creation_time
1061
1059
 
1062
1060
  try:
@@ -2423,34 +2421,55 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2423
2421
  timestamp (dec): time stamp
2424
2422
 
2425
2423
  Returns:
2426
- str: name of media file containing the event
2424
+ str: path of media file containing the event
2427
2425
  """
2426
+ if observation.get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
2427
+ # media creation date/time was used for coding
2428
+ video_file_name = None
2429
+ for media_path in observation[cfg.MEDIA_INFO].get(cfg.MEDIA_CREATION_TIME, {}):
2430
+ start_media = observation[cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_path]
2431
+ duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_path]
2432
+ if start_media <= timestamp <= start_media + duration:
2433
+ video_file_name = media_path
2434
+ break
2428
2435
 
2429
- cumul_media_durations: list = [dec(0)]
2430
- for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2431
- try:
2432
- media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2433
- cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2434
- except KeyError:
2435
- return None
2436
+ else: # no media creation date
2437
+ cumul_media_durations: list = [dec(0)]
2438
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2439
+ try:
2440
+ media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
2441
+ # cut off media duration to 3 decimal places as that is how fine the player is
2442
+ media_duration = floor(media_duration * 10**3) / dec(10**3)
2443
+ cumul_media_durations.append(floor((cumul_media_durations[-1] + media_duration) * 10**3) / dec(10**3))
2444
+ except KeyError:
2445
+ return None
2446
+
2447
+ """
2448
+ cumul_media_durations: list = [dec(0)]
2449
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2450
+ try:
2451
+ media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2452
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2453
+ except KeyError:
2454
+ return None
2455
+ """
2436
2456
 
2437
- cumul_media_durations.remove(dec(0))
2457
+ cumul_media_durations.remove(dec(0))
2438
2458
 
2439
- # test if timestamp is at end of last media
2440
- if timestamp == cumul_media_durations[-1]:
2441
- player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2442
- else:
2443
- player_idx = -1
2444
- for idx, value in enumerate(cumul_media_durations):
2445
- start = 0 if idx == 0 else cumul_media_durations[idx - 1]
2446
- if start <= timestamp < value:
2447
- player_idx = idx
2448
- break
2459
+ logging.debug(f"{cumul_media_durations=}")
2449
2460
 
2450
- if player_idx != -1:
2451
- video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx]
2452
- else:
2453
- video_file_name = None
2461
+ # test if timestamp is at end of last media
2462
+ if timestamp == cumul_media_durations[-1]:
2463
+ player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2464
+ else:
2465
+ player_idx = None
2466
+ for idx, value in enumerate(cumul_media_durations):
2467
+ start = 0 if idx == 0 else cumul_media_durations[idx - 1]
2468
+ if start <= timestamp < value:
2469
+ player_idx = idx
2470
+ break
2471
+
2472
+ video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx] if player_idx is not None else None
2454
2473
 
2455
2474
  return video_file_name
2456
2475
 
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