boris-behav-obs 9.4__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.
@@ -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
+ )
boris/observation.py CHANGED
@@ -1241,9 +1241,11 @@ class Observation(QDialog, Ui_Form):
1241
1241
  str: error message or empty string
1242
1242
  """
1243
1243
 
1244
+ logging.debug(f"check_media function for {file_path}")
1245
+
1244
1246
  media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
1245
1247
 
1246
- print(f"{media_info=}")
1248
+ logging.debug(f"{media_info=}")
1247
1249
 
1248
1250
  if "error" in media_info:
1249
1251
  return (True, media_info["error"])
@@ -1265,6 +1267,9 @@ class Observation(QDialog, Ui_Form):
1265
1267
  self.mediaFPS[file_path] = float(media_info["fps"])
1266
1268
  self.mediaHasVideo[file_path] = media_info["has_video"]
1267
1269
  self.mediaHasAudio[file_path] = media_info["has_audio"]
1270
+
1271
+ logging.debug(f"{file_path=}")
1272
+
1268
1273
  self.add_media_to_listview(file_path)
1269
1274
  return (False, "")
1270
1275
 
@@ -1324,6 +1329,8 @@ class Observation(QDialog, Ui_Form):
1324
1329
  if "media " in mode:
1325
1330
  file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
1326
1331
 
1332
+ logging.debug(f"{file_paths=}")
1333
+
1327
1334
  if file_paths:
1328
1335
  # store directory for next usage
1329
1336
  self.mem_dir = str(pl.Path(file_paths[0]).parent)
@@ -1341,9 +1348,6 @@ class Observation(QDialog, Ui_Form):
1341
1348
 
1342
1349
  for file_path in file_paths:
1343
1350
  (error, msg) = self.check_media(file_path, mode)
1344
-
1345
- print(f"{(error, msg)=}")
1346
-
1347
1351
  if error:
1348
1352
  QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
1349
1353
 
@@ -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,6 +1055,8 @@ 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
+
1058
1060
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME] = observationWindow.media_creation_time
1059
1061
 
1060
1062
  try:
@@ -2424,6 +2426,17 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2424
2426
  str: name of media file containing the event
2425
2427
  """
2426
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
+ """
2427
2440
  cumul_media_durations: list = [dec(0)]
2428
2441
  for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2429
2442
  try:
@@ -2431,9 +2444,12 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2431
2444
  cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2432
2445
  except KeyError:
2433
2446
  return None
2447
+ """
2434
2448
 
2435
2449
  cumul_media_durations.remove(dec(0))
2436
2450
 
2451
+ logging.debug(f"{cumul_media_durations=}")
2452
+
2437
2453
  # test if timestamp is at end of last media
2438
2454
  if timestamp == cumul_media_durations[-1]:
2439
2455
  player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
@@ -43,30 +43,6 @@ from PySide6.QtCore import Signal, QEvent, Qt
43
43
  from PySide6.QtGui import QIcon, QAction
44
44
 
45
45
 
46
- """
47
- try:
48
- # import last version of python-mpv
49
- from . import mpv2 as mpv
50
-
51
- # check if MPV API v. 1
52
- # is v. 1 use the old version of mpv.py
53
- try:
54
- if "libmpv.so.1" in mpv.sofile:
55
- from . import mpv as mpv
56
- except AttributeError:
57
- if "mpv-1.dll" in mpv.dll:
58
- from . import mpv as mpv
59
-
60
- except RuntimeError: # libmpv found but version too old
61
- from . import mpv as mpv
62
-
63
- except OSError: # libmpv not found
64
- msg = "LIBMPV library not found!\n"
65
- logging.critical(msg)
66
- sys.exit()
67
- """
68
-
69
-
70
46
  class Clickable_label(QLabel):
71
47
  """
72
48
  QLabel class for visualiziong frames for geometric measurments
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