boris-behav-obs 9.7.8__py3-none-any.whl → 9.7.9__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/_export_to_feral.py +225 -0
- boris/core.py +10 -9
- boris/observation_operations.py +48 -40
- boris/plugins.py +60 -23
- boris/utilities.py +9 -0
- boris/version.py +2 -2
- {boris_behav_obs-9.7.8.dist-info → boris_behav_obs-9.7.9.dist-info}/METADATA +2 -2
- {boris_behav_obs-9.7.8.dist-info → boris_behav_obs-9.7.9.dist-info}/RECORD +12 -11
- {boris_behav_obs-9.7.8.dist-info → boris_behav_obs-9.7.9.dist-info}/WHEEL +0 -0
- {boris_behav_obs-9.7.8.dist-info → boris_behav_obs-9.7.9.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.7.8.dist-info → boris_behav_obs-9.7.9.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.8.dist-info → boris_behav_obs-9.7.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS plugin
|
|
3
|
+
|
|
4
|
+
Export to FERAL (getferal.ai)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from PySide6.QtWidgets import QFileDialog
|
|
12
|
+
|
|
13
|
+
# dependencies for CategoryDialog
|
|
14
|
+
from PySide6.QtWidgets import QListWidget, QListWidgetItem, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QDialog
|
|
15
|
+
from PySide6.QtCore import Qt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.1"
|
|
19
|
+
__version_date__ = "2025-11-28"
|
|
20
|
+
__plugin_name__ = "Export observations to FERAL"
|
|
21
|
+
__author__ = "Olivier Friard - University of Torino - Italy"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CategoryDialog(QDialog):
|
|
25
|
+
def __init__(self, items, parent=None):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
|
|
28
|
+
self.setWindowTitle("Organize the videos in categories")
|
|
29
|
+
|
|
30
|
+
self.setModal(True)
|
|
31
|
+
|
|
32
|
+
# Main layout
|
|
33
|
+
main_layout = QVBoxLayout(self)
|
|
34
|
+
lists_layout = QHBoxLayout()
|
|
35
|
+
|
|
36
|
+
# All videos
|
|
37
|
+
self.list_unclassified = self._create_list_widget()
|
|
38
|
+
self.label_unclassified = QLabel("All videos")
|
|
39
|
+
col0_layout = QVBoxLayout()
|
|
40
|
+
col0_layout.addWidget(self.label_unclassified)
|
|
41
|
+
col0_layout.addWidget(self.list_unclassified)
|
|
42
|
+
|
|
43
|
+
self.list_cat1 = self._create_list_widget()
|
|
44
|
+
self.label_cat1 = QLabel("train")
|
|
45
|
+
col1_layout = QVBoxLayout()
|
|
46
|
+
col1_layout.addWidget(self.label_cat1)
|
|
47
|
+
col1_layout.addWidget(self.list_cat1)
|
|
48
|
+
|
|
49
|
+
self.list_cat2 = self._create_list_widget()
|
|
50
|
+
self.label_cat2 = QLabel("val")
|
|
51
|
+
col2_layout = QVBoxLayout()
|
|
52
|
+
col2_layout.addWidget(self.label_cat2)
|
|
53
|
+
col2_layout.addWidget(self.list_cat2)
|
|
54
|
+
|
|
55
|
+
self.list_cat3 = self._create_list_widget()
|
|
56
|
+
self.label_cat3 = QLabel("test")
|
|
57
|
+
col3_layout = QVBoxLayout()
|
|
58
|
+
col3_layout.addWidget(self.label_cat3)
|
|
59
|
+
col3_layout.addWidget(self.list_cat3)
|
|
60
|
+
|
|
61
|
+
self.list_cat4 = self._create_list_widget()
|
|
62
|
+
self.label_cat4 = QLabel("inference")
|
|
63
|
+
col4_layout = QVBoxLayout()
|
|
64
|
+
col4_layout.addWidget(self.label_cat4)
|
|
65
|
+
col4_layout.addWidget(self.list_cat4)
|
|
66
|
+
|
|
67
|
+
# Add all columns to the horizontal layout
|
|
68
|
+
lists_layout.addLayout(col0_layout)
|
|
69
|
+
lists_layout.addLayout(col1_layout)
|
|
70
|
+
lists_layout.addLayout(col2_layout)
|
|
71
|
+
lists_layout.addLayout(col3_layout)
|
|
72
|
+
lists_layout.addLayout(col4_layout)
|
|
73
|
+
|
|
74
|
+
main_layout.addLayout(lists_layout)
|
|
75
|
+
|
|
76
|
+
buttons_layout = QHBoxLayout()
|
|
77
|
+
self.btn_ok = QPushButton("OK")
|
|
78
|
+
self.btn_cancel = QPushButton("Cancel")
|
|
79
|
+
|
|
80
|
+
self.btn_ok.clicked.connect(self.accept)
|
|
81
|
+
self.btn_cancel.clicked.connect(self.reject)
|
|
82
|
+
|
|
83
|
+
buttons_layout.addStretch()
|
|
84
|
+
buttons_layout.addWidget(self.btn_ok)
|
|
85
|
+
buttons_layout.addWidget(self.btn_cancel)
|
|
86
|
+
|
|
87
|
+
main_layout.addLayout(buttons_layout)
|
|
88
|
+
|
|
89
|
+
# Populate "Unclassified" with input items
|
|
90
|
+
for text in items:
|
|
91
|
+
QListWidgetItem(text, self.list_unclassified)
|
|
92
|
+
|
|
93
|
+
def _create_list_widget(self):
|
|
94
|
+
"""
|
|
95
|
+
Create a QListWidget ready for drag & drop.
|
|
96
|
+
"""
|
|
97
|
+
lw = QListWidget()
|
|
98
|
+
lw.setSelectionMode(QListWidget.ExtendedSelection)
|
|
99
|
+
lw.setDragEnabled(True)
|
|
100
|
+
lw.setAcceptDrops(True)
|
|
101
|
+
lw.setDropIndicatorShown(True)
|
|
102
|
+
lw.setDragDropMode(QListWidget.DragDrop)
|
|
103
|
+
lw.setDefaultDropAction(Qt.MoveAction)
|
|
104
|
+
return lw
|
|
105
|
+
|
|
106
|
+
def get_categories(self):
|
|
107
|
+
"""
|
|
108
|
+
Return the content of all categories as a dictionary of lists.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def collect(widget):
|
|
112
|
+
return [widget.item(i).text().rstrip("*") for i in range(widget.count())]
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"unclassified": collect(self.list_unclassified),
|
|
116
|
+
"train": collect(self.list_cat1),
|
|
117
|
+
"val": collect(self.list_cat2),
|
|
118
|
+
"test": collect(self.list_cat3),
|
|
119
|
+
"inference": collect(self.list_cat4),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run(df: pd.DataFrame, project: dict):
|
|
124
|
+
"""
|
|
125
|
+
Export observations to FERAL
|
|
126
|
+
See https://www.getferal.ai/ > Label Preparation
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
out: dict = {
|
|
130
|
+
"is_multilabel": False,
|
|
131
|
+
"splits": {
|
|
132
|
+
"train": [],
|
|
133
|
+
"val": [],
|
|
134
|
+
"test": [],
|
|
135
|
+
"inference": [],
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log: list = []
|
|
140
|
+
|
|
141
|
+
# class names
|
|
142
|
+
class_names = {x: project["behaviors_conf"][x]["code"] for x in project["behaviors_conf"]}
|
|
143
|
+
out["class_names"] = class_names
|
|
144
|
+
reversed_class_names = {project["behaviors_conf"][x]["code"]: int(x) for x in project["behaviors_conf"]}
|
|
145
|
+
log.append(f"{class_names=}")
|
|
146
|
+
|
|
147
|
+
observations: list = sorted([x for x in project["observations"]])
|
|
148
|
+
log.append(f"Selected observation: {observations}")
|
|
149
|
+
|
|
150
|
+
labels: dict = {}
|
|
151
|
+
video_list: list = []
|
|
152
|
+
for observation_id in observations:
|
|
153
|
+
log.append("---")
|
|
154
|
+
log.append(observation_id)
|
|
155
|
+
|
|
156
|
+
# check number of media file in player #1
|
|
157
|
+
if len(project["observations"][observation_id]["file"]["1"]) != 1:
|
|
158
|
+
log.append(f"The observation {observation_id} contains more than one video")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# check number of coded subjects
|
|
162
|
+
if len(set([x[1] for x in project["observations"][observation_id]["events"]])) > 1:
|
|
163
|
+
log.append(f"The observation {observation_id} contains more than one subject")
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
media_file_path: str = project["observations"][observation_id]["file"]["1"][0]
|
|
167
|
+
media_file_name = str(Path(media_file_path).name)
|
|
168
|
+
|
|
169
|
+
# skip if no events
|
|
170
|
+
if not project["observations"][observation_id]["events"]:
|
|
171
|
+
video_list.append(media_file_name)
|
|
172
|
+
log.append(f"No events for observation {observation_id}")
|
|
173
|
+
continue
|
|
174
|
+
else:
|
|
175
|
+
video_list.append(media_file_name + "*")
|
|
176
|
+
|
|
177
|
+
# extract FPS
|
|
178
|
+
FPS = project["observations"][observation_id]["media_info"]["fps"][media_file_path]
|
|
179
|
+
log.append(f"{media_file_name} {FPS=}")
|
|
180
|
+
# extract media duration
|
|
181
|
+
duration = project["observations"][observation_id]["media_info"]["length"][media_file_path]
|
|
182
|
+
log.append(f"{media_file_name} {duration=}")
|
|
183
|
+
|
|
184
|
+
number_of_frames = int(duration / (1 / FPS))
|
|
185
|
+
log.append(f"{number_of_frames=}")
|
|
186
|
+
|
|
187
|
+
labels[media_file_name] = [0] * number_of_frames
|
|
188
|
+
|
|
189
|
+
for idx in range(number_of_frames):
|
|
190
|
+
t = idx * (1 / FPS)
|
|
191
|
+
behaviors = (
|
|
192
|
+
df[(df["Observation id"] == observation_id) & (df["Start (s)"] <= t) & (df["Stop (s)"] >= t)]["Behavior"].unique().tolist()
|
|
193
|
+
)
|
|
194
|
+
if len(behaviors) > 1:
|
|
195
|
+
log.append(f"The observation {observation_id} contains more than one behavior for frame {idx}")
|
|
196
|
+
del labels[media_file_name]
|
|
197
|
+
break
|
|
198
|
+
if behaviors:
|
|
199
|
+
behaviors_idx = reversed_class_names[behaviors[0]]
|
|
200
|
+
labels[media_file_name][idx] = behaviors_idx
|
|
201
|
+
|
|
202
|
+
out["labels"] = labels
|
|
203
|
+
|
|
204
|
+
# splits
|
|
205
|
+
dlg = CategoryDialog(video_list)
|
|
206
|
+
|
|
207
|
+
if dlg.exec(): # Dialog accepted
|
|
208
|
+
result = dlg.get_categories()
|
|
209
|
+
del result["unclassified"]
|
|
210
|
+
out["splits"] = result
|
|
211
|
+
|
|
212
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
213
|
+
None,
|
|
214
|
+
"Choose a file to save",
|
|
215
|
+
"", # start directory
|
|
216
|
+
"JSON files (*.json);;All files (*.*)",
|
|
217
|
+
)
|
|
218
|
+
if filename:
|
|
219
|
+
with open(filename, "w") as f_out:
|
|
220
|
+
f_out.write(json.dumps(out, separators=(",", ": "), indent=1))
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
log.append("splits section missing")
|
|
224
|
+
|
|
225
|
+
return "\n".join(log)
|
boris/core.py
CHANGED
|
@@ -1500,7 +1500,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
1500
1500
|
logging.debug("previous media file")
|
|
1501
1501
|
|
|
1502
1502
|
if self.playerType == cfg.MEDIA:
|
|
1503
|
-
#if len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1]) == 1:
|
|
1503
|
+
# if len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1]) == 1:
|
|
1504
1504
|
# self.seek_mediaplayer(dec(0))
|
|
1505
1505
|
# return
|
|
1506
1506
|
|
|
@@ -1510,7 +1510,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
1510
1510
|
|
|
1511
1511
|
elif self.dw_player[0].player.playlist_count == 1:
|
|
1512
1512
|
self.seek_mediaplayer(dec(0))
|
|
1513
|
-
#self.statusbar.showMessage("There is only one media file", 5000)
|
|
1513
|
+
# self.statusbar.showMessage("There is only one media file", 5000)
|
|
1514
1514
|
|
|
1515
1515
|
if hasattr(self, "spectro"):
|
|
1516
1516
|
self.spectro.memChunk = -1
|
|
@@ -4151,8 +4151,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
4151
4151
|
called only in IPC mode
|
|
4152
4152
|
"""
|
|
4153
4153
|
# check if eof reached
|
|
4154
|
-
#print(f"{self.dw_player[0].player.playlist_pos=}")
|
|
4155
|
-
#print(f"{self.dw_player[0].player.playlist_count=}")
|
|
4154
|
+
# print(f"{self.dw_player[0].player.playlist_pos=}")
|
|
4155
|
+
# print(f"{self.dw_player[0].player.playlist_count=}")
|
|
4156
4156
|
if self.dw_player[0].player.eof_reached and self.dw_player[0].player.core_idle:
|
|
4157
4157
|
logging.debug("end of playlist reached")
|
|
4158
4158
|
if self.dw_player[0].player.playlist_pos is not None and self.dw_player[0].player.playlist_count is not None:
|
|
@@ -4220,8 +4220,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
4220
4220
|
|
|
4221
4221
|
# sync players 2..8 if time diff >= 1 s
|
|
4222
4222
|
if not math.isnan(ct) and not math.isnan(ct0):
|
|
4223
|
-
if
|
|
4224
|
-
|
|
4223
|
+
if (
|
|
4224
|
+
abs(ct0 - (ct + dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OFFSET][str(n_player + 1)])))
|
|
4225
|
+
>= 1
|
|
4226
|
+
):
|
|
4227
|
+
self.sync_time(n_player, float(ct0)) # self.seek_mediaplayer(ct0, n_player)
|
|
4225
4228
|
|
|
4226
4229
|
currentTimeOffset = dec(cumulative_time_pos + self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET])
|
|
4227
4230
|
|
|
@@ -5485,7 +5488,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
5485
5488
|
if self.dw_player[0].player.playlist_pos == self.dw_player[0].player.playlist_count - 1:
|
|
5486
5489
|
self.seek_mediaplayer(dec(0))
|
|
5487
5490
|
|
|
5488
|
-
|
|
5489
5491
|
# check if player 1 is ended
|
|
5490
5492
|
for i, dw in enumerate(self.dw_player):
|
|
5491
5493
|
if (
|
|
@@ -5867,7 +5869,7 @@ def main():
|
|
|
5867
5869
|
QMessageBox.warning(
|
|
5868
5870
|
None,
|
|
5869
5871
|
cfg.programName,
|
|
5870
|
-
(
|
|
5872
|
+
("This version of BORIS for macOS is still EXPERIMENTAL and should be used at your own risk."),
|
|
5871
5873
|
QMessageBox.Ok | QMessageBox.Default,
|
|
5872
5874
|
QMessageBox.NoButton,
|
|
5873
5875
|
)
|
|
@@ -5898,4 +5900,3 @@ def main():
|
|
|
5898
5900
|
del window
|
|
5899
5901
|
|
|
5900
5902
|
sys.exit(return_code)
|
|
5901
|
-
|
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
|
|
|
@@ -131,7 +131,6 @@ def observations_list(self):
|
|
|
131
131
|
else:
|
|
132
132
|
close_observation(self)
|
|
133
133
|
|
|
134
|
-
|
|
135
134
|
QtTest.QTest.qWait(1000)
|
|
136
135
|
|
|
137
136
|
if result == cfg.OPEN:
|
|
@@ -602,7 +601,7 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
|
|
|
602
601
|
close_observation(self)
|
|
603
602
|
|
|
604
603
|
observationWindow = observation.Observation(
|
|
605
|
-
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(),
|
|
606
605
|
project_path=self.projectFileName,
|
|
607
606
|
converters=self.pj.get(cfg.CONVERTERS, {}),
|
|
608
607
|
time_format=self.timeFormat,
|
|
@@ -1315,7 +1314,7 @@ def check_creation_date(self) -> Tuple[int, dict]:
|
|
|
1315
1314
|
|
|
1316
1315
|
if ret == 1: # use file creation time
|
|
1317
1316
|
for media in not_tagged_media_list:
|
|
1318
|
-
media_creation_time[media] =
|
|
1317
|
+
media_creation_time[media] = Path(media).stat().st_ctime
|
|
1319
1318
|
return (0, media_creation_time) # OK use media file creation date/time
|
|
1320
1319
|
else:
|
|
1321
1320
|
return (1, {})
|
|
@@ -1856,10 +1855,9 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1856
1855
|
# start timer for activating the main window
|
|
1857
1856
|
self.main_window_activation_timer = QTimer()
|
|
1858
1857
|
self.main_window_activation_timer.setInterval(500)
|
|
1859
|
-
#self.main_window_activation_timer.timeout.connect(self.activateWindow)
|
|
1858
|
+
# self.main_window_activation_timer.timeout.connect(self.activateWindow)
|
|
1860
1859
|
self.main_window_activation_timer.timeout.connect(self.activate_main_window)
|
|
1861
1860
|
self.main_window_activation_timer.start()
|
|
1862
|
-
|
|
1863
1861
|
|
|
1864
1862
|
for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
|
|
1865
1863
|
logging.debug(f"media file: {mediaFile}")
|
|
@@ -1904,7 +1902,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
1904
1902
|
self.dw_player[i].player.playlist_append(media_full_path)
|
|
1905
1903
|
|
|
1906
1904
|
# add media file name to player window title
|
|
1907
|
-
self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({
|
|
1905
|
+
self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({Path(media_full_path).name})")
|
|
1908
1906
|
|
|
1909
1907
|
# media duration cumuled in seconds
|
|
1910
1908
|
self.dw_player[i].cumul_media_durations_sec = [round(dec(x / 1000), 3) for x in self.dw_player[i].cumul_media_durations]
|
|
@@ -2013,7 +2011,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
2013
2011
|
tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
|
|
2014
2012
|
|
|
2015
2013
|
wav_file_path = (
|
|
2016
|
-
|
|
2014
|
+
Path(tmp_dir) / Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
|
|
2017
2015
|
)
|
|
2018
2016
|
|
|
2019
2017
|
if not wav_file_path.is_file():
|
|
@@ -2029,7 +2027,7 @@ def initialize_new_media_observation(self) -> bool:
|
|
|
2029
2027
|
tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
|
|
2030
2028
|
|
|
2031
2029
|
wav_file_path = (
|
|
2032
|
-
|
|
2030
|
+
Path(tmp_dir) / Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
|
|
2033
2031
|
)
|
|
2034
2032
|
|
|
2035
2033
|
if not wav_file_path.is_file():
|
|
@@ -2313,8 +2311,8 @@ def initialize_new_images_observation(self):
|
|
|
2313
2311
|
sorted(
|
|
2314
2312
|
list(
|
|
2315
2313
|
set(
|
|
2316
|
-
[str(x) for x in
|
|
2317
|
-
+ [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())]
|
|
2318
2316
|
)
|
|
2319
2317
|
)
|
|
2320
2318
|
)
|
|
@@ -2439,26 +2437,31 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2439
2437
|
|
|
2440
2438
|
def create_observations(self):
|
|
2441
2439
|
"""
|
|
2442
|
-
Create observations from a media
|
|
2440
|
+
Create observations from a directory of media files
|
|
2443
2441
|
"""
|
|
2444
|
-
# print(self.pj[cfg.OBSERVATIONS])
|
|
2445
2442
|
|
|
2446
|
-
dir_path
|
|
2447
|
-
if not dir_path:
|
|
2443
|
+
if not (dir_path := QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))):
|
|
2448
2444
|
return
|
|
2449
2445
|
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
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
|
+
[
|
|
2453
2452
|
("cb", "Recurse the subdirectories", False),
|
|
2454
|
-
("cb", "Save the absolute media file path", True),
|
|
2455
2453
|
("cb", "Visualize spectrogram", False),
|
|
2456
2454
|
("cb", "Visualize waveform", False),
|
|
2457
2455
|
("cb", "Media creation date as offset", False),
|
|
2458
2456
|
("cb", "Close behaviors between videos", False),
|
|
2459
2457
|
("dsb", "Time offset (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
2460
2458
|
("dsb", "Media scan sampling duration (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
2461
|
-
]
|
|
2459
|
+
]
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2462
|
+
dlg = dialog.Input_dialog(
|
|
2463
|
+
label_caption="Set the following observation parameters",
|
|
2464
|
+
elements_list=elements_list,
|
|
2462
2465
|
title="Observation parameters",
|
|
2463
2466
|
)
|
|
2464
2467
|
if not dlg.exec_():
|
|
@@ -2467,9 +2470,9 @@ def create_observations(self):
|
|
|
2467
2470
|
file_count: int = 0
|
|
2468
2471
|
|
|
2469
2472
|
if dlg.elements["Recurse the subdirectories"].isChecked():
|
|
2470
|
-
files_list =
|
|
2473
|
+
files_list = Path(dir_path).rglob("*")
|
|
2471
2474
|
else:
|
|
2472
|
-
files_list =
|
|
2475
|
+
files_list = Path(dir_path).glob("*")
|
|
2473
2476
|
|
|
2474
2477
|
for file in files_list:
|
|
2475
2478
|
if not file.is_file():
|
|
@@ -2479,22 +2482,25 @@ def create_observations(self):
|
|
|
2479
2482
|
if not r.get("frames_number", 0):
|
|
2480
2483
|
continue
|
|
2481
2484
|
|
|
2482
|
-
if dlg.elements["
|
|
2483
|
-
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))
|
|
2484
2487
|
else:
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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
|
|
2498
2504
|
|
|
2499
2505
|
if media_file in self.pj[cfg.OBSERVATIONS]:
|
|
2500
2506
|
QMessageBox.critical(
|
|
@@ -2535,4 +2541,6 @@ def create_observations(self):
|
|
|
2535
2541
|
else:
|
|
2536
2542
|
message: str = f"No media file were found in {dir_path}"
|
|
2537
2543
|
|
|
2544
|
+
menu_options.update_menu(self)
|
|
2545
|
+
|
|
2538
2546
|
QMessageBox.information(self, cfg.programName, message)
|
boris/plugins.py
CHANGED
|
@@ -24,6 +24,8 @@ import logging
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
import pandas as pd
|
|
26
26
|
from pathlib import Path
|
|
27
|
+
import copy
|
|
28
|
+
import inspect
|
|
27
29
|
|
|
28
30
|
from PySide6.QtGui import QAction
|
|
29
31
|
from PySide6.QtWidgets import QMessageBox
|
|
@@ -297,6 +299,7 @@ def run_plugin(self, plugin_name):
|
|
|
297
299
|
|
|
298
300
|
logging.debug(f"{plugin_path=}")
|
|
299
301
|
|
|
302
|
+
# check if plugin file exists
|
|
300
303
|
if not Path(plugin_path).is_file():
|
|
301
304
|
QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
|
|
302
305
|
return
|
|
@@ -308,27 +311,7 @@ def run_plugin(self, plugin_name):
|
|
|
308
311
|
if not selected_observations:
|
|
309
312
|
return
|
|
310
313
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
message, df = project_functions.project2dataframe(self.pj, selected_observations)
|
|
314
|
-
if message:
|
|
315
|
-
logging.critical(message)
|
|
316
|
-
QMessageBox.critical(self, cfg.programName, message)
|
|
317
|
-
return
|
|
318
|
-
|
|
319
|
-
logging.info("done")
|
|
320
|
-
|
|
321
|
-
"""
|
|
322
|
-
logging.debug("dataframe info")
|
|
323
|
-
logging.debug(f"{df.info()}")
|
|
324
|
-
logging.debug(f"{df.head()}")
|
|
325
|
-
"""
|
|
326
|
-
|
|
327
|
-
# filter the dataframe with parameters
|
|
328
|
-
logging.info("filtering dataframe for plugin")
|
|
329
|
-
filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
|
|
330
|
-
logging.info("done")
|
|
331
|
-
|
|
314
|
+
# Python plugin
|
|
332
315
|
if Path(plugin_path).suffix == ".py":
|
|
333
316
|
# load plugin as module
|
|
334
317
|
module_name = Path(plugin_path).stem
|
|
@@ -347,9 +330,48 @@ def run_plugin(self, plugin_name):
|
|
|
347
330
|
f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
|
|
348
331
|
)
|
|
349
332
|
|
|
350
|
-
# run plugin
|
|
351
|
-
|
|
333
|
+
# check arguments required by the run function of the plugin
|
|
334
|
+
dataframe_required = False
|
|
335
|
+
project_required = False
|
|
336
|
+
# for param in inspect.signature(plugin_module.run).parameters.values():
|
|
337
|
+
for name, annotation in inspect.getfullargspec(plugin_module.run).annotations.items():
|
|
338
|
+
if name == "df" and annotation is pd.DataFrame:
|
|
339
|
+
dataframe_required = True
|
|
340
|
+
if name == "project" and annotation is dict:
|
|
341
|
+
project_required = True
|
|
342
|
+
|
|
343
|
+
# create arguments for the plugin run function
|
|
344
|
+
plugin_kwargs: dict = {}
|
|
345
|
+
|
|
346
|
+
if dataframe_required:
|
|
347
|
+
logging.info("preparing dataframe for plugin")
|
|
348
|
+
message, df = project_functions.project2dataframe(self.pj, selected_observations)
|
|
349
|
+
if message:
|
|
350
|
+
logging.critical(message)
|
|
351
|
+
QMessageBox.critical(self, cfg.programName, message)
|
|
352
|
+
return
|
|
353
|
+
logging.info("done")
|
|
354
|
+
|
|
355
|
+
# filter the dataframe with parameters
|
|
356
|
+
logging.info("filtering dataframe for plugin")
|
|
357
|
+
filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
|
|
358
|
+
logging.info("done")
|
|
352
359
|
|
|
360
|
+
plugin_kwargs["df"] = filtered_df
|
|
361
|
+
|
|
362
|
+
if project_required:
|
|
363
|
+
pj_copy = copy.deepcopy(self.pj)
|
|
364
|
+
|
|
365
|
+
# remove unselected observations from project
|
|
366
|
+
for obs_id in self.pj[cfg.OBSERVATIONS]:
|
|
367
|
+
if obs_id not in selected_observations:
|
|
368
|
+
del pj_copy[cfg.OBSERVATIONS][obs_id]
|
|
369
|
+
|
|
370
|
+
plugin_kwargs["project"] = pj_copy
|
|
371
|
+
|
|
372
|
+
plugin_results = plugin_module.run(**plugin_kwargs)
|
|
373
|
+
|
|
374
|
+
# R plugin
|
|
353
375
|
if Path(plugin_path).suffix in (".R", ".r"):
|
|
354
376
|
try:
|
|
355
377
|
from rpy2 import robjects
|
|
@@ -360,6 +382,21 @@ def run_plugin(self, plugin_name):
|
|
|
360
382
|
QMessageBox.critical(self, cfg.programName, "The rpy2 Python module is not installed. R plugins cannot be used")
|
|
361
383
|
return
|
|
362
384
|
|
|
385
|
+
logging.info("preparing dataframe for plugin")
|
|
386
|
+
|
|
387
|
+
message, df = project_functions.project2dataframe(self.pj, selected_observations)
|
|
388
|
+
if message:
|
|
389
|
+
logging.critical(message)
|
|
390
|
+
QMessageBox.critical(self, cfg.programName, message)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
logging.info("done")
|
|
394
|
+
|
|
395
|
+
# filter the dataframe with parameters
|
|
396
|
+
logging.info("filtering dataframe for plugin")
|
|
397
|
+
filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
|
|
398
|
+
logging.info("done")
|
|
399
|
+
|
|
363
400
|
# Read code from file
|
|
364
401
|
try:
|
|
365
402
|
with open(plugin_path, "r") as f:
|
boris/utilities.py
CHANGED
|
@@ -126,6 +126,15 @@ if (sys.platform.startswith("win") or sys.platform.startswith("linux")) and ("-i
|
|
|
126
126
|
sys.exit(5)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
def is_subdir(a: Path, b: Path) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Return True if directory A is inside directory B.
|
|
132
|
+
"""
|
|
133
|
+
a = a.resolve()
|
|
134
|
+
b = b.resolve()
|
|
135
|
+
return a.is_relative_to(b)
|
|
136
|
+
|
|
137
|
+
|
|
129
138
|
def test_mpv_ipc(socket_path: str = cfg.MPV_SOCKET) -> bool:
|
|
130
139
|
"""
|
|
131
140
|
test if socket available
|
boris/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boris-behav-obs
|
|
3
|
-
Version: 9.7.
|
|
3
|
+
Version: 9.7.9
|
|
4
4
|
Summary: BORIS - Behavioral Observation Research Interactive Software
|
|
5
5
|
Author-email: Olivier Friard <olivier.friard@unito.it>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -51,7 +51,7 @@ It provides also some analysis tools like time budget and some plotting function
|
|
|
51
51
|
<!-- The BO-RIS paper has more than [ citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
The BORIS paper has more than
|
|
54
|
+
The BORIS paper has more than 2443 citations in peer-reviewed scientific publications.
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
|
|
@@ -16,7 +16,7 @@ boris/connections.py,sha256=KsC17LnS4tRM6O3Nu3mD1H9kQ7uYhhad9229jXfGF94,19774
|
|
|
16
16
|
boris/converters.py,sha256=n6gDM9x2hS-ZOoHLruiifuXxnC7ERsUukiFokhHZPoQ,11678
|
|
17
17
|
boris/converters_ui.py,sha256=uu7LOBV_fKv2DBdOqsqPwjGsjgONr5ODBoscAA-EP48,9900
|
|
18
18
|
boris/cooccurence.py,sha256=tVERC-V8MWjWHlGEfDuu08iS94qjt4do-38jwI62QaY,10367
|
|
19
|
-
boris/core.py,sha256=
|
|
19
|
+
boris/core.py,sha256=PfpWAsA7vWVPSYkCcZ79xsPgwOc-dP4V-yCD9XgybtQ,234375
|
|
20
20
|
boris/core_qrc.py,sha256=Hz51Xw70ZIlDbYB281nfGtCm43_ItYhamMu2T5X8Tu8,639882
|
|
21
21
|
boris/core_ui.py,sha256=uDAI9mbadBe2mSsgugzNfRHxASc6Mu5kUf5tB9CZCjY,77445
|
|
22
22
|
boris/db_functions.py,sha256=TfCJ0Hq0pTFOKrZz3RzdvnR-NKCmrPHU2qL9BSXeeGQ,13379
|
|
@@ -47,7 +47,7 @@ boris/modifiers_coding_map.py,sha256=oT56ZY_PXhEJsMoblEsyNMAPbDpv7ZMOCnvmt7Ibx_Y
|
|
|
47
47
|
boris/mpv.py,sha256=EfzIHjPbgewG4w3smEtqEUPZoVwYmMQkL4Q8ZyW-a58,76410
|
|
48
48
|
boris/mpv2.py,sha256=IUI4t4r9GYX7G5OXTjd3RhMMOkDdfal_15buBgksLsk,92152
|
|
49
49
|
boris/observation.py,sha256=K-Xi99BMLSohx6pPLKk-Eqi56fLWDFLScHxXKkoFAlg,57507
|
|
50
|
-
boris/observation_operations.py,sha256=
|
|
50
|
+
boris/observation_operations.py,sha256=HPPat_-6c_Fu0cFs0wQD86AIE7g7eKHutOGi_xexMAM,106581
|
|
51
51
|
boris/observation_ui.py,sha256=DAnU94QBNvkLuHT6AxTwqSk_D_n6VUhSl8PexZv_dUk,33309
|
|
52
52
|
boris/observations_list.py,sha256=NqwECGHtHYmKhSe-qCfqPmJ24SSfzlXvIXS2i3op_zE,10591
|
|
53
53
|
boris/otx_parser.py,sha256=70QvilzFHXbjAHR88YH0aEXJ3xxheLS5fZGgHFHGpNE,16367
|
|
@@ -59,7 +59,7 @@ boris/plot_events.py,sha256=tKiUWH0TNSkK7xz5Vf0tAD3KiuAalv6UZEVtOnoFpWY,24059
|
|
|
59
59
|
boris/plot_events_rt.py,sha256=xJmjwqhQxCN4FDBYRQ0O2eHm356Rbexzr3m1qTefMDU,8326
|
|
60
60
|
boris/plot_spectrogram_rt.py,sha256=wDhnkqwjd2UfCxrfOejOUxoNOqfMNo6vo1JSvYgM-2A,10925
|
|
61
61
|
boris/plot_waveform_rt.py,sha256=RNXhcBzRKnoGoVjRAHsVvOaj0BJbbI2cpCMjMUiGqX0,7534
|
|
62
|
-
boris/plugins.py,sha256=
|
|
62
|
+
boris/plugins.py,sha256=AzjKLPsqBk-K8wPM3HXe4w-sNaDU1bQm2XEoLr4Zx0k,17234
|
|
63
63
|
boris/preferences.py,sha256=YcGBaXRfimH4jpFLfcUAgCAGESXSLhB6cli_cg4cDl0,21902
|
|
64
64
|
boris/preferences_ui.py,sha256=zkmbQbkb0WqhPyMtnU-DU9Y2enSph_r-LnwsmEOgzkk,35090
|
|
65
65
|
boris/project.py,sha256=4dE4nqo8at1lz9KykprF5Rr62R78yu4vBrTfde1gQ-g,86438
|
|
@@ -77,8 +77,8 @@ boris/synthetic_time_budget.py,sha256=3Eb9onMLmgqCLd50CuxV9L8RV2ESzfaMWvPK_bXUMM
|
|
|
77
77
|
boris/time_budget_functions.py,sha256=SbGyTF2xySEqBdRJZeWFTirruvK3r6pwM6e4Gz17W1s,52186
|
|
78
78
|
boris/time_budget_widget.py,sha256=z-tyITBtIz-KH1H2OdMB5a8x9QQLK7Wu96-zkC6NVDA,43213
|
|
79
79
|
boris/transitions.py,sha256=okyDCO-Vn4p_Fixd8cGiSIaUhUxG5ePIOqGSuP52g_c,12246
|
|
80
|
-
boris/utilities.py,sha256=
|
|
81
|
-
boris/version.py,sha256=
|
|
80
|
+
boris/utilities.py,sha256=Ae3YjXkulJYQxbE4Fpvle4tX2nA_qJQhAn980sTFHqw,58805
|
|
81
|
+
boris/version.py,sha256=WD0mTHLHZshmktAU-U6dY_d9wm09W0c_v9t3wdvW5-I,787
|
|
82
82
|
boris/video_equalizer.py,sha256=cm2JXe1eAPkqDzxYB2OMKyuveMBdq4a9-gD1bBorbn4,5823
|
|
83
83
|
boris/video_equalizer_ui.py,sha256=1CG3s79eM4JAbaCx3i1ILZXLceb41_gGXlOLNfpBgnw,10142
|
|
84
84
|
boris/video_operations.py,sha256=YlwcX3rgVsv6h78ylQYS9ITZs5MsXT8h0XsyaBU5mN8,11015
|
|
@@ -86,6 +86,7 @@ boris/view_df.py,sha256=fhcNMFFVbQ4ZTpPwN_4Bg66DDIoV25zaPryZUsOxsKg,3338
|
|
|
86
86
|
boris/view_df_ui.py,sha256=CaMeRH_vQ00CTDDFQn73ZZaS-r8BSTWpL-dMCFqzJ_Q,2775
|
|
87
87
|
boris/write_event.py,sha256=xC-dUjgPS4-tvrQfvyL7O_xi-tyQI7leSiSV8_x8hgg,23817
|
|
88
88
|
boris/analysis_plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
|
+
boris/analysis_plugins/_export_to_feral.py,sha256=_eirrreEn3Udp4POBG8cA1Gno4e-x_LFYXfvDhy11fg,7417
|
|
89
90
|
boris/analysis_plugins/_latency.py,sha256=9kCdFDtb5Zdao1xFpioi_exm_IxyGm6RlY3Opn6GUFo,2030
|
|
90
91
|
boris/analysis_plugins/irr_cohen_kappa.py,sha256=OqmivIE6i1hTcFVMp0EtY0Sr7C1Jm9t0D4IKbDfTJ7U,4268
|
|
91
92
|
boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py,sha256=DtzFLRToR9GdkmWYDcCmpSvxHGpguVp-_n8F-t7ND7c,4461
|
|
@@ -101,9 +102,9 @@ boris/portion/dict.py,sha256=uNM-LEY52CZ2VNMMW_C9QukoyTvPlQf8vcbGa1lQBHI,11281
|
|
|
101
102
|
boris/portion/func.py,sha256=mSQr20YS1ug7R1fRqBg8LifjtXDRvJ6Kjc3WOeL9P34,2172
|
|
102
103
|
boris/portion/interval.py,sha256=sOlj3MAGGaB-JxCkigS-n3qw0fY7TANAsXv1pavr8J4,19931
|
|
103
104
|
boris/portion/io.py,sha256=kpq44pw3xnIyAlPwaR5qRHKRdZ72f8HS9YVIWs5k2pk,6367
|
|
104
|
-
boris_behav_obs-9.7.
|
|
105
|
-
boris_behav_obs-9.7.
|
|
106
|
-
boris_behav_obs-9.7.
|
|
107
|
-
boris_behav_obs-9.7.
|
|
108
|
-
boris_behav_obs-9.7.
|
|
109
|
-
boris_behav_obs-9.7.
|
|
105
|
+
boris_behav_obs-9.7.9.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
|
|
106
|
+
boris_behav_obs-9.7.9.dist-info/METADATA,sha256=aZnGCR1F6i4L1MqgrpzSsraISOgHo_rcBGDzx1xvS7k,5203
|
|
107
|
+
boris_behav_obs-9.7.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
108
|
+
boris_behav_obs-9.7.9.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
|
|
109
|
+
boris_behav_obs-9.7.9.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
|
|
110
|
+
boris_behav_obs-9.7.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|