boris-behav-obs 9.7.12__py3-none-any.whl → 9.8.2__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/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +4 -3
- boris/add_modifier.py +1 -1
- boris/advanced_event_filtering.py +1 -1
- boris/analysis_plugins/export_to_feral.py +336 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +2 -2
- boris/behav_coding_map_creator.py +1 -1
- boris/behavior_binary_table.py +1 -1
- boris/behaviors_coding_map.py +1 -1
- boris/boris_cli.py +1 -1
- boris/cmd_arguments.py +1 -1
- boris/coding_pad.py +1 -1
- boris/config.py +15 -3
- boris/config_file.py +18 -19
- boris/connections.py +12 -13
- boris/converters.py +1 -1
- boris/converters_ui.py +2 -3
- boris/cooccurence.py +1 -1
- boris/core.py +168 -166
- boris/core_qrc.py +1830 -1967
- boris/core_ui.py +1 -1
- boris/db_functions.py +5 -14
- boris/dialog.py +24 -24
- boris/edit_event.py +1 -1
- boris/event_operations.py +1 -1
- boris/events_cursor.py +1 -1
- boris/events_snapshots.py +133 -78
- boris/exclusion_matrix.py +1 -1
- boris/export_events.py +49 -43
- boris/export_observation.py +1 -1
- boris/external_processes.py +1 -1
- boris/geometric_measurement.py +1 -1
- boris/gui_utilities.py +1 -1
- boris/image_overlay.py +1 -1
- boris/import_observations.py +1 -1
- boris/ipc_mpv.py +1 -1
- boris/irr.py +1 -1
- boris/latency.py +1 -1
- boris/measurement_widget.py +1 -1
- boris/media_file.py +1 -1
- boris/menu_options.py +14 -12
- boris/modifier_coding_map_creator.py +1 -1
- boris/modifiers_coding_map.py +1 -1
- boris/observation.py +13 -14
- boris/observation_operations.py +1 -1
- boris/observations_list.py +1 -1
- boris/otx_parser.py +1 -1
- boris/param_panel.py +1 -1
- boris/player_dock_widget.py +1 -1
- boris/plot_data_module.py +1 -1
- boris/plot_events.py +1 -1
- boris/plot_events_rt.py +1 -1
- boris/plot_spectrogram_rt.py +42 -73
- boris/plot_waveform_rt.py +1 -1
- boris/plugins.py +1 -1
- boris/preferences.py +35 -4
- boris/preferences_ui.py +48 -18
- boris/project.py +1 -1
- boris/project_functions.py +19 -22
- boris/project_import_export.py +1 -1
- boris/select_modifiers.py +1 -1
- boris/select_observations.py +22 -23
- boris/select_subj_behav.py +4 -4
- boris/state_events.py +1 -1
- boris/subjects_pad.py +1 -1
- boris/synthetic_time_budget.py +1 -1
- boris/time_budget_functions.py +1 -1
- boris/time_budget_widget.py +1 -1
- boris/transitions.py +1 -1
- boris/utilities.py +1 -1
- boris/version.py +3 -3
- boris/video_equalizer.py +1 -1
- boris/video_operations.py +1 -1
- boris/view_df.py +28 -4
- boris/write_event.py +1 -1
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/METADATA +2 -2
- boris_behav_obs-9.8.2.dist-info/RECORD +110 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/WHEEL +1 -1
- boris/analysis_plugins/_export_to_feral.py +0 -225
- boris_behav_obs-9.7.12.dist-info/RECORD +0 -110
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/top_level.txt +0 -0
boris/__init__.py
CHANGED
boris/__main__.py
CHANGED
boris/about.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -80,7 +80,8 @@ def actionAbout_activated(self):
|
|
|
80
80
|
programs_versions.extend(["\nGraphViz", gv_result if "graphviz" in gv_result else "not installed", "https://www.graphviz.org/"])
|
|
81
81
|
|
|
82
82
|
about_dialog: QMessageBox = QMessageBox()
|
|
83
|
-
about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
|
|
83
|
+
# about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
|
|
84
|
+
about_dialog.setIconPixmap(QPixmap(":/dbios_unito"))
|
|
84
85
|
|
|
85
86
|
about_dialog.setWindowTitle(f"About {cfg.programName}")
|
|
86
87
|
about_dialog.setStandardButtons(QMessageBox.Ok)
|
|
@@ -90,7 +91,7 @@ def actionAbout_activated(self):
|
|
|
90
91
|
about_dialog.setInformativeText(
|
|
91
92
|
(
|
|
92
93
|
f"<b>{cfg.programName}</b> v. {version.__version__} - {version.__version_date__}"
|
|
93
|
-
"<p>Copyright © 2012-
|
|
94
|
+
"<p>Copyright © 2012-2026 Olivier Friard - Marco Gamba<br>"
|
|
94
95
|
"Department of Life Sciences and Systems Biology<br>"
|
|
95
96
|
"University of Torino - Italy<br>"
|
|
96
97
|
"<br>"
|
boris/add_modifier.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS plugin
|
|
3
|
+
|
|
4
|
+
Export observations to FERAL (getferal.ai)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtWidgets import (
|
|
13
|
+
QDialog,
|
|
14
|
+
QFileDialog,
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QLabel,
|
|
17
|
+
QListWidget,
|
|
18
|
+
QListWidgetItem,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = "0.3.2"
|
|
24
|
+
__version_date__ = "2025-12-19"
|
|
25
|
+
__plugin_name__ = "Export observations to FERAL"
|
|
26
|
+
__author__ = "Jacopo Razzauti - The Rockefeller University; Olivier Friard - University of Torino - Italy"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------
|
|
30
|
+
# Dialog: choose behaviors
|
|
31
|
+
# ---------------------------
|
|
32
|
+
class BehaviorSelectDialog(QDialog):
|
|
33
|
+
"""Select which BORIS behavior codes should become FERAL classes.
|
|
34
|
+
|
|
35
|
+
Class 0 is reserved for "other". Any behavior not selected is mapped to 0.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, behavior_codes, parent=None):
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
|
|
41
|
+
self.setWindowTitle("Select behaviors to export (0 is 'other')")
|
|
42
|
+
self.setModal(True)
|
|
43
|
+
|
|
44
|
+
main_layout = QVBoxLayout(self)
|
|
45
|
+
|
|
46
|
+
info = QLabel("Select behaviors to export.\nClass 0 is reserved for 'other'.\nUnselected behaviors are mapped to 0.")
|
|
47
|
+
info.setWordWrap(True)
|
|
48
|
+
main_layout.addWidget(info)
|
|
49
|
+
|
|
50
|
+
self.list_behaviors = QListWidget()
|
|
51
|
+
self.list_behaviors.setSelectionMode(QListWidget.ExtendedSelection)
|
|
52
|
+
|
|
53
|
+
for code in sorted(behavior_codes):
|
|
54
|
+
item = QListWidgetItem(code)
|
|
55
|
+
item.setSelected(True) # default: select all
|
|
56
|
+
self.list_behaviors.addItem(item)
|
|
57
|
+
|
|
58
|
+
main_layout.addWidget(self.list_behaviors)
|
|
59
|
+
|
|
60
|
+
buttons_layout = QHBoxLayout()
|
|
61
|
+
btn_all = QPushButton("Select all")
|
|
62
|
+
btn_none = QPushButton("Select none")
|
|
63
|
+
btn_ok = QPushButton("OK")
|
|
64
|
+
btn_cancel = QPushButton("Cancel")
|
|
65
|
+
|
|
66
|
+
btn_all.clicked.connect(self._select_all)
|
|
67
|
+
btn_none.clicked.connect(self._select_none)
|
|
68
|
+
btn_ok.clicked.connect(self.accept)
|
|
69
|
+
btn_cancel.clicked.connect(self.reject)
|
|
70
|
+
|
|
71
|
+
buttons_layout.addWidget(btn_all)
|
|
72
|
+
buttons_layout.addWidget(btn_none)
|
|
73
|
+
buttons_layout.addStretch()
|
|
74
|
+
buttons_layout.addWidget(btn_ok)
|
|
75
|
+
buttons_layout.addWidget(btn_cancel)
|
|
76
|
+
|
|
77
|
+
main_layout.addLayout(buttons_layout)
|
|
78
|
+
|
|
79
|
+
def _select_all(self):
|
|
80
|
+
for i in range(self.list_behaviors.count()):
|
|
81
|
+
self.list_behaviors.item(i).setSelected(True)
|
|
82
|
+
|
|
83
|
+
def _select_none(self):
|
|
84
|
+
for i in range(self.list_behaviors.count()):
|
|
85
|
+
self.list_behaviors.item(i).setSelected(False)
|
|
86
|
+
|
|
87
|
+
def selected_codes(self):
|
|
88
|
+
return [it.text() for it in self.list_behaviors.selectedItems()]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------
|
|
92
|
+
# Dialog: split videos
|
|
93
|
+
# ---------------------------
|
|
94
|
+
class CategoryDialog(QDialog):
|
|
95
|
+
def __init__(self, items, parent=None):
|
|
96
|
+
super().__init__(parent)
|
|
97
|
+
|
|
98
|
+
self.setWindowTitle("Organize the videos in categories")
|
|
99
|
+
self.setModal(True)
|
|
100
|
+
|
|
101
|
+
main_layout = QVBoxLayout(self)
|
|
102
|
+
lists_layout = QHBoxLayout()
|
|
103
|
+
|
|
104
|
+
self.list_unclassified = self._make_list_widget()
|
|
105
|
+
self.list_train = self._make_list_widget()
|
|
106
|
+
self.list_val = self._make_list_widget()
|
|
107
|
+
self.list_test = self._make_list_widget()
|
|
108
|
+
self.list_inference = self._make_list_widget()
|
|
109
|
+
|
|
110
|
+
lists_layout.addLayout(self._make_column("All videos", self.list_unclassified))
|
|
111
|
+
lists_layout.addLayout(self._make_column("train", self.list_train))
|
|
112
|
+
lists_layout.addLayout(self._make_column("val", self.list_val))
|
|
113
|
+
lists_layout.addLayout(self._make_column("test", self.list_test))
|
|
114
|
+
lists_layout.addLayout(self._make_column("inference", self.list_inference))
|
|
115
|
+
|
|
116
|
+
main_layout.addLayout(lists_layout)
|
|
117
|
+
|
|
118
|
+
buttons_layout = QHBoxLayout()
|
|
119
|
+
btn_ok = QPushButton("OK")
|
|
120
|
+
btn_cancel = QPushButton("Cancel")
|
|
121
|
+
btn_ok.clicked.connect(self.accept)
|
|
122
|
+
btn_cancel.clicked.connect(self.reject)
|
|
123
|
+
|
|
124
|
+
buttons_layout.addStretch()
|
|
125
|
+
buttons_layout.addWidget(btn_ok)
|
|
126
|
+
buttons_layout.addWidget(btn_cancel)
|
|
127
|
+
main_layout.addLayout(buttons_layout)
|
|
128
|
+
|
|
129
|
+
for text in items:
|
|
130
|
+
QListWidgetItem(text, self.list_unclassified)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _make_column(title, widget):
|
|
134
|
+
col = QVBoxLayout()
|
|
135
|
+
col.addWidget(QLabel(title))
|
|
136
|
+
col.addWidget(widget)
|
|
137
|
+
return col
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _make_list_widget():
|
|
141
|
+
lw = QListWidget()
|
|
142
|
+
lw.setSelectionMode(QListWidget.ExtendedSelection)
|
|
143
|
+
lw.setDragEnabled(True)
|
|
144
|
+
lw.setAcceptDrops(True)
|
|
145
|
+
lw.setDropIndicatorShown(True)
|
|
146
|
+
lw.setDragDropMode(QListWidget.DragDrop)
|
|
147
|
+
lw.setDefaultDropAction(Qt.MoveAction)
|
|
148
|
+
return lw
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _collect(widget):
|
|
152
|
+
# "*" is used to mark videos with at least one event
|
|
153
|
+
return [widget.item(i).text().rstrip("*") for i in range(widget.count())]
|
|
154
|
+
|
|
155
|
+
def categories(self):
|
|
156
|
+
return {
|
|
157
|
+
"unclassified": self._collect(self.list_unclassified),
|
|
158
|
+
"train": self._collect(self.list_train),
|
|
159
|
+
"val": self._collect(self.list_val),
|
|
160
|
+
"test": self._collect(self.list_test),
|
|
161
|
+
"inference": self._collect(self.list_inference),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run(df: pd.DataFrame, project: dict):
|
|
166
|
+
"""Export BORIS observations/events to a FERAL-compatible JSON.
|
|
167
|
+
|
|
168
|
+
See https://www.getferal.ai/ > Label Preparation
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def log(msg):
|
|
172
|
+
messages.append(str(msg))
|
|
173
|
+
|
|
174
|
+
def safe_float(d, key):
|
|
175
|
+
try:
|
|
176
|
+
return float(d[key])
|
|
177
|
+
except Exception:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
messages = []
|
|
181
|
+
|
|
182
|
+
out = {
|
|
183
|
+
"is_multilabel": False,
|
|
184
|
+
"splits": {"train": [], "val": [], "test": [], "inference": []},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ---- Behaviors (FERAL classes) ----
|
|
188
|
+
behavior_conf = project.get("behaviors_conf", {})
|
|
189
|
+
boris_codes = [behavior_conf[k].get("code") for k in behavior_conf]
|
|
190
|
+
boris_codes = [c for c in boris_codes if c] # drop None/empty
|
|
191
|
+
|
|
192
|
+
# Reserve 0 for background. If BORIS has a behavior literally named "other",
|
|
193
|
+
# treat it as background and do not include as a class.
|
|
194
|
+
boris_codes_no_other = [c for c in boris_codes if c != "other"]
|
|
195
|
+
|
|
196
|
+
dlg = BehaviorSelectDialog(boris_codes_no_other)
|
|
197
|
+
if not dlg.exec():
|
|
198
|
+
return "Behavior selection canceled; export aborted."
|
|
199
|
+
|
|
200
|
+
selected = sorted(set(dlg.selected_codes()))
|
|
201
|
+
if not selected:
|
|
202
|
+
log("No behaviors selected: everything will be mapped to class 0 ('other').")
|
|
203
|
+
|
|
204
|
+
class_names = {"0": "other"}
|
|
205
|
+
for i, code in enumerate(selected, start=1):
|
|
206
|
+
class_names[str(i)] = code
|
|
207
|
+
|
|
208
|
+
out["class_names"] = class_names
|
|
209
|
+
behavior_to_idx = {code: i for i, code in enumerate(selected, start=1)}
|
|
210
|
+
|
|
211
|
+
if selected:
|
|
212
|
+
log(f"Selected behaviors: {', '.join(selected)}")
|
|
213
|
+
log(f"Classes: {class_names}")
|
|
214
|
+
|
|
215
|
+
# ---- Iterate observations/videos ----
|
|
216
|
+
labels = {}
|
|
217
|
+
video_list = []
|
|
218
|
+
|
|
219
|
+
# df dataframe cannot have a "Media file" column
|
|
220
|
+
# has_media_file_col = "Media file" in df.columns
|
|
221
|
+
has_subject_col = "Subject" in df.columns
|
|
222
|
+
|
|
223
|
+
observations = sorted(project.get("observations", {}).keys())
|
|
224
|
+
if not observations:
|
|
225
|
+
return "No observations found in project; nothing to export."
|
|
226
|
+
|
|
227
|
+
for obs_id in observations:
|
|
228
|
+
log("---")
|
|
229
|
+
log(obs_id)
|
|
230
|
+
|
|
231
|
+
obs = project["observations"][obs_id]
|
|
232
|
+
media_files = obs.get("file", {}).get("1", [])
|
|
233
|
+
if not media_files:
|
|
234
|
+
log(f"Observation {obs_id} has no video in player 1.")
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
media_info = obs.get("media_info", {})
|
|
238
|
+
fps_dict = media_info.get("fps", {})
|
|
239
|
+
length_dict = media_info.get("length", {})
|
|
240
|
+
frames_dict = media_info.get("frames", {}) or {}
|
|
241
|
+
|
|
242
|
+
for media_path in media_files:
|
|
243
|
+
video_name = Path(media_path).name
|
|
244
|
+
|
|
245
|
+
if video_name in labels:
|
|
246
|
+
log(f"Duplicate video name '{video_name}' encountered; skipping (obs {obs_id}).")
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# df dataframe cannot have a "Media file" column
|
|
250
|
+
# Filter events for this observation + this media file when possible
|
|
251
|
+
# if has_media_file_col:
|
|
252
|
+
# df_video = df[(df["Observation id"] == obs_id) & (df["Media file"] == media_path)]
|
|
253
|
+
# else:
|
|
254
|
+
# df_video = df[df["Observation id"] == obs_id]
|
|
255
|
+
# log("Warning: df has no 'Media file' column; using all events from observation.")
|
|
256
|
+
|
|
257
|
+
df_video = df[df["Observation id"] == obs_id]
|
|
258
|
+
|
|
259
|
+
# Enforce single-subject labeling when Subject column exists
|
|
260
|
+
if has_subject_col and not df_video.empty:
|
|
261
|
+
subjects = df_video["Subject"].dropna().unique().tolist()
|
|
262
|
+
if len(subjects) > 1:
|
|
263
|
+
log(f"More than one subject in {video_name}: {subjects}. Skipping.")
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Mark videos that contain at least one event with "*"
|
|
267
|
+
video_list.append(video_name + ("*" if not df_video.empty else ""))
|
|
268
|
+
|
|
269
|
+
fps = safe_float(fps_dict, media_path)
|
|
270
|
+
duration = safe_float(length_dict, media_path)
|
|
271
|
+
if fps is None:
|
|
272
|
+
log(f"Missing/invalid FPS for {video_name}. Skipping.")
|
|
273
|
+
continue
|
|
274
|
+
if duration is None:
|
|
275
|
+
log(f"Missing/invalid duration for {video_name}. Skipping.")
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if media_path in frames_dict:
|
|
279
|
+
n_frames = int(frames_dict[media_path])
|
|
280
|
+
log(f"{video_name}: fps={fps} duration={duration} frames={n_frames} (BORIS)")
|
|
281
|
+
else:
|
|
282
|
+
n_frames = int(round(duration * fps))
|
|
283
|
+
log(f"{video_name}: fps={fps} duration={duration} frames={n_frames} (rounded)")
|
|
284
|
+
|
|
285
|
+
if n_frames <= 0:
|
|
286
|
+
log(f"Non-positive frame count for {video_name}. Skipping.")
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
frame_dt = 1.0 / fps
|
|
290
|
+
labels[video_name] = [0] * n_frames # default: "other"
|
|
291
|
+
|
|
292
|
+
# Fill per-frame labels
|
|
293
|
+
for frame_idx in range(n_frames):
|
|
294
|
+
t = frame_idx * frame_dt
|
|
295
|
+
behaviors = df_video[(df_video["Start (s)"] <= t) & (df_video["Stop (s)"] >= t)]["Behavior"].unique().tolist()
|
|
296
|
+
|
|
297
|
+
if len(behaviors) > 1:
|
|
298
|
+
log(
|
|
299
|
+
f"{video_name}: overlapping behaviors at frame {frame_idx} (t={t:.6f}s): "
|
|
300
|
+
f"{behaviors}. Removing video (is_multilabel=False)."
|
|
301
|
+
)
|
|
302
|
+
del labels[video_name]
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
if not behaviors:
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
labels[video_name][frame_idx] = behavior_to_idx.get(behaviors[0], 0)
|
|
309
|
+
|
|
310
|
+
out["labels"] = labels
|
|
311
|
+
|
|
312
|
+
# ---- Splits dialog ----
|
|
313
|
+
split_dlg = CategoryDialog(video_list)
|
|
314
|
+
if not split_dlg.exec():
|
|
315
|
+
log("Export canceled at split assignment stage.")
|
|
316
|
+
return "\n".join(messages)
|
|
317
|
+
|
|
318
|
+
splits = split_dlg.categories()
|
|
319
|
+
splits.pop("unclassified", None)
|
|
320
|
+
out["splits"] = splits
|
|
321
|
+
|
|
322
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
323
|
+
None,
|
|
324
|
+
"Choose a file to save",
|
|
325
|
+
"",
|
|
326
|
+
"JSON files (*.json);;All files (*.*)",
|
|
327
|
+
)
|
|
328
|
+
if not filename:
|
|
329
|
+
log("No output file selected; nothing written.")
|
|
330
|
+
return "\n".join(messages)
|
|
331
|
+
|
|
332
|
+
with open(filename, "w", encoding="utf-8") as f_out:
|
|
333
|
+
json.dump(out, f_out, indent=2)
|
|
334
|
+
|
|
335
|
+
log(f"Saved: {filename}")
|
|
336
|
+
return "\n".join(messages)
|
|
@@ -4,9 +4,9 @@ BORIS plugin
|
|
|
4
4
|
Inter Rater Reliability (IRR) Weighted Cohen's Kappa
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
from typing import List, Tuple, Dict, Optional
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
8
|
|
|
9
|
+
import pandas as pd
|
|
10
10
|
from PySide6.QtWidgets import QInputDialog
|
|
11
11
|
|
|
12
12
|
__version__ = "0.0.3"
|
boris/behavior_binary_table.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
boris/behaviors_coding_map.py
CHANGED
boris/boris_cli.py
CHANGED
|
@@ -3,7 +3,7 @@ BORIS CLI
|
|
|
3
3
|
|
|
4
4
|
Behavioral Observation Research Interactive Software Command Line Interface
|
|
5
5
|
|
|
6
|
-
Copyright 2012-
|
|
6
|
+
Copyright 2012-2026 Olivier Friard
|
|
7
7
|
|
|
8
8
|
This program is free software; you can redistribute it and/or modify
|
|
9
9
|
it under the terms of the GNU General Public License as published by
|
boris/cmd_arguments.py
CHANGED
boris/coding_pad.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
boris/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -165,6 +165,7 @@ REMOVE = "Remove"
|
|
|
165
165
|
SAVE = "Save"
|
|
166
166
|
DISCARD = "Discard"
|
|
167
167
|
OK = "OK"
|
|
168
|
+
ABORT = "Abort"
|
|
168
169
|
OVERWRITE = "Overwrite"
|
|
169
170
|
OVERWRITE_ALL = "Overwrite all"
|
|
170
171
|
SKIP = "Skip"
|
|
@@ -493,6 +494,13 @@ FRAME_DEFAULT_CACHE_SIZE = 1
|
|
|
493
494
|
|
|
494
495
|
EXCLUDED = "excluded"
|
|
495
496
|
|
|
497
|
+
# codes for Input_dialog class
|
|
498
|
+
CHECKBOX = "cb"
|
|
499
|
+
LINE_EDIT = "le"
|
|
500
|
+
SPINBOX = "sb"
|
|
501
|
+
DOUBLE_SPINBOX = "dsb"
|
|
502
|
+
ITEMS_LIST = "il"
|
|
503
|
+
|
|
496
504
|
# modifiers
|
|
497
505
|
MODIFIERS = "modifiers"
|
|
498
506
|
SINGLE_SELECTION = 0
|
|
@@ -539,10 +547,13 @@ SPECTROGRAM_DEFAULT_TIME_INTERVAL = 10
|
|
|
539
547
|
SPECTROGRAM_WINDOW_TYPE = "SPECTROGRAM_WINDOW_TYPE"
|
|
540
548
|
SPECTROGRAM_DEFAULT_WINDOW_TYPE = "hanning"
|
|
541
549
|
SPECTROGRAM_NFFT = "SPECTROGRAM_NFFT"
|
|
542
|
-
SPECTROGRAM_DEFAULT_NFFT = "
|
|
550
|
+
SPECTROGRAM_DEFAULT_NFFT = "256"
|
|
543
551
|
SPECTROGRAM_NOVERLAP = "SPECTROGRAM_NOVERLAP"
|
|
544
|
-
SPECTROGRAM_DEFAULT_NOVERLAP =
|
|
552
|
+
SPECTROGRAM_DEFAULT_NOVERLAP = 128
|
|
545
553
|
SPECTROGRAM_VMIN = "SPECTROGRAM_VMIN"
|
|
554
|
+
|
|
555
|
+
SPECTROGRAM_USE_VMIN_VMAX = "SPECTROGRAM_USE_VMIN_VMAX"
|
|
556
|
+
SPECTROGRAM_USE_VMIN_VMAX_DEFAULT = False
|
|
546
557
|
SPECTROGRAM_DEFAULT_VMIN = -100
|
|
547
558
|
SPECTROGRAM_VMAX = "SPECTROGRAM_VMAX"
|
|
548
559
|
SPECTROGRAM_DEFAULT_VMAX = -20
|
|
@@ -737,6 +748,7 @@ INIT_PARAM = {
|
|
|
737
748
|
PROJECT_FILE_INDENTATION: PROJECT_FILE_INDENTATION_DEFAULT_VALUE,
|
|
738
749
|
f"{MEDIA} tw fields": MEDIA_TW_EVENTS_FIELDS_DEFAULT,
|
|
739
750
|
# FRAME_STEP_SIZE: FRAME_STEP_SIZE_DEFAULT_VALUE,
|
|
751
|
+
TOOLBAR_ICON_SIZE: DEFAULT_TOOLBAR_ICON_SIZE_VALUE,
|
|
740
752
|
}
|
|
741
753
|
|
|
742
754
|
SDIS_EXT = "sds"
|
boris/config_file.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -21,27 +21,27 @@ This file is part of BORIS.
|
|
|
21
21
|
Read and write the BORIS config file
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
import pathlib as pl
|
|
25
24
|
import logging
|
|
25
|
+
import pathlib as pl
|
|
26
26
|
import time
|
|
27
27
|
|
|
28
|
+
from PySide6.QtCore import QByteArray, QSettings
|
|
29
|
+
|
|
28
30
|
from . import config as cfg
|
|
29
31
|
from . import dialog
|
|
30
32
|
|
|
31
|
-
from PySide6.QtCore import QByteArray, QSettings
|
|
32
|
-
|
|
33
33
|
|
|
34
|
-
def read(self):
|
|
34
|
+
def read(self) -> None:
|
|
35
35
|
"""
|
|
36
36
|
read config file
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
ini_file_path = pl.Path.home() / pl.Path(".boris")
|
|
40
40
|
|
|
41
|
-
logging.debug(f"read config file: {
|
|
41
|
+
logging.debug(f"read config file: {ini_file_path}")
|
|
42
42
|
|
|
43
|
-
if
|
|
44
|
-
settings = QSettings(str(
|
|
43
|
+
if ini_file_path.is_file():
|
|
44
|
+
settings = QSettings(str(ini_file_path), QSettings.Format.IniFormat)
|
|
45
45
|
|
|
46
46
|
try:
|
|
47
47
|
self.config_param = settings.value("config")
|
|
@@ -180,7 +180,7 @@ def read(self):
|
|
|
180
180
|
"(An internet connection is required)\n"
|
|
181
181
|
"You can change this option in the Preferences (File > Preferences)"
|
|
182
182
|
),
|
|
183
|
-
|
|
183
|
+
(cfg.YES, cfg.NO),
|
|
184
184
|
)
|
|
185
185
|
== cfg.YES
|
|
186
186
|
)
|
|
@@ -244,7 +244,7 @@ def read(self):
|
|
|
244
244
|
dialog.MessageDialog(
|
|
245
245
|
cfg.programName,
|
|
246
246
|
("The colors list contain colors that are very light.\nDo you want to reload the default colors list?"),
|
|
247
|
-
|
|
247
|
+
(cfg.NO, cfg.YES),
|
|
248
248
|
)
|
|
249
249
|
== cfg.YES
|
|
250
250
|
):
|
|
@@ -261,7 +261,7 @@ def read(self):
|
|
|
261
261
|
dialog.MessageDialog(
|
|
262
262
|
cfg.programName,
|
|
263
263
|
("The colors list contain colors that are very light.\nDo you want to reload the default colors list?"),
|
|
264
|
-
|
|
264
|
+
(cfg.NO, cfg.YES),
|
|
265
265
|
)
|
|
266
266
|
== cfg.YES
|
|
267
267
|
):
|
|
@@ -280,7 +280,7 @@ def read(self):
|
|
|
280
280
|
"You can change this option in the"
|
|
281
281
|
" Preferences (File > Preferences)"
|
|
282
282
|
),
|
|
283
|
-
|
|
283
|
+
(cfg.NO, cfg.YES),
|
|
284
284
|
)
|
|
285
285
|
== cfg.YES
|
|
286
286
|
)
|
|
@@ -289,18 +289,17 @@ def read(self):
|
|
|
289
289
|
|
|
290
290
|
# recent projects
|
|
291
291
|
logging.debug("read recent projects")
|
|
292
|
+
self.recent_projects: list = []
|
|
292
293
|
recent_projects_file_path = pl.Path.home() / ".boris_recent_projects"
|
|
293
294
|
if recent_projects_file_path.is_file():
|
|
294
|
-
settings = QSettings(str(recent_projects_file_path), QSettings.IniFormat)
|
|
295
|
+
settings = QSettings(str(recent_projects_file_path), QSettings.Format.IniFormat)
|
|
295
296
|
try:
|
|
296
297
|
self.recent_projects = settings.value("recent_projects").split("|||")
|
|
297
298
|
while "" in self.recent_projects:
|
|
298
299
|
self.recent_projects.remove("")
|
|
299
300
|
self.set_recent_projects_menu()
|
|
300
301
|
except Exception:
|
|
301
|
-
|
|
302
|
-
else:
|
|
303
|
-
self.recent_projects = []
|
|
302
|
+
pass
|
|
304
303
|
|
|
305
304
|
|
|
306
305
|
def save(self, lastCheckForNewVersion=0):
|
|
@@ -312,7 +311,7 @@ def save(self, lastCheckForNewVersion=0):
|
|
|
312
311
|
|
|
313
312
|
logging.debug(f"save config file: {file_path}")
|
|
314
313
|
|
|
315
|
-
settings = QSettings(str(file_path), QSettings.IniFormat)
|
|
314
|
+
settings = QSettings(str(file_path), QSettings.Format.IniFormat)
|
|
316
315
|
|
|
317
316
|
settings.setValue("config", self.config_param)
|
|
318
317
|
|
|
@@ -352,5 +351,5 @@ def save(self, lastCheckForNewVersion=0):
|
|
|
352
351
|
# recent projects
|
|
353
352
|
logging.debug("Save recent projects")
|
|
354
353
|
|
|
355
|
-
settings = QSettings(str(pl.Path.home() / ".boris_recent_projects"), QSettings.IniFormat)
|
|
354
|
+
settings = QSettings(str(pl.Path.home() / ".boris_recent_projects"), QSettings.Format.IniFormat)
|
|
356
355
|
settings.setValue("recent_projects", "|||".join(self.recent_projects))
|