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.
@@ -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)