boris-behav-obs 9.7.12__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/analysis_plugins/export_to_feral.py +336 -0
- boris/config.py +5 -2
- boris/converters_ui.py +2 -3
- boris/core.py +127 -124
- boris/plot_spectrogram_rt.py +41 -72
- boris/preferences.py +34 -3
- boris/preferences_ui.py +48 -18
- boris/project_functions.py +7 -10
- boris/version.py +2 -2
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.7.15.dist-info}/METADATA +2 -2
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.7.15.dist-info}/RECORD +15 -15
- boris/analysis_plugins/_export_to_feral.py +0 -225
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.7.15.dist-info}/WHEEL +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.7.15.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.7.15.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.7.15.dist-info}/top_level.txt +0 -0
|
@@ -1,225 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|