boris-behav-obs 9.7.1__py3-none-any.whl → 9.7.15__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/about.py +5 -5
- boris/add_modifier_ui.py +47 -29
- boris/analysis_plugins/export_to_feral.py +336 -0
- boris/behav_coding_map_creator.py +7 -7
- boris/coding_pad.py +4 -3
- boris/config.py +12 -2
- boris/config_file.py +3 -3
- boris/converters_ui.py +2 -3
- boris/core.py +223 -182
- boris/ipc_mpv.py +52 -31
- boris/modifier_coding_map_creator.py +6 -6
- boris/observation.py +8 -1
- boris/observation_operations.py +68 -43
- boris/plot_data_module.py +2 -0
- boris/plot_spectrogram_rt.py +43 -71
- boris/plot_waveform_rt.py +4 -1
- boris/plugins.py +60 -23
- boris/preferences.py +43 -3
- boris/preferences_ui.py +95 -45
- boris/project.py +1 -1
- boris/project_functions.py +33 -20
- boris/subjects_pad.py +2 -2
- boris/utilities.py +32 -4
- boris/version.py +2 -2
- boris/video_operations.py +9 -1
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.15.dist-info}/METADATA +3 -4
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.15.dist-info}/RECORD +31 -30
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.15.dist-info}/WHEEL +0 -0
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.15.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.15.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.15.dist-info}/top_level.txt +0 -0
boris/ipc_mpv.py
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
1
23
|
import socket
|
|
2
24
|
import json
|
|
3
25
|
import subprocess
|
|
4
26
|
|
|
5
|
-
# from PySide6.QtCore import QTimer
|
|
6
27
|
import logging
|
|
7
28
|
import config as cfg
|
|
8
29
|
|
|
@@ -36,11 +57,13 @@ class IPC_MPV:
|
|
|
36
57
|
self.process = subprocess.Popen(
|
|
37
58
|
[
|
|
38
59
|
"mpv",
|
|
60
|
+
"--ontop",
|
|
39
61
|
"--no-border",
|
|
40
62
|
"--osc=no", # no on screen commands
|
|
41
63
|
"--input-ipc-server=" + self.socket_path,
|
|
42
64
|
# "--wid=" + str(int(self.winId())), # Embed in the widget
|
|
43
|
-
"--idle", # Keeps mpv running with no video
|
|
65
|
+
"--idle=yes", # Keeps mpv running with no video
|
|
66
|
+
"--keep-open=always",
|
|
44
67
|
"--input-default-bindings=no",
|
|
45
68
|
"--input-vo-keyboard=no",
|
|
46
69
|
],
|
|
@@ -48,30 +71,6 @@ class IPC_MPV:
|
|
|
48
71
|
stderr=subprocess.PIPE,
|
|
49
72
|
)
|
|
50
73
|
|
|
51
|
-
'''
|
|
52
|
-
def init_socket(self):
|
|
53
|
-
"""
|
|
54
|
-
Initialize the JSON IPC socket.
|
|
55
|
-
"""
|
|
56
|
-
print("init socket")
|
|
57
|
-
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
58
|
-
QTimer.singleShot(5000, self.connect_socket) # Allow time for mpv to initialize
|
|
59
|
-
'''
|
|
60
|
-
|
|
61
|
-
'''
|
|
62
|
-
def connect_socket(self):
|
|
63
|
-
"""
|
|
64
|
-
Connect to the mpv IPC socket.
|
|
65
|
-
"""
|
|
66
|
-
print("connect socket")
|
|
67
|
-
try:
|
|
68
|
-
self.sock.connect(self.socket_path)
|
|
69
|
-
print("Connected to mpv IPC server.")
|
|
70
|
-
except socket.error as e:
|
|
71
|
-
print(f"Failed to connect to mpv IPC server: {e}")
|
|
72
|
-
print("end of connect_socket fucntion")
|
|
73
|
-
'''
|
|
74
|
-
|
|
75
74
|
def send_command(self, command):
|
|
76
75
|
"""
|
|
77
76
|
Send a JSON command to the mpv IPC server.
|
|
@@ -92,9 +91,7 @@ class IPC_MPV:
|
|
|
92
91
|
# Parse the response as JSON
|
|
93
92
|
response_data = json.loads(response.decode("utf-8"))
|
|
94
93
|
if response_data["error"] != "success":
|
|
95
|
-
|
|
96
|
-
print(f"{response_data=}")
|
|
97
|
-
print()
|
|
94
|
+
logging.warning(f"send command: {command} response data: {response_data}")
|
|
98
95
|
# Return the 'data' field which contains the playback position
|
|
99
96
|
return response_data.get("data")
|
|
100
97
|
except FileNotFoundError:
|
|
@@ -128,7 +125,6 @@ class IPC_MPV:
|
|
|
128
125
|
|
|
129
126
|
@pause.setter
|
|
130
127
|
def pause(self, value):
|
|
131
|
-
print(f"set pause to {value}")
|
|
132
128
|
return self.send_command({"command": ["set_property", "pause", value]})
|
|
133
129
|
|
|
134
130
|
@property
|
|
@@ -143,6 +139,14 @@ class IPC_MPV:
|
|
|
143
139
|
def playlist(self):
|
|
144
140
|
return self.send_command({"command": ["get_property", "playlist"]})
|
|
145
141
|
|
|
142
|
+
def playlist_next(self):
|
|
143
|
+
self.send_command({"command": ["playlist-next"]})
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
def playlist_prev(self):
|
|
147
|
+
self.send_command({"command": ["playlist-prev"]})
|
|
148
|
+
return
|
|
149
|
+
|
|
146
150
|
@property
|
|
147
151
|
def playlist_pos(self):
|
|
148
152
|
return self.send_command({"command": ["get_property", "playlist-pos"]})
|
|
@@ -168,7 +172,6 @@ class IPC_MPV:
|
|
|
168
172
|
@property
|
|
169
173
|
def playback_time(self):
|
|
170
174
|
playback_time_ = self.send_command({"command": ["get_property", "playback-time"]})
|
|
171
|
-
print(f"playback_time: {playback_time_}")
|
|
172
175
|
return playback_time_
|
|
173
176
|
|
|
174
177
|
def frame_step(self):
|
|
@@ -292,6 +295,24 @@ class IPC_MPV:
|
|
|
292
295
|
def core_idle(self):
|
|
293
296
|
return self.send_command({"command": ["get_property", "core-idle"]})
|
|
294
297
|
|
|
298
|
+
@property
|
|
299
|
+
def video_pan_x(self):
|
|
300
|
+
return self.send_command({"command": ["get_property", "video-pan-x"]})
|
|
301
|
+
|
|
302
|
+
@video_pan_x.setter
|
|
303
|
+
def video_pan_x(self, value):
|
|
304
|
+
self.send_command({"command": ["set_property", "video-pan-x", value]})
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def video_pan_y(self):
|
|
309
|
+
return self.send_command({"command": ["get_property", "video-pan-y"]})
|
|
310
|
+
|
|
311
|
+
@video_pan_y.setter
|
|
312
|
+
def video_pan_y(self, value):
|
|
313
|
+
self.send_command({"command": ["set_property", "video-pan-y", value]})
|
|
314
|
+
return
|
|
315
|
+
|
|
295
316
|
"""
|
|
296
317
|
@property
|
|
297
318
|
def xxx(self):
|
|
@@ -134,7 +134,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
|
|
|
134
134
|
self.saveMapAction.setShortcut("Ctrl+S")
|
|
135
135
|
self.saveMapAction.setStatusTip("Save modifiers map")
|
|
136
136
|
self.saveMapAction.setEnabled(False)
|
|
137
|
-
self.saveMapAction.triggered.connect(self.
|
|
137
|
+
self.saveMapAction.triggered.connect(self.save_map_clicked)
|
|
138
138
|
|
|
139
139
|
self.saveAsMapAction = QAction(QIcon(), "Save modifiers map as", self)
|
|
140
140
|
self.saveAsMapAction.setStatusTip("Save modifiers map as")
|
|
@@ -343,7 +343,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
|
|
|
343
343
|
)
|
|
344
344
|
|
|
345
345
|
if response == cfg.SAVE:
|
|
346
|
-
if not self.
|
|
346
|
+
if not self.save_map_clicked():
|
|
347
347
|
event.ignore()
|
|
348
348
|
|
|
349
349
|
if response == cfg.CANCEL:
|
|
@@ -567,7 +567,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
|
|
|
567
567
|
)
|
|
568
568
|
|
|
569
569
|
if response == "Save":
|
|
570
|
-
if not self.
|
|
570
|
+
if not self.save_map_clicked():
|
|
571
571
|
return
|
|
572
572
|
|
|
573
573
|
if response == "Cancel":
|
|
@@ -606,7 +606,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
|
|
|
606
606
|
)
|
|
607
607
|
|
|
608
608
|
if response == cfg.SAVE:
|
|
609
|
-
if not self.
|
|
609
|
+
if not self.save_map_clicked():
|
|
610
610
|
return
|
|
611
611
|
|
|
612
612
|
if response == cfg.CANCEL:
|
|
@@ -733,7 +733,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
|
|
|
733
733
|
self.fileName += ".boris_map"
|
|
734
734
|
self.saveMap()
|
|
735
735
|
|
|
736
|
-
def
|
|
736
|
+
def save_map_clicked(self):
|
|
737
737
|
if not self.fileName:
|
|
738
738
|
fn = QFileDialog(self).getSaveFileName(
|
|
739
739
|
self,
|
|
@@ -746,7 +746,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
|
|
|
746
746
|
else:
|
|
747
747
|
self.fileName = fn
|
|
748
748
|
|
|
749
|
-
if self.fileName and Path(self.fileName).suffix
|
|
749
|
+
if self.fileName and Path(self.fileName).suffix != ".boris_map":
|
|
750
750
|
self.fileName += ".boris_map"
|
|
751
751
|
|
|
752
752
|
if self.fileName:
|
boris/observation.py
CHANGED
|
@@ -42,6 +42,7 @@ from PySide6.QtWidgets import (
|
|
|
42
42
|
QApplication,
|
|
43
43
|
QMenu,
|
|
44
44
|
QListWidgetItem,
|
|
45
|
+
QHeaderView,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
from . import config as cfg
|
|
@@ -75,7 +76,10 @@ class AssignConverter(QDialog):
|
|
|
75
76
|
self.cbb[-1].addItems(["None"] + sorted(converters.keys()))
|
|
76
77
|
|
|
77
78
|
if column_idx in col_conv:
|
|
78
|
-
|
|
79
|
+
if col_conv[column_idx] in (["None"] + sorted(converters.keys())):
|
|
80
|
+
self.cbb[-1].setCurrentIndex((["None"] + sorted(converters.keys())).index(col_conv[column_idx]))
|
|
81
|
+
else:
|
|
82
|
+
self.cbb[-1].setCurrentIndex(0)
|
|
79
83
|
else:
|
|
80
84
|
self.cbb[-1].setCurrentIndex(0)
|
|
81
85
|
hbox.addWidget(self.cbb[-1])
|
|
@@ -224,6 +228,9 @@ class Observation(QDialog, Ui_Form):
|
|
|
224
228
|
self.pbCancel.clicked.connect(self.pbCancel_clicked)
|
|
225
229
|
|
|
226
230
|
self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
|
|
231
|
+
self.tw_data_files.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
232
|
+
|
|
233
|
+
self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
227
234
|
|
|
228
235
|
self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio, self.media_creation_time = {}, {}, {}, {}, {}
|
|
229
236
|
|
boris/observation_operations.py
CHANGED
|
@@ -26,7 +26,7 @@ from decimal import Decimal as dec
|
|
|
26
26
|
import json
|
|
27
27
|
from math import log2, floor
|
|
28
28
|
import os
|
|
29
|
-
|
|
29
|
+
from pathlib import Path
|
|
30
30
|
import socket
|
|
31
31
|
import subprocess
|
|
32
32
|
import sys
|
|
@@ -89,10 +89,10 @@ def export_observations_list_clicked(self):
|
|
|
89
89
|
return
|
|
90
90
|
|
|
91
91
|
output_format = cfg.FILE_NAME_SUFFIX[filter_]
|
|
92
|
-
if
|
|
93
|
-
file_name = str(
|
|
92
|
+
if Path(file_name).suffix != "." + output_format:
|
|
93
|
+
file_name = str(Path(file_name)) + "." + output_format
|
|
94
94
|
# check if file name with extension already exists
|
|
95
|
-
if
|
|
95
|
+
if Path(file_name).is_file():
|
|
96
96
|
if dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
|
|
97
97
|
return
|
|
98
98
|
|
|
@@ -113,6 +113,8 @@ def observations_list(self):
|
|
|
113
113
|
result, selected_obs = select_observations.select_observations2(self, cfg.SINGLE)
|
|
114
114
|
|
|
115
115
|
if not selected_obs:
|
|
116
|
+
# activate main window
|
|
117
|
+
self.activateWindow()
|
|
116
118
|
return
|
|
117
119
|
|
|
118
120
|
if self.observationId:
|
|
@@ -122,6 +124,9 @@ def observations_list(self):
|
|
|
122
124
|
)
|
|
123
125
|
if response == cfg.NO:
|
|
124
126
|
self.show_data_files()
|
|
127
|
+
# activate main window
|
|
128
|
+
self.activateWindow()
|
|
129
|
+
|
|
125
130
|
return ""
|
|
126
131
|
else:
|
|
127
132
|
close_observation(self)
|
|
@@ -145,6 +150,8 @@ def observations_list(self):
|
|
|
145
150
|
)
|
|
146
151
|
|
|
147
152
|
logging.debug("end observations list")
|
|
153
|
+
# activate main window
|
|
154
|
+
self.activateWindow()
|
|
148
155
|
|
|
149
156
|
|
|
150
157
|
def open_observation(self, mode: str) -> str:
|
|
@@ -594,7 +601,7 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
|
|
|
594
601
|
close_observation(self)
|
|
595
602
|
|
|
596
603
|
observationWindow = observation.Observation(
|
|
597
|
-
tmp_dir=self.ffmpeg_cache_dir if (self.ffmpeg_cache_dir and
|
|
604
|
+
tmp_dir=self.ffmpeg_cache_dir if (self.ffmpeg_cache_dir and Path(self.ffmpeg_cache_dir).is_dir()) else tempfile.gettempdir(),
|
|
598
605
|
project_path=self.projectFileName,
|
|
599
606
|
converters=self.pj.get(cfg.CONVERTERS, {}),
|
|
600
607
|
time_format=self.timeFormat,
|
|
@@ -1151,6 +1158,9 @@ def close_observation(self):
|
|
|
1151
1158
|
logging.info("Stop plot timer")
|
|
1152
1159
|
self.plot_timer.stop()
|
|
1153
1160
|
|
|
1161
|
+
if self.MPV_IPC_MODE:
|
|
1162
|
+
self.main_window_activation_timer.stop()
|
|
1163
|
+
|
|
1154
1164
|
for i, player in enumerate(self.dw_player):
|
|
1155
1165
|
if (
|
|
1156
1166
|
str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
|
|
@@ -1304,7 +1314,7 @@ def check_creation_date(self) -> Tuple[int, dict]:
|
|
|
1304
1314
|
|
|
1305
1315
|
if ret == 1: # use file creation time
|
|
1306
1316
|
for media in not_tagged_media_list:
|
|
1307
|
-
media_creation_time[media] =
|
|
1317
|
+
media_creation_time[media] = Path(media).stat().st_ctime
|
|
1308
1318
|
return (0, media_creation_time) # OK use media file creation date/time
|
|
1309
1319
|
else:
|
|
1310
1320
|
return (1, {})
|
|
@@ -1340,8 +1350,6 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1340
1350
|
self.playerType = cfg.VIEWER_MEDIA
|
|
1341
1351
|
return True
|
|
1342
1352
|
|
|
1343
|
-
# print(f"{self.process=}")
|
|
1344
|
-
|
|
1345
1353
|
self.playerType = cfg.MEDIA
|
|
1346
1354
|
self.fps = 0
|
|
1347
1355
|
|
|
@@ -1365,7 +1373,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1365
1373
|
|
|
1366
1374
|
# add all media files to media lists
|
|
1367
1375
|
self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)
|
|
1368
|
-
self.dw_player
|
|
1376
|
+
self.dw_player = []
|
|
1369
1377
|
|
|
1370
1378
|
# check if media creation time used as offset
|
|
1371
1379
|
# TODO check if cfg.MEDIA_CREATION_TIME dict is present
|
|
@@ -1831,8 +1839,8 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1831
1839
|
self.dw_player[i].resize_signal.connect(self.resize_dw)
|
|
1832
1840
|
|
|
1833
1841
|
# add durations list
|
|
1834
|
-
self.dw_player[i].media_durations
|
|
1835
|
-
self.dw_player[i].cumul_media_durations
|
|
1842
|
+
self.dw_player[i].media_durations = []
|
|
1843
|
+
self.dw_player[i].cumul_media_durations = [0] # [idx for idx,x in enumerate(l) if l[idx-1]<pos<=x]
|
|
1836
1844
|
|
|
1837
1845
|
# add fps list
|
|
1838
1846
|
self.dw_player[i].fps = {}
|
|
@@ -1844,6 +1852,13 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1844
1852
|
if r:
|
|
1845
1853
|
break
|
|
1846
1854
|
|
|
1855
|
+
# start timer for activating the main window
|
|
1856
|
+
self.main_window_activation_timer = QTimer()
|
|
1857
|
+
self.main_window_activation_timer.setInterval(500)
|
|
1858
|
+
# self.main_window_activation_timer.timeout.connect(self.activateWindow)
|
|
1859
|
+
self.main_window_activation_timer.timeout.connect(self.activate_main_window)
|
|
1860
|
+
self.main_window_activation_timer.start()
|
|
1861
|
+
|
|
1847
1862
|
for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
|
|
1848
1863
|
logging.debug(f"media file: {mediaFile}")
|
|
1849
1864
|
|
|
@@ -1887,7 +1902,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1887
1902
|
self.dw_player[i].player.playlist_append(media_full_path)
|
|
1888
1903
|
|
|
1889
1904
|
# add media file name to player window title
|
|
1890
|
-
self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({
|
|
1905
|
+
self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({Path(media_full_path).name})")
|
|
1891
1906
|
|
|
1892
1907
|
# media duration cumuled in seconds
|
|
1893
1908
|
self.dw_player[i].cumul_media_durations_sec = [round(dec(x / 1000), 3) for x in self.dw_player[i].cumul_media_durations]
|
|
@@ -1964,7 +1979,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1964
1979
|
|
|
1965
1980
|
menu_options.update_menu(self)
|
|
1966
1981
|
|
|
1967
|
-
if
|
|
1982
|
+
if self.MPV_IPC_MODE:
|
|
1968
1983
|
# activate timer
|
|
1969
1984
|
self.ipc_mpv_timer = QTimer()
|
|
1970
1985
|
self.ipc_mpv_timer.setInterval(500)
|
|
@@ -1996,7 +2011,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1996
2011
|
tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
|
|
1997
2012
|
|
|
1998
2013
|
wav_file_path = (
|
|
1999
|
-
|
|
2014
|
+
Path(tmp_dir) / Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
|
|
2000
2015
|
)
|
|
2001
2016
|
|
|
2002
2017
|
if not wav_file_path.is_file():
|
|
@@ -2012,7 +2027,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
2012
2027
|
tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
|
|
2013
2028
|
|
|
2014
2029
|
wav_file_path = (
|
|
2015
|
-
|
|
2030
|
+
Path(tmp_dir) / Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
|
|
2016
2031
|
)
|
|
2017
2032
|
|
|
2018
2033
|
if not wav_file_path.is_file():
|
|
@@ -2296,8 +2311,8 @@ def initialize_new_images_observation(self):
|
|
|
2296
2311
|
sorted(
|
|
2297
2312
|
list(
|
|
2298
2313
|
set(
|
|
2299
|
-
[str(x) for x in
|
|
2300
|
-
+ [str(x) for x in
|
|
2314
|
+
[str(x) for x in Path(full_dir_path).glob(pattern)]
|
|
2315
|
+
+ [str(x) for x in Path(full_dir_path).glob(pattern.upper())]
|
|
2301
2316
|
)
|
|
2302
2317
|
)
|
|
2303
2318
|
)
|
|
@@ -2422,26 +2437,31 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2422
2437
|
|
|
2423
2438
|
def create_observations(self):
|
|
2424
2439
|
"""
|
|
2425
|
-
Create observations from a media
|
|
2440
|
+
Create observations from a directory of media files
|
|
2426
2441
|
"""
|
|
2427
|
-
# print(self.pj[cfg.OBSERVATIONS])
|
|
2428
2442
|
|
|
2429
|
-
dir_path
|
|
2430
|
-
if not dir_path:
|
|
2443
|
+
if not (dir_path := QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))):
|
|
2431
2444
|
return
|
|
2432
2445
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
elements_list
|
|
2446
|
+
elements_list: list = []
|
|
2447
|
+
if util.is_subdir(Path(dir_path), Path(self.projectFileName).parent):
|
|
2448
|
+
elements_list.append(("cb", "Use relative paths", False))
|
|
2449
|
+
|
|
2450
|
+
elements_list.extend(
|
|
2451
|
+
[
|
|
2436
2452
|
("cb", "Recurse the subdirectories", False),
|
|
2437
|
-
("cb", "Save the absolute media file path", True),
|
|
2438
2453
|
("cb", "Visualize spectrogram", False),
|
|
2439
2454
|
("cb", "Visualize waveform", False),
|
|
2440
2455
|
("cb", "Media creation date as offset", False),
|
|
2441
2456
|
("cb", "Close behaviors between videos", False),
|
|
2442
2457
|
("dsb", "Time offset (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
2443
2458
|
("dsb", "Media scan sampling duration (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
2444
|
-
]
|
|
2459
|
+
]
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2462
|
+
dlg = dialog.Input_dialog(
|
|
2463
|
+
label_caption="Set the following observation parameters",
|
|
2464
|
+
elements_list=elements_list,
|
|
2445
2465
|
title="Observation parameters",
|
|
2446
2466
|
)
|
|
2447
2467
|
if not dlg.exec_():
|
|
@@ -2450,9 +2470,9 @@ def create_observations(self):
|
|
|
2450
2470
|
file_count: int = 0
|
|
2451
2471
|
|
|
2452
2472
|
if dlg.elements["Recurse the subdirectories"].isChecked():
|
|
2453
|
-
files_list =
|
|
2473
|
+
files_list = Path(dir_path).rglob("*")
|
|
2454
2474
|
else:
|
|
2455
|
-
files_list =
|
|
2475
|
+
files_list = Path(dir_path).glob("*")
|
|
2456
2476
|
|
|
2457
2477
|
for file in files_list:
|
|
2458
2478
|
if not file.is_file():
|
|
@@ -2462,22 +2482,25 @@ def create_observations(self):
|
|
|
2462
2482
|
if not r.get("frames_number", 0):
|
|
2463
2483
|
continue
|
|
2464
2484
|
|
|
2465
|
-
if dlg.elements["
|
|
2466
|
-
media_file = str(file)
|
|
2485
|
+
if "Use relative paths" in dlg.elements and dlg.elements["Use relative paths"].isChecked():
|
|
2486
|
+
media_file = str(file.relative_to(Path(self.projectFileName).parent))
|
|
2467
2487
|
else:
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2488
|
+
media_file = str(file)
|
|
2489
|
+
|
|
2490
|
+
# else:
|
|
2491
|
+
# try:
|
|
2492
|
+
# media_file = str(file.relative_to(Path(self.projectFileName).parent))
|
|
2493
|
+
# except ValueError:
|
|
2494
|
+
# QMessageBox.critical(
|
|
2495
|
+
# self,
|
|
2496
|
+
# cfg.programName,
|
|
2497
|
+
# (
|
|
2498
|
+
# f"the media file <b>{file}</b> can not be relative to the project directory "
|
|
2499
|
+
# f"(<b>{Path(self.projectFileName).parent}</b>)"
|
|
2500
|
+
# "<br><br>Aborting the creation of observations"
|
|
2501
|
+
# ),
|
|
2502
|
+
# )
|
|
2503
|
+
# return
|
|
2481
2504
|
|
|
2482
2505
|
if media_file in self.pj[cfg.OBSERVATIONS]:
|
|
2483
2506
|
QMessageBox.critical(
|
|
@@ -2518,4 +2541,6 @@ def create_observations(self):
|
|
|
2518
2541
|
else:
|
|
2519
2542
|
message: str = f"No media file were found in {dir_path}"
|
|
2520
2543
|
|
|
2544
|
+
menu_options.update_menu(self)
|
|
2545
|
+
|
|
2521
2546
|
QMessageBox.information(self, cfg.programName, message)
|
boris/plot_data_module.py
CHANGED
boris/plot_spectrogram_rt.py
CHANGED
|
@@ -183,7 +183,7 @@ class Plot_spectrogram_RT(QWidget):
|
|
|
183
183
|
|
|
184
184
|
return {"media_length": self.media_length, "frame_rate": self.frame_rate}
|
|
185
185
|
|
|
186
|
-
def plot_spectro(self, current_time: float, force_plot: bool = False) -> tuple[float, bool]:
|
|
186
|
+
def plot_spectro(self, current_time: float | None, force_plot: bool = False) -> tuple[float, bool] | None:
|
|
187
187
|
"""
|
|
188
188
|
plot sound spectrogram centered on the current time
|
|
189
189
|
|
|
@@ -192,45 +192,54 @@ class Plot_spectrogram_RT(QWidget):
|
|
|
192
192
|
force_plot (bool): force plot even if media paused
|
|
193
193
|
"""
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
def spectrogram(self, x, window_type, nfft, noverlap, vmin, vmax) -> None:
|
|
196
|
+
window = matplotlib.mlab.window_hanning
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
if window_type == "hanning":
|
|
199
|
+
window = matplotlib.mlab.window_hanning
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
if window_type == "hamming":
|
|
202
|
+
window = signal.get_window(window_type, nfft)
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
noverlap = self.config_param.get(cfg.SPECTROGRAM_NOVERLAP, cfg.SPECTROGRAM_DEFAULT_NOVERLAP)
|
|
205
|
-
vmin = self.config_param.get(cfg.SPECTROGRAM_VMIN, cfg.SPECTROGRAM_DEFAULT_VMIN)
|
|
206
|
-
vmax = self.config_param.get(cfg.SPECTROGRAM_VMAX, cfg.SPECTROGRAM_DEFAULT_VMAX)
|
|
204
|
+
if window_type == "blackmanharris":
|
|
205
|
+
window = signal.get_window(window_type, nfft)
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
if current_time <= self.interval / 2:
|
|
207
|
+
# print(f"{self.frame_rate=} {vmin=} {vmax=} {window_type=} {nfft=} {noverlap=}")
|
|
210
208
|
self.ax.specgram(
|
|
211
|
-
|
|
209
|
+
x,
|
|
212
210
|
mode="psd",
|
|
213
211
|
NFFT=nfft,
|
|
214
212
|
Fs=self.frame_rate,
|
|
215
213
|
noverlap=noverlap,
|
|
216
|
-
window=
|
|
217
|
-
# matplotlib.mlab.window_hanning
|
|
218
|
-
# if window_type == "hanning"
|
|
219
|
-
# else matplotlib.mlab.window_hamming
|
|
220
|
-
# if window_type == "hamming"
|
|
221
|
-
# else matplotlib.mlab.window_blackmanharris
|
|
222
|
-
# if window_type == "blackmanharris"
|
|
223
|
-
# else matplotlib.mlab.window_hanning,
|
|
214
|
+
window=window,
|
|
224
215
|
cmap=self.spectro_color_map,
|
|
225
216
|
vmin=vmin,
|
|
226
217
|
vmax=vmax,
|
|
227
|
-
# mode="psd",
|
|
228
|
-
## NFFT=1024,
|
|
229
|
-
# Fs=self.frame_rate,
|
|
230
|
-
## noverlap=900,
|
|
231
|
-
# cmap=self.spectro_color_map,
|
|
232
218
|
)
|
|
233
219
|
|
|
220
|
+
if not force_plot and current_time == self.time_mem:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
self.time_mem = current_time
|
|
224
|
+
|
|
225
|
+
self.ax.clear()
|
|
226
|
+
|
|
227
|
+
window_type = self.config_param.get(cfg.SPECTROGRAM_WINDOW_TYPE, cfg.SPECTROGRAM_DEFAULT_WINDOW_TYPE)
|
|
228
|
+
nfft = int(self.config_param.get(cfg.SPECTROGRAM_NFFT, cfg.SPECTROGRAM_DEFAULT_NFFT))
|
|
229
|
+
noverlap = self.config_param.get(cfg.SPECTROGRAM_NOVERLAP, cfg.SPECTROGRAM_DEFAULT_NOVERLAP)
|
|
230
|
+
if self.config_param.get(cfg.SPECTROGRAM_USE_VMIN_VMAX, cfg.SPECTROGRAM_USE_VMIN_VMAX_DEFAULT):
|
|
231
|
+
vmin = self.config_param.get(cfg.SPECTROGRAM_VMIN, cfg.SPECTROGRAM_DEFAULT_VMIN)
|
|
232
|
+
vmax = self.config_param.get(cfg.SPECTROGRAM_VMAX, cfg.SPECTROGRAM_DEFAULT_VMAX)
|
|
233
|
+
else:
|
|
234
|
+
vmin, vmax = None, None
|
|
235
|
+
|
|
236
|
+
if current_time is None:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# start
|
|
240
|
+
if current_time <= self.interval / 2:
|
|
241
|
+
spectrogram(self, self.sound_info[: int(self.interval * self.frame_rate)], window_type, nfft, noverlap, vmin, vmax)
|
|
242
|
+
|
|
234
243
|
self.ax.set_xlim(current_time - self.interval / 2, current_time + self.interval / 2)
|
|
235
244
|
|
|
236
245
|
# cursor
|
|
@@ -239,29 +248,7 @@ class Plot_spectrogram_RT(QWidget):
|
|
|
239
248
|
elif current_time >= self.media_length - self.interval / 2:
|
|
240
249
|
i = int(round(len(self.sound_info) - (self.interval * self.frame_rate), 0))
|
|
241
250
|
|
|
242
|
-
self.
|
|
243
|
-
self.sound_info[i:],
|
|
244
|
-
mode="psd",
|
|
245
|
-
NFFT=nfft,
|
|
246
|
-
Fs=self.frame_rate,
|
|
247
|
-
noverlap=noverlap,
|
|
248
|
-
window=signal.get_window(window_type, nfft),
|
|
249
|
-
# matplotlib.mlab.window_hanning
|
|
250
|
-
# if window_type == "hanning"
|
|
251
|
-
# else matplotlib.mlab.window_hamming
|
|
252
|
-
# if window_type == "hamming"
|
|
253
|
-
# else matplotlib.mlab.window_blackmanharris
|
|
254
|
-
# if window_type == "blackmanharris"
|
|
255
|
-
# else matplotlib.mlab.window_hanning,
|
|
256
|
-
cmap=self.spectro_color_map,
|
|
257
|
-
vmin=vmin,
|
|
258
|
-
vmax=vmax,
|
|
259
|
-
# mode="psd",
|
|
260
|
-
## NFFT=1024,
|
|
261
|
-
# Fs=self.frame_rate,
|
|
262
|
-
## noverlap=900,
|
|
263
|
-
# cmap=self.spectro_color_map,
|
|
264
|
-
)
|
|
251
|
+
spectrogram(self, self.sound_info[i:], window_type, nfft, noverlap, vmin, vmax)
|
|
265
252
|
|
|
266
253
|
lim1 = current_time - (self.media_length - self.interval / 2)
|
|
267
254
|
lim2 = lim1 + self.interval
|
|
@@ -276,32 +263,18 @@ class Plot_spectrogram_RT(QWidget):
|
|
|
276
263
|
|
|
277
264
|
# middle
|
|
278
265
|
else:
|
|
279
|
-
|
|
266
|
+
spectrogram(
|
|
267
|
+
self,
|
|
280
268
|
self.sound_info[
|
|
281
269
|
int(round((current_time - self.interval / 2) * self.frame_rate, 0)) : int(
|
|
282
270
|
round((current_time + self.interval / 2) * self.frame_rate, 0)
|
|
283
271
|
)
|
|
284
272
|
],
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
# matplotlib.mlab.window_hanning
|
|
291
|
-
# if window_type == "hanning"
|
|
292
|
-
# else matplotlib.mlab.window_hamming
|
|
293
|
-
# if window_type == "hamming"
|
|
294
|
-
# else matplotlib.mlab.window_blackmanharris
|
|
295
|
-
# if window_type == "blackmanharris"
|
|
296
|
-
# else matplotlib.mlab.window_hanning,
|
|
297
|
-
cmap=self.spectro_color_map,
|
|
298
|
-
vmin=vmin,
|
|
299
|
-
vmax=vmax,
|
|
300
|
-
# mode="psd",
|
|
301
|
-
## NFFT=1024,
|
|
302
|
-
# Fs=self.frame_rate,
|
|
303
|
-
## noverlap=900,
|
|
304
|
-
# cmap=self.spectro_color_map,
|
|
273
|
+
window_type,
|
|
274
|
+
nfft,
|
|
275
|
+
noverlap,
|
|
276
|
+
vmin,
|
|
277
|
+
vmax,
|
|
305
278
|
)
|
|
306
279
|
|
|
307
280
|
self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
|
|
@@ -311,6 +284,5 @@ class Plot_spectrogram_RT(QWidget):
|
|
|
311
284
|
self.ax.axvline(x=self.interval / 2, color=self.cursor_color, linestyle="-")
|
|
312
285
|
|
|
313
286
|
self.ax.set_ylim(self.sb_freq_min.value(), self.sb_freq_max.value())
|
|
314
|
-
"""self.figure.subplots_adjust(wspace=0, hspace=0)"""
|
|
315
287
|
|
|
316
288
|
self.canvas.draw()
|