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.
- boris/analysis_plugins/_latency.py +1 -1
- boris/config.py +10 -0
- boris/connections.py +2 -1
- boris/core.py +45 -54
- boris/core_ui.py +6 -2
- boris/external_processes.py +98 -73
- boris/import_observations.py +28 -19
- boris/observation.py +8 -4
- boris/observation_operations.py +17 -1
- boris/player_dock_widget.py +0 -24
- boris/plot_events.py +1 -1
- boris/plot_events_rt.py +1 -1
- boris/plot_spectrogram_rt.py +62 -13
- boris/plot_waveform_rt.py +1 -1
- boris/plugins.py +136 -25
- boris/preferences.py +182 -108
- boris/preferences_ui.py +216 -32
- boris/project_functions.py +1 -3
- boris/utilities.py +21 -14
- boris/version.py +2 -2
- boris/write_event.py +11 -2
- {boris_behav_obs-9.4.dist-info → boris_behav_obs-9.5.dist-info}/METADATA +4 -1
- {boris_behav_obs-9.4.dist-info → boris_behav_obs-9.5.dist-info}/RECORD +27 -28
- {boris_behav_obs-9.4.dist-info → boris_behav_obs-9.5.dist-info}/WHEEL +1 -1
- boris/1.py +0 -45
- {boris_behav_obs-9.4.dist-info → boris_behav_obs-9.5.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.4.dist-info → boris_behav_obs-9.5.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.4.dist-info → boris_behav_obs-9.5.dist-info}/top_level.txt +0 -0
boris/external_processes.py
CHANGED
|
@@ -22,7 +22,7 @@ This file is part of BORIS.
|
|
|
22
22
|
|
|
23
23
|
import os
|
|
24
24
|
import tempfile
|
|
25
|
-
|
|
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"{
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
242
|
+
self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
|
|
239
243
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
"-
|
|
249
|
-
|
|
250
|
-
"-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
286
|
-
"-codec:a",
|
|
287
|
-
"copy",
|
|
276
|
+
f"scale={horiz_resol}:-1",
|
|
288
277
|
"-b:v",
|
|
289
|
-
f"{
|
|
290
|
-
f"{file_name}.
|
|
278
|
+
f"{video_quality * 1024 * 1024}",
|
|
279
|
+
f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi",
|
|
291
280
|
]
|
|
292
281
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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])
|
boris/import_observations.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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>
|
|
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.
|
|
151
|
+
if Path(project_file_path).suffix.lower() == ".xlsx":
|
|
145
152
|
engine = "openpyxl"
|
|
146
|
-
elif Path(project_file_path).suffix.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
boris/observation_operations.py
CHANGED
|
@@ -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
|
boris/player_dock_widget.py
CHANGED
|
@@ -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
boris/plot_events_rt.py
CHANGED
boris/plot_spectrogram_rt.py
CHANGED
|
@@ -24,10 +24,10 @@ Copyright 2012-2025 Olivier Friard
|
|
|
24
24
|
import wave
|
|
25
25
|
import matplotlib
|
|
26
26
|
|
|
27
|
-
matplotlib.use("
|
|
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(
|
|
210
|
+
self.sound_info[: int(self.interval * self.frame_rate)],
|
|
207
211
|
mode="psd",
|
|
208
|
-
|
|
212
|
+
NFFT=nfft,
|
|
209
213
|
Fs=self.frame_rate,
|
|
210
|
-
|
|
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
|
-
|
|
244
|
+
NFFT=nfft,
|
|
226
245
|
Fs=self.frame_rate,
|
|
227
|
-
|
|
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
|
-
|
|
285
|
+
NFFT=nfft,
|
|
252
286
|
Fs=self.frame_rate,
|
|
253
|
-
|
|
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