boris-behav-obs 9.7.1__py3-none-any.whl → 9.7.12__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 +225 -0
- boris/behav_coding_map_creator.py +7 -7
- boris/coding_pad.py +4 -3
- boris/config.py +7 -0
- boris/config_file.py +3 -3
- boris/core.py +98 -60
- 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 +3 -0
- boris/plot_waveform_rt.py +4 -1
- boris/plugins.py +60 -23
- boris/preferences.py +9 -0
- boris/preferences_ui.py +48 -28
- boris/project.py +1 -1
- boris/project_functions.py +26 -10
- 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.12.dist-info}/METADATA +3 -4
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.12.dist-info}/RECORD +30 -29
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +0 -0
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.12.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.12.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.1.dist-info → boris_behav_obs-9.7.12.dist-info}/top_level.txt +0 -0
boris/about.py
CHANGED
|
@@ -40,7 +40,7 @@ def actionAbout_activated(self):
|
|
|
40
40
|
About dialog
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
|
-
programs_versions: list = ["MPV media player"]
|
|
43
|
+
programs_versions: list[str] = ["MPV media player"]
|
|
44
44
|
|
|
45
45
|
mpv_lib_version, mpv_lib_file_path, mpv_api_version = util.mpv_lib_version()
|
|
46
46
|
programs_versions.append(
|
|
@@ -53,13 +53,13 @@ def actionAbout_activated(self):
|
|
|
53
53
|
|
|
54
54
|
# ffmpeg
|
|
55
55
|
if self.ffmpeg_bin == "ffmpeg" and sys.platform.startswith("linux"):
|
|
56
|
-
ffmpeg_true_path = subprocess.getoutput("which ffmpeg")
|
|
56
|
+
ffmpeg_true_path: str = subprocess.getoutput("which ffmpeg")
|
|
57
57
|
else:
|
|
58
58
|
ffmpeg_true_path = self.ffmpeg_bin
|
|
59
59
|
programs_versions.extend(
|
|
60
60
|
[
|
|
61
61
|
"\nFFmpeg",
|
|
62
|
-
subprocess.getoutput(f'"{self.ffmpeg_bin}" -version').split("\n")[0],
|
|
62
|
+
subprocess.getoutput(cmd=f'"{self.ffmpeg_bin}" -version').split(sep="\n")[0],
|
|
63
63
|
f"Path: {ffmpeg_true_path}",
|
|
64
64
|
"https://www.ffmpeg.org",
|
|
65
65
|
]
|
|
@@ -75,11 +75,11 @@ def actionAbout_activated(self):
|
|
|
75
75
|
programs_versions.extend(["\nPandas", f"version {pd.__version__}", "https://pandas.pydata.org"])
|
|
76
76
|
|
|
77
77
|
# graphviz
|
|
78
|
-
gv_result = subprocess.getoutput("dot -V")
|
|
78
|
+
gv_result = subprocess.getoutput(cmd="dot -V")
|
|
79
79
|
|
|
80
80
|
programs_versions.extend(["\nGraphViz", gv_result if "graphviz" in gv_result else "not installed", "https://www.graphviz.org/"])
|
|
81
81
|
|
|
82
|
-
about_dialog = QMessageBox()
|
|
82
|
+
about_dialog: QMessageBox = QMessageBox()
|
|
83
83
|
about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
|
|
84
84
|
|
|
85
85
|
about_dialog.setWindowTitle(f"About {cfg.programName}")
|
boris/add_modifier_ui.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
################################################################################
|
|
4
4
|
## Form generated from reading UI file 'add_modifier.ui'
|
|
5
5
|
##
|
|
6
|
-
## Created by: Qt User Interface Compiler version 6.
|
|
6
|
+
## Created by: Qt User Interface Compiler version 6.10.0
|
|
7
7
|
##
|
|
8
8
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
|
9
9
|
################################################################################
|
|
@@ -24,18 +24,16 @@ class Ui_Dialog(object):
|
|
|
24
24
|
def setupUi(self, Dialog):
|
|
25
25
|
if not Dialog.objectName():
|
|
26
26
|
Dialog.setObjectName(u"Dialog")
|
|
27
|
-
Dialog.resize(
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
27
|
+
Dialog.resize(1339, 789)
|
|
28
|
+
self.verticalLayout_4 = QVBoxLayout(Dialog)
|
|
29
|
+
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
|
|
30
30
|
self.cb_ask_at_stop = QCheckBox(Dialog)
|
|
31
31
|
self.cb_ask_at_stop.setObjectName(u"cb_ask_at_stop")
|
|
32
32
|
|
|
33
|
-
self.
|
|
33
|
+
self.verticalLayout_4.addWidget(self.cb_ask_at_stop)
|
|
34
34
|
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
37
|
-
self.horizontalLayout_5 = QHBoxLayout()
|
|
38
|
-
self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
|
|
35
|
+
self.horizontalLayout_8 = QHBoxLayout()
|
|
36
|
+
self.horizontalLayout_8.setObjectName(u"horizontalLayout_8")
|
|
39
37
|
self.verticalLayout_2 = QVBoxLayout()
|
|
40
38
|
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
|
41
39
|
self.lbModifier = QLabel(Dialog)
|
|
@@ -69,7 +67,7 @@ class Ui_Dialog(object):
|
|
|
69
67
|
self.verticalLayout_2.addItem(self.verticalSpacer)
|
|
70
68
|
|
|
71
69
|
|
|
72
|
-
self.
|
|
70
|
+
self.horizontalLayout_8.addLayout(self.verticalLayout_2)
|
|
73
71
|
|
|
74
72
|
self.verticalLayout_3 = QVBoxLayout()
|
|
75
73
|
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
|
|
@@ -88,7 +86,7 @@ class Ui_Dialog(object):
|
|
|
88
86
|
self.verticalLayout_3.addItem(self.verticalSpacer_2)
|
|
89
87
|
|
|
90
88
|
|
|
91
|
-
self.
|
|
89
|
+
self.horizontalLayout_8.addLayout(self.verticalLayout_3)
|
|
92
90
|
|
|
93
91
|
self.verticalLayout = QVBoxLayout()
|
|
94
92
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
|
@@ -102,30 +100,42 @@ class Ui_Dialog(object):
|
|
|
102
100
|
|
|
103
101
|
self.verticalLayout.addWidget(self.tabWidgetModifiersSets)
|
|
104
102
|
|
|
103
|
+
self.horizontalLayout_5 = QHBoxLayout()
|
|
104
|
+
self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
|
|
105
105
|
self.lb_name = QLabel(Dialog)
|
|
106
106
|
self.lb_name.setObjectName(u"lb_name")
|
|
107
107
|
|
|
108
|
-
self.
|
|
108
|
+
self.horizontalLayout_5.addWidget(self.lb_name)
|
|
109
109
|
|
|
110
110
|
self.le_name = QLineEdit(Dialog)
|
|
111
111
|
self.le_name.setObjectName(u"le_name")
|
|
112
112
|
|
|
113
|
-
self.
|
|
113
|
+
self.horizontalLayout_5.addWidget(self.le_name)
|
|
114
114
|
|
|
115
|
+
|
|
116
|
+
self.verticalLayout.addLayout(self.horizontalLayout_5)
|
|
117
|
+
|
|
118
|
+
self.horizontalLayout_6 = QHBoxLayout()
|
|
119
|
+
self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
|
|
115
120
|
self.lb_description = QLabel(Dialog)
|
|
116
121
|
self.lb_description.setObjectName(u"lb_description")
|
|
117
122
|
|
|
118
|
-
self.
|
|
123
|
+
self.horizontalLayout_6.addWidget(self.lb_description)
|
|
119
124
|
|
|
120
125
|
self.le_description = QLineEdit(Dialog)
|
|
121
126
|
self.le_description.setObjectName(u"le_description")
|
|
122
127
|
|
|
123
|
-
self.
|
|
128
|
+
self.horizontalLayout_6.addWidget(self.le_description)
|
|
129
|
+
|
|
124
130
|
|
|
131
|
+
self.verticalLayout.addLayout(self.horizontalLayout_6)
|
|
132
|
+
|
|
133
|
+
self.horizontalLayout_7 = QHBoxLayout()
|
|
134
|
+
self.horizontalLayout_7.setObjectName(u"horizontalLayout_7")
|
|
125
135
|
self.lbType = QLabel(Dialog)
|
|
126
136
|
self.lbType.setObjectName(u"lbType")
|
|
127
137
|
|
|
128
|
-
self.
|
|
138
|
+
self.horizontalLayout_7.addWidget(self.lbType)
|
|
129
139
|
|
|
130
140
|
self.cbType = QComboBox(Dialog)
|
|
131
141
|
self.cbType.addItem("")
|
|
@@ -134,7 +144,14 @@ class Ui_Dialog(object):
|
|
|
134
144
|
self.cbType.addItem("")
|
|
135
145
|
self.cbType.setObjectName(u"cbType")
|
|
136
146
|
|
|
137
|
-
self.
|
|
147
|
+
self.horizontalLayout_7.addWidget(self.cbType)
|
|
148
|
+
|
|
149
|
+
self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
150
|
+
|
|
151
|
+
self.horizontalLayout_7.addItem(self.horizontalSpacer_3)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
self.verticalLayout.addLayout(self.horizontalLayout_7)
|
|
138
155
|
|
|
139
156
|
self.lbValues = QLabel(Dialog)
|
|
140
157
|
self.lbValues.setObjectName(u"lbValues")
|
|
@@ -198,28 +215,32 @@ class Ui_Dialog(object):
|
|
|
198
215
|
|
|
199
216
|
self.horizontalLayout_4 = QHBoxLayout()
|
|
200
217
|
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
|
|
201
|
-
|
|
202
|
-
self.verticalLayout.addLayout(self.horizontalLayout_4)
|
|
203
|
-
|
|
204
218
|
self.pb_add_subjects = QPushButton(Dialog)
|
|
205
219
|
self.pb_add_subjects.setObjectName(u"pb_add_subjects")
|
|
206
220
|
|
|
207
|
-
self.
|
|
221
|
+
self.horizontalLayout_4.addWidget(self.pb_add_subjects)
|
|
208
222
|
|
|
209
223
|
self.pb_load_file = QPushButton(Dialog)
|
|
210
224
|
self.pb_load_file.setObjectName(u"pb_load_file")
|
|
211
225
|
|
|
212
|
-
self.
|
|
226
|
+
self.horizontalLayout_4.addWidget(self.pb_load_file)
|
|
227
|
+
|
|
228
|
+
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
229
|
+
|
|
230
|
+
self.horizontalLayout_4.addItem(self.horizontalSpacer_2)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
self.verticalLayout.addLayout(self.horizontalLayout_4)
|
|
213
234
|
|
|
214
235
|
self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
215
236
|
|
|
216
237
|
self.verticalLayout.addItem(self.verticalSpacer_3)
|
|
217
238
|
|
|
218
239
|
|
|
219
|
-
self.
|
|
240
|
+
self.horizontalLayout_8.addLayout(self.verticalLayout)
|
|
220
241
|
|
|
221
242
|
|
|
222
|
-
self.verticalLayout_4.addLayout(self.
|
|
243
|
+
self.verticalLayout_4.addLayout(self.horizontalLayout_8)
|
|
223
244
|
|
|
224
245
|
self.horizontalLayout_2 = QHBoxLayout()
|
|
225
246
|
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
|
@@ -241,9 +262,6 @@ class Ui_Dialog(object):
|
|
|
241
262
|
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
|
242
263
|
|
|
243
264
|
|
|
244
|
-
self.verticalLayout_5.addLayout(self.verticalLayout_4)
|
|
245
|
-
|
|
246
|
-
|
|
247
265
|
self.retranslateUi(Dialog)
|
|
248
266
|
|
|
249
267
|
self.tabWidgetModifiersSets.setCurrentIndex(-1)
|
|
@@ -256,8 +274,8 @@ class Ui_Dialog(object):
|
|
|
256
274
|
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Set modifiers", None))
|
|
257
275
|
self.cb_ask_at_stop.setText(QCoreApplication.translate("Dialog", u"Ask for modifier(s) when behavior stops", None))
|
|
258
276
|
self.lbModifier.setText(QCoreApplication.translate("Dialog", u"Modifier", None))
|
|
259
|
-
self.lbCode.setText(QCoreApplication.translate("Dialog", u"
|
|
260
|
-
self.lbCodeHelp.setText(QCoreApplication.translate("Dialog", u"
|
|
277
|
+
self.lbCode.setText(QCoreApplication.translate("Dialog", u"Shortcut", None))
|
|
278
|
+
self.lbCodeHelp.setText(QCoreApplication.translate("Dialog", u"The shortcut is case sensitive. Type one character or a function key (F1, F2... F12)", None))
|
|
261
279
|
self.pbAddModifier.setText(QCoreApplication.translate("Dialog", u"->", None))
|
|
262
280
|
self.pbModifyModifier.setText(QCoreApplication.translate("Dialog", u"<-", None))
|
|
263
281
|
self.lb_name.setText(QCoreApplication.translate("Dialog", u"Set name", None))
|
|
@@ -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)
|
|
@@ -119,12 +119,12 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
119
119
|
self.saveMapAction.setShortcut("Ctrl+S")
|
|
120
120
|
self.saveMapAction.setStatusTip("Save the behavior coding map")
|
|
121
121
|
self.saveMapAction.setEnabled(False)
|
|
122
|
-
self.saveMapAction.triggered.connect(self.
|
|
122
|
+
self.saveMapAction.triggered.connect(self.save_map_clicked)
|
|
123
123
|
|
|
124
124
|
self.saveAsMapAction = QAction(QIcon(), "Save the behavior coding map as ...", self)
|
|
125
125
|
self.saveAsMapAction.setStatusTip("Save the behavior coding map as ...")
|
|
126
126
|
self.saveAsMapAction.setEnabled(False)
|
|
127
|
-
self.saveAsMapAction.triggered.connect(self.
|
|
127
|
+
self.saveAsMapAction.triggered.connect(self.save_as_map_clicked)
|
|
128
128
|
|
|
129
129
|
self.mapNameAction = QAction(QIcon(), "&Edit name of behaviors coding map", self)
|
|
130
130
|
self.mapNameAction.setShortcut("Ctrl+M")
|
|
@@ -389,7 +389,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
389
389
|
)
|
|
390
390
|
|
|
391
391
|
if response == cfg.SAVE:
|
|
392
|
-
if not self.
|
|
392
|
+
if not self.save_map_clicked():
|
|
393
393
|
event.ignore()
|
|
394
394
|
|
|
395
395
|
if response == cfg.CANCEL:
|
|
@@ -615,7 +615,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
615
615
|
)
|
|
616
616
|
|
|
617
617
|
if response == cfg.SAVE:
|
|
618
|
-
if not self.
|
|
618
|
+
if not self.save_map_clicked():
|
|
619
619
|
return
|
|
620
620
|
|
|
621
621
|
if response == cfg.CANCEL:
|
|
@@ -659,7 +659,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
659
659
|
["Save", "Discard", "Cancel"],
|
|
660
660
|
)
|
|
661
661
|
|
|
662
|
-
if (response == "Save" and not self.
|
|
662
|
+
if (response == "Save" and not self.save_map_clicked()) or (response == "Cancel"):
|
|
663
663
|
return
|
|
664
664
|
|
|
665
665
|
fileName, _ = QFileDialog(self).getOpenFileName(
|
|
@@ -783,7 +783,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
783
783
|
else:
|
|
784
784
|
return False
|
|
785
785
|
|
|
786
|
-
def
|
|
786
|
+
def save_as_map_clicked(self):
|
|
787
787
|
filters = "Behaviors coding map (*.behav_coding_map);;All files (*)"
|
|
788
788
|
|
|
789
789
|
self.fileName, _ = QFileDialog.getSaveFileName(self, "Save behaviors coding map as", "", filters)
|
|
@@ -794,7 +794,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
794
794
|
self.fileName += ".behav_coding_map"
|
|
795
795
|
self.saveMap()
|
|
796
796
|
|
|
797
|
-
def
|
|
797
|
+
def save_map_clicked(self):
|
|
798
798
|
if not self.fileName:
|
|
799
799
|
self.fileName, _ = QFileDialog().getSaveFileName(
|
|
800
800
|
self,
|
boris/coding_pad.py
CHANGED
|
@@ -38,7 +38,7 @@ class Button(QWidget):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class CodingPad(QWidget):
|
|
41
|
-
|
|
41
|
+
click_signal = Signal(str)
|
|
42
42
|
sendEventSignal = Signal(QEvent)
|
|
43
43
|
close_signal = Signal(QRect, dict)
|
|
44
44
|
|
|
@@ -208,7 +208,8 @@ class CodingPad(QWidget):
|
|
|
208
208
|
"""
|
|
209
209
|
Button clicked
|
|
210
210
|
"""
|
|
211
|
-
|
|
211
|
+
print(f"{behavior_code=}")
|
|
212
|
+
self.click_signal.emit(behavior_code)
|
|
212
213
|
|
|
213
214
|
def eventFilter(self, receiver, event) -> bool:
|
|
214
215
|
"""
|
|
@@ -261,7 +262,7 @@ def show_coding_pad(self):
|
|
|
261
262
|
self.codingpad.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
262
263
|
self.codingpad.sendEventSignal.connect(self.signal_from_widget)
|
|
263
264
|
|
|
264
|
-
self.codingpad.
|
|
265
|
+
self.codingpad.click_signal.connect(self.click_signal_from_coding_pad)
|
|
265
266
|
self.codingpad.close_signal.connect(self.close_signal_from_coding_pad)
|
|
266
267
|
self.codingpad.show()
|
|
267
268
|
|
boris/config.py
CHANGED
|
@@ -132,6 +132,8 @@ POINT_EVENT_PLOT_COLOR = "black"
|
|
|
132
132
|
|
|
133
133
|
CHAR_FORBIDDEN_IN_MODIFIERS = "(|),`~"
|
|
134
134
|
|
|
135
|
+
FAST_FORWARD_DEFAULT_VALUE: float = 10.0
|
|
136
|
+
|
|
135
137
|
ADAPT_FAST_JUMP = "adapt_fast_jump"
|
|
136
138
|
ADAPT_FAST_JUMP_DEFAULT = False
|
|
137
139
|
|
|
@@ -419,6 +421,10 @@ MPV_HWDEC_AUTOSAFE = "auto-safe"
|
|
|
419
421
|
MPV_HWDEC_OPTIONS = (MPV_HWDEC_AUTO, MPV_HWDEC_AUTOSAFE, MPV_HWDEC_NO)
|
|
420
422
|
MPV_HWDEC_DEFAULT_VALUE = MPV_HWDEC_AUTO
|
|
421
423
|
|
|
424
|
+
# frame step size (disabled)
|
|
425
|
+
# FRAME_STEP_SIZE: str = "frame_step_size"
|
|
426
|
+
# FRAME_STEP_SIZE_DEFAULT_VALUE: int = 1
|
|
427
|
+
|
|
422
428
|
ANALYSIS_PLUGINS = "analysis_plugins"
|
|
423
429
|
EXCLUDED_PLUGINS = "excluded_plugins"
|
|
424
430
|
PERSONAL_PLUGINS_DIR = "personal_plugins_dir"
|
|
@@ -730,6 +736,7 @@ INIT_PARAM = {
|
|
|
730
736
|
MPV_HWDEC: MPV_HWDEC_DEFAULT_VALUE,
|
|
731
737
|
PROJECT_FILE_INDENTATION: PROJECT_FILE_INDENTATION_DEFAULT_VALUE,
|
|
732
738
|
f"{MEDIA} tw fields": MEDIA_TW_EVENTS_FIELDS_DEFAULT,
|
|
739
|
+
# FRAME_STEP_SIZE: FRAME_STEP_SIZE_DEFAULT_VALUE,
|
|
733
740
|
}
|
|
734
741
|
|
|
735
742
|
SDIS_EXT = "sds"
|
boris/config_file.py
CHANGED
|
@@ -82,11 +82,11 @@ def read(self):
|
|
|
82
82
|
|
|
83
83
|
logging.debug(f"time format: {self.timeFormat}")
|
|
84
84
|
|
|
85
|
-
self.fast =
|
|
85
|
+
self.fast = cfg.FAST_FORWARD_DEFAULT_VALUE
|
|
86
86
|
try:
|
|
87
|
-
self.fast =
|
|
87
|
+
self.fast = float(settings.value("Time/fast_forward_speed"))
|
|
88
88
|
except Exception:
|
|
89
|
-
self.fast =
|
|
89
|
+
self.fast = cfg.FAST_FORWARD_DEFAULT_VALUE
|
|
90
90
|
|
|
91
91
|
logging.debug(f"Time/fast_forward_speed: {self.fast}")
|
|
92
92
|
|