boris-behav-obs 9.7.7__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 +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/boris_cli.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS CLI
|
|
3
|
+
|
|
4
|
+
Behavioral Observation Research Interactive Software Command Line Interface
|
|
5
|
+
|
|
6
|
+
Copyright 2012-2025 Olivier Friard
|
|
7
|
+
|
|
8
|
+
This program is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not, write to the Free Software
|
|
20
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
21
|
+
MA 02110-1301, USA.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
import re
|
|
27
|
+
import pathlib
|
|
28
|
+
import utilities
|
|
29
|
+
import project_functions
|
|
30
|
+
from config import *
|
|
31
|
+
import db_functions
|
|
32
|
+
import export_observation
|
|
33
|
+
import irr
|
|
34
|
+
import plot_events
|
|
35
|
+
import version
|
|
36
|
+
|
|
37
|
+
__version__ = version.__version__
|
|
38
|
+
__version_date__ = version.__version_date__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cleanhtml(raw_html):
|
|
42
|
+
raw_html = raw_html.replace("<br>", "\n")
|
|
43
|
+
cleanr = re.compile("<.*?>")
|
|
44
|
+
cleantext = re.sub(cleanr, "", raw_html)
|
|
45
|
+
return cleantext
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def all_observations(pj):
|
|
49
|
+
return [idx for idx in sorted(pj[OBSERVATIONS])]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
commands_list = ["check_state_events", "export_events", "irr", "subtitles", "check_project_integrity", "plot_events"]
|
|
53
|
+
commands_usage = {
|
|
54
|
+
"check_state_events": (
|
|
55
|
+
"usage:\nboris_cli -p PROJECT_FILE -o OBSERVATION_ID --command check_state_events\n"
|
|
56
|
+
"where\n"
|
|
57
|
+
"PROJECT_FILE is the path of the BORIS project\n"
|
|
58
|
+
"OBSERVATION_ID is the id of observation(s) (if ommitted all observations are checked)"
|
|
59
|
+
),
|
|
60
|
+
"export_events": (
|
|
61
|
+
"usage:\nboris_cli -p PROJECT_FILE -o OBSERVATION_ID --command export_events [OUTPUT_FORMAT]\n"
|
|
62
|
+
"where:\n"
|
|
63
|
+
"PROJECT_FILE is the path of the BORIS project\n"
|
|
64
|
+
"OBSERVATION_ID is the id of observation(s) (if ommitted all observations are exported)\n"
|
|
65
|
+
"OUTPUT_FORMAT can be tsv (default), csv, xls, xlsx, ods, html"
|
|
66
|
+
),
|
|
67
|
+
"irr": (
|
|
68
|
+
'usage:\nboris_cli -p PROJECT_FILE -o "OBSERVATION_ID1" "OBSERVATION_ID2" --command irr [INTERVAL] [INCLUDE_MODIFIERS]\n'
|
|
69
|
+
"where:\n"
|
|
70
|
+
"PROJECT_FILE is the path of the BORIS project\n"
|
|
71
|
+
"INTERVAL in seconds (default is 1)\n"
|
|
72
|
+
"INCLUDE_MODIFIERS must be true or false (default is true)"
|
|
73
|
+
),
|
|
74
|
+
"subtitles": (
|
|
75
|
+
'usage:\nboris_cli -p PROJECT_FILE -o "OBSERVATION_ID" --command subtitles [OUTPUT_DIRECTORY]\n'
|
|
76
|
+
"where:\n"
|
|
77
|
+
"OUTPUT_DIRECTORY is the directory where subtitles files will be saved"
|
|
78
|
+
),
|
|
79
|
+
"check_project_integrity": "usage:\nboris_cli -p PROJECT_FILE --command check_project_integrity",
|
|
80
|
+
"plot_events": (
|
|
81
|
+
"usage:\nboris_cli - p PROJECT_FILE -o OBSERVATION_ID --command plot_events "
|
|
82
|
+
"[OUTPUT_DIRECTORY] [INCLUDE_MODIFIERS] [EXCLUDE_BEHAVIORS] [PLOT_FORMAT]\n"
|
|
83
|
+
"where\n"
|
|
84
|
+
"OUTPUT_DIRECTORY is the directory where the plots will be saved\n"
|
|
85
|
+
"INCLUDE_MODIFIERS must be true or false (default is true)\n"
|
|
86
|
+
"EXCLUDE_BEHAVIORS: True: behaviors without events are not plotted (default is true)\n"
|
|
87
|
+
"PLOT_FORMAT can be png, svg, pdf, ps"
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
parser = argparse.ArgumentParser(description="BORIS CLI")
|
|
92
|
+
parser.add_argument("-v", "--version", action="store_true", dest="version", help="BORIS version")
|
|
93
|
+
parser.add_argument("-p", "--project", action="store", dest="project_file", help="Project file path")
|
|
94
|
+
parser.add_argument("-o", "--observation", nargs="*", action="store", default=[], dest="observation_id", help="Observation id")
|
|
95
|
+
parser.add_argument("-i", "--info", action="store_true", dest="project_info", help="Project information")
|
|
96
|
+
parser.add_argument("-c", "--command", nargs="*", action="store", dest="command", help="Command to execute")
|
|
97
|
+
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
|
|
100
|
+
pj, observations_id_list = {}, {}
|
|
101
|
+
|
|
102
|
+
if args.version:
|
|
103
|
+
print("version {}".format(__version__))
|
|
104
|
+
sys.exit()
|
|
105
|
+
|
|
106
|
+
if args.command:
|
|
107
|
+
if args.command[0].upper() == "LIST":
|
|
108
|
+
for command in commands_list:
|
|
109
|
+
print(command)
|
|
110
|
+
print("=" * len(command))
|
|
111
|
+
if command in commands_usage:
|
|
112
|
+
print(commands_usage[command])
|
|
113
|
+
print()
|
|
114
|
+
print()
|
|
115
|
+
sys.exit()
|
|
116
|
+
|
|
117
|
+
if args.project_file:
|
|
118
|
+
if not args.command:
|
|
119
|
+
print("Project path: {}".format(args.project_file))
|
|
120
|
+
|
|
121
|
+
project_path, project_changed, pj, msg = project_functions.open_project_json(args.project_file)
|
|
122
|
+
if "error" in pj:
|
|
123
|
+
print(pj["error"])
|
|
124
|
+
sys.exit()
|
|
125
|
+
if msg:
|
|
126
|
+
print(msg)
|
|
127
|
+
|
|
128
|
+
if args.observation_id:
|
|
129
|
+
observations_id_list = args.observation_id
|
|
130
|
+
"""
|
|
131
|
+
if not args.command:
|
|
132
|
+
print("\nObservations:")
|
|
133
|
+
for observation_id in observations_id_list:
|
|
134
|
+
if observation_id in pj[OBSERVATIONS]:
|
|
135
|
+
print("Id: {}".format(observation_id))
|
|
136
|
+
else:
|
|
137
|
+
print("{}: NOT FOUND in project".format(observation_id))
|
|
138
|
+
print()
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
if args.project_info:
|
|
142
|
+
if not args.command:
|
|
143
|
+
if pj:
|
|
144
|
+
print("Project name: {}".format(pj[PROJECT_NAME]))
|
|
145
|
+
print("Project date: {}".format(pj[PROJECT_DATE]))
|
|
146
|
+
print("Project description: {}".format(pj[PROJECT_DESCRIPTION]))
|
|
147
|
+
print()
|
|
148
|
+
|
|
149
|
+
if not observations_id_list:
|
|
150
|
+
print("Ethogram\n========")
|
|
151
|
+
print("Number of behaviors in ethogram: {}".format(len(pj[ETHOGRAM])))
|
|
152
|
+
for idx in utilities.sorted_keys(pj[ETHOGRAM]):
|
|
153
|
+
print(
|
|
154
|
+
"Code: {}\tDescription: {}\tType: {}".format(
|
|
155
|
+
pj[ETHOGRAM][idx][BEHAVIOR_CODE], pj[ETHOGRAM][idx]["description"], pj[ETHOGRAM][idx][TYPE]
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
"""print("Behaviors: {}".format(",".join([pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])])))"""
|
|
159
|
+
print()
|
|
160
|
+
|
|
161
|
+
print("Subjects\n========")
|
|
162
|
+
print("Number of subjects: {}".format(len(pj[SUBJECTS])))
|
|
163
|
+
for idx in utilities.sorted_keys(pj[SUBJECTS]):
|
|
164
|
+
print("Name: {}\tDescription: {}".format(pj[SUBJECTS][idx]["name"], pj[SUBJECTS][idx]["description"]))
|
|
165
|
+
print()
|
|
166
|
+
|
|
167
|
+
print("Observations\n============")
|
|
168
|
+
print("Number of observations: {}".format(len(pj[OBSERVATIONS])))
|
|
169
|
+
print("List of observations:")
|
|
170
|
+
for observation_id in sorted(pj[OBSERVATIONS].keys()):
|
|
171
|
+
print("Id: {}\tDate: {}".format(observation_id, pj[OBSERVATIONS][observation_id]["date"]))
|
|
172
|
+
|
|
173
|
+
else:
|
|
174
|
+
for observation_id in observations_id_list:
|
|
175
|
+
print("Observation id: {}".format(observation_id))
|
|
176
|
+
if pj[OBSERVATIONS][observation_id][EVENTS]:
|
|
177
|
+
for event in pj[OBSERVATIONS][observation_id][EVENTS]:
|
|
178
|
+
print("\t".join([str(x) for x in event]))
|
|
179
|
+
else:
|
|
180
|
+
print("No events recorded")
|
|
181
|
+
print()
|
|
182
|
+
else:
|
|
183
|
+
print("No project")
|
|
184
|
+
sys.exit()
|
|
185
|
+
|
|
186
|
+
if args.command:
|
|
187
|
+
print("Command: {}\n".format(" ".join(args.command)))
|
|
188
|
+
|
|
189
|
+
if not pj:
|
|
190
|
+
print("No project")
|
|
191
|
+
sys.exit()
|
|
192
|
+
|
|
193
|
+
if "check_state_events" in args.command:
|
|
194
|
+
if not observations_id_list:
|
|
195
|
+
print("No observation selected. Command applied on all observations found in project\n")
|
|
196
|
+
observations_id_list = all_observations(pj)
|
|
197
|
+
|
|
198
|
+
for observation_id in observations_id_list:
|
|
199
|
+
ret, msg = project_functions.check_state_events_obs(observation_id, pj[ETHOGRAM], pj[OBSERVATIONS][observation_id], HHMMSS)
|
|
200
|
+
print("{}: {}".format(observation_id, cleanhtml(msg)))
|
|
201
|
+
sys.exit()
|
|
202
|
+
|
|
203
|
+
if "export_events" in args.command:
|
|
204
|
+
if not observations_id_list:
|
|
205
|
+
print("No observation selected. Command applied on all observations found in project\n")
|
|
206
|
+
observations_id_list = [idx for idx in pj[OBSERVATIONS]]
|
|
207
|
+
|
|
208
|
+
behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
|
|
209
|
+
subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
|
|
210
|
+
|
|
211
|
+
output_format = "tsv"
|
|
212
|
+
if len(args.command) > 1:
|
|
213
|
+
output_format = args.command[1]
|
|
214
|
+
|
|
215
|
+
for observation_id in observations_id_list:
|
|
216
|
+
ok, msg = export_observation.export_events(
|
|
217
|
+
{"selected subjects": subjects, "selected behaviors": behaviors},
|
|
218
|
+
observation_id,
|
|
219
|
+
pj[OBSERVATIONS][observation_id],
|
|
220
|
+
pj[ETHOGRAM],
|
|
221
|
+
utilities.safeFileName(observation_id + "." + output_format),
|
|
222
|
+
output_format,
|
|
223
|
+
)
|
|
224
|
+
if not ok:
|
|
225
|
+
print(msg)
|
|
226
|
+
|
|
227
|
+
sys.exit()
|
|
228
|
+
|
|
229
|
+
if "irr" in args.command:
|
|
230
|
+
if len(observations_id_list) != 2:
|
|
231
|
+
print("select 2 observations")
|
|
232
|
+
sys.exit()
|
|
233
|
+
|
|
234
|
+
behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
|
|
235
|
+
subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
|
|
236
|
+
|
|
237
|
+
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(pj, subjects, observations_id_list, behaviors)
|
|
238
|
+
|
|
239
|
+
if not ok:
|
|
240
|
+
print(cleanhtml(msg))
|
|
241
|
+
sys.exit()
|
|
242
|
+
|
|
243
|
+
cursor = db_connector.cursor()
|
|
244
|
+
|
|
245
|
+
interval = 1
|
|
246
|
+
if len(args.command) > 1:
|
|
247
|
+
interval = utilities.float2decimal(args.command[1])
|
|
248
|
+
|
|
249
|
+
include_modifiers = True
|
|
250
|
+
if len(args.command) > 2:
|
|
251
|
+
include_modifiers = "TRUE" in args.command[2].upper()
|
|
252
|
+
|
|
253
|
+
K, out = irr.cohen_kappa(cursor, observations_id_list[0], observations_id_list[1], interval, subjects, include_modifiers)
|
|
254
|
+
|
|
255
|
+
print(("Cohen's Kappa - Index of Inter-Rater Reliability\n\nInterval time: {interval:.3f} s\n").format(interval=interval))
|
|
256
|
+
|
|
257
|
+
print(out)
|
|
258
|
+
sys.exit()
|
|
259
|
+
|
|
260
|
+
if "subtitles" in args.command:
|
|
261
|
+
if not observations_id_list:
|
|
262
|
+
print("No observation selected. Command applied on all observations found in project\n")
|
|
263
|
+
observations_id_list = all_observations(pj)
|
|
264
|
+
|
|
265
|
+
behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
|
|
266
|
+
subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
|
|
267
|
+
|
|
268
|
+
export_dir = "."
|
|
269
|
+
if len(args.command) > 1:
|
|
270
|
+
export_dir = args.command[1]
|
|
271
|
+
if not pathlib.Path(export_dir).is_dir():
|
|
272
|
+
print("{} is not a valid directory".format(export_dir))
|
|
273
|
+
sys.exit()
|
|
274
|
+
|
|
275
|
+
ok, msg = project_functions.create_subtitles(
|
|
276
|
+
pj,
|
|
277
|
+
observations_id_list,
|
|
278
|
+
{"selected subjects": subjects, "selected behaviors": behaviors, "include modifiers": True},
|
|
279
|
+
export_dir,
|
|
280
|
+
)
|
|
281
|
+
if not ok:
|
|
282
|
+
print(cleanhtml(msg))
|
|
283
|
+
sys.exit()
|
|
284
|
+
|
|
285
|
+
if "check_project_integrity" in args.command[0]:
|
|
286
|
+
msg = project_functions.check_project_integrity(pj, HHMMSS, args.project_file)
|
|
287
|
+
if msg:
|
|
288
|
+
print(cleanhtml(msg))
|
|
289
|
+
else:
|
|
290
|
+
print("No issuses found in project")
|
|
291
|
+
sys.exit()
|
|
292
|
+
|
|
293
|
+
if "plot_events" in args.command[0]:
|
|
294
|
+
if not observations_id_list:
|
|
295
|
+
print("No observation selected. Command applied on all observations found in project\n")
|
|
296
|
+
observations_id_list = all_observations(pj)
|
|
297
|
+
|
|
298
|
+
behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
|
|
299
|
+
subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
|
|
300
|
+
|
|
301
|
+
export_dir = "."
|
|
302
|
+
if len(args.command) > 1:
|
|
303
|
+
export_dir = args.command[1]
|
|
304
|
+
if not pathlib.Path(export_dir).is_dir():
|
|
305
|
+
print("{} is not a valid directory".format(export_dir))
|
|
306
|
+
sys.exit()
|
|
307
|
+
|
|
308
|
+
include_modifiers = True
|
|
309
|
+
if len(args.command) > 2:
|
|
310
|
+
include_modifiers = "TRUE" in args.command[2].upper()
|
|
311
|
+
|
|
312
|
+
exclude_behaviors = True
|
|
313
|
+
if len(args.command) > 3:
|
|
314
|
+
exclude_behaviors = False if "FALSE" in args.command[3].upper() else True
|
|
315
|
+
|
|
316
|
+
plot_format = "png"
|
|
317
|
+
if len(args.command) > 4:
|
|
318
|
+
plot_format = args.command[4].lower()
|
|
319
|
+
|
|
320
|
+
plot_events.create_events_plot(
|
|
321
|
+
pj,
|
|
322
|
+
observations_id_list,
|
|
323
|
+
{
|
|
324
|
+
"selected subjects": subjects,
|
|
325
|
+
"selected behaviors": behaviors,
|
|
326
|
+
"include modifiers": include_modifiers,
|
|
327
|
+
"exclude behaviors": exclude_behaviors,
|
|
328
|
+
"time": TIME_FULL_OBS,
|
|
329
|
+
"start time": 0,
|
|
330
|
+
"end time": 0,
|
|
331
|
+
},
|
|
332
|
+
plot_colors=BEHAVIORS_PLOT_COLORS,
|
|
333
|
+
plot_directory=export_dir,
|
|
334
|
+
file_format=plot_format,
|
|
335
|
+
)
|
|
336
|
+
sys.exit()
|
|
337
|
+
|
|
338
|
+
print("Command {} not found!".format(args.command[0]))
|
|
339
|
+
|
|
340
|
+
print()
|
boris/cmd_arguments.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This program is free software; you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program; if not, write to the Free Software
|
|
19
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
+
MA 02110-1301, USA.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from optparse import OptionParser
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_arguments():
|
|
28
|
+
# check if argument
|
|
29
|
+
usage = 'usage: %prog [options] [-p PROJECT_PATH] [-o "OBSERVATION ID"]'
|
|
30
|
+
parser = OptionParser(usage=usage)
|
|
31
|
+
|
|
32
|
+
parser.add_option("-d", "--debug", action="store_true", default=False, dest="debug", help="Use debugging mode")
|
|
33
|
+
parser.add_option("-q", "--quit", action="store_true", default=False, dest="quit", help="Quit after launch")
|
|
34
|
+
parser.add_option("-v", "--version", action="store_true", default=False, dest="version", help="Print version")
|
|
35
|
+
parser.add_option("-n", "--nosplashscreen", action="store_true", default=False, help="No splash screen")
|
|
36
|
+
parser.add_option("-p", "--project", action="store", default="", dest="project", help="Project file")
|
|
37
|
+
parser.add_option("-o", "--observation", action="store", default="", dest="observation", help="Observation id")
|
|
38
|
+
parser.add_option("-i", "--ipc", action="store_true", default="", dest="ipc", help="MPV IPC mode")
|
|
39
|
+
|
|
40
|
+
parser.add_option(
|
|
41
|
+
"-f",
|
|
42
|
+
"--no-first-launch-dialog",
|
|
43
|
+
action="store_true",
|
|
44
|
+
default=False,
|
|
45
|
+
dest="no_first_launch_dialog",
|
|
46
|
+
help="No first launch dialog (for new version automatic check)",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return parser.parse_args()
|
boris/coding_pad.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from PySide6.QtCore import Qt, Signal, QEvent, QRect
|
|
23
|
+
from PySide6.QtGui import QFont
|
|
24
|
+
from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGridLayout, QComboBox, QMessageBox
|
|
25
|
+
|
|
26
|
+
from . import config as cfg
|
|
27
|
+
from . import utilities as util
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Button(QWidget):
|
|
31
|
+
def __init__(self, parent=None):
|
|
32
|
+
super(Button, self).__init__(parent)
|
|
33
|
+
self.pushButton = QPushButton()
|
|
34
|
+
self.pushButton.setFocusPolicy(Qt.NoFocus)
|
|
35
|
+
layout = QHBoxLayout()
|
|
36
|
+
layout.addWidget(self.pushButton)
|
|
37
|
+
self.setLayout(layout)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CodingPad(QWidget):
|
|
41
|
+
clickSignal = Signal(str)
|
|
42
|
+
sendEventSignal = Signal(QEvent)
|
|
43
|
+
close_signal = Signal(QRect, dict)
|
|
44
|
+
|
|
45
|
+
def __init__(self, pj: dict, filtered_behaviors, parent=None):
|
|
46
|
+
super().__init__(parent)
|
|
47
|
+
self.pj: dict = pj
|
|
48
|
+
self.filtered_behaviors = filtered_behaviors
|
|
49
|
+
|
|
50
|
+
self.behavioral_category_colors_list: list = []
|
|
51
|
+
self.behavior_colors_list: list = []
|
|
52
|
+
|
|
53
|
+
self.behavioral_category_colors: dict = {}
|
|
54
|
+
self.behavior_colors: dict = {}
|
|
55
|
+
|
|
56
|
+
self.preferences: dict = {"button font size": 20, "button color": cfg.BEHAVIOR_CATEGORY}
|
|
57
|
+
|
|
58
|
+
self.button_css: str = "min-width: 50px; min-height:50px; font-weight: bold; max-height:5000px; max-width: 5000px;"
|
|
59
|
+
|
|
60
|
+
self.setWindowTitle("Coding pad")
|
|
61
|
+
|
|
62
|
+
self.grid = QGridLayout(self)
|
|
63
|
+
|
|
64
|
+
self.installEventFilter(self)
|
|
65
|
+
|
|
66
|
+
def config(self):
|
|
67
|
+
"""
|
|
68
|
+
Configure the coding pad
|
|
69
|
+
"""
|
|
70
|
+
if self.cb_config.currentIndex() == 1: # increase text size
|
|
71
|
+
self.preferences["button font size"] += 4
|
|
72
|
+
if self.cb_config.currentIndex() == 2: # decrease text size
|
|
73
|
+
self.preferences["button font size"] -= 4
|
|
74
|
+
if self.cb_config.currentIndex() == 3:
|
|
75
|
+
self.preferences["button color"] = cfg.BEHAVIOR_CATEGORY
|
|
76
|
+
if self.cb_config.currentIndex() == 4:
|
|
77
|
+
self.preferences["button color"] = "behavior"
|
|
78
|
+
if self.cb_config.currentIndex() == 5:
|
|
79
|
+
self.preferences["button color"] = "no color"
|
|
80
|
+
|
|
81
|
+
self.cb_config.setCurrentIndex(0)
|
|
82
|
+
self.button_configuration()
|
|
83
|
+
|
|
84
|
+
def compose(self):
|
|
85
|
+
"""
|
|
86
|
+
Add buttons to coding pad
|
|
87
|
+
"""
|
|
88
|
+
for i in reversed(range(self.grid.count())):
|
|
89
|
+
if self.grid.itemAt(i).widget() is not None:
|
|
90
|
+
self.grid.itemAt(i).widget().setParent(None)
|
|
91
|
+
|
|
92
|
+
# combobox for coding pad configuration
|
|
93
|
+
vlayout = QHBoxLayout()
|
|
94
|
+
self.cb_config = QComboBox()
|
|
95
|
+
self.cb_config.setFocusPolicy(Qt.NoFocus)
|
|
96
|
+
self.cb_config.addItems(
|
|
97
|
+
[
|
|
98
|
+
"Choose an option to configure",
|
|
99
|
+
"Increase button text size",
|
|
100
|
+
"Decrease button text size",
|
|
101
|
+
"Color button by behavioral category",
|
|
102
|
+
"Color button by behavior",
|
|
103
|
+
"No color",
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
self.cb_config.currentIndexChanged.connect(self.config)
|
|
107
|
+
vlayout.addWidget(self.cb_config)
|
|
108
|
+
self.grid.addLayout(vlayout, 0, 1, 1, 1)
|
|
109
|
+
|
|
110
|
+
self.all_behaviors = [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]
|
|
111
|
+
|
|
112
|
+
# behavioral category colors
|
|
113
|
+
self.unique_behavioral_categories = sorted(
|
|
114
|
+
set([self.pj[cfg.ETHOGRAM][x].get(cfg.BEHAVIOR_CATEGORY, "") for x in self.pj[cfg.ETHOGRAM]])
|
|
115
|
+
)
|
|
116
|
+
for idx, category in enumerate(self.unique_behavioral_categories):
|
|
117
|
+
self.behavioral_category_colors[category] = self.behavioral_category_colors_list[
|
|
118
|
+
idx % len(self.behavioral_category_colors_list)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# sorted list of unique behavior categories
|
|
122
|
+
behaviorsList = [
|
|
123
|
+
[self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY], self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE]]
|
|
124
|
+
for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])
|
|
125
|
+
if cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][x] and self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] in self.filtered_behaviors
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# square grid dimension
|
|
129
|
+
dim = int(len(behaviorsList) ** 0.5 + 0.999)
|
|
130
|
+
|
|
131
|
+
c = 0
|
|
132
|
+
for i in range(1, dim + 1):
|
|
133
|
+
for j in range(1, dim + 1):
|
|
134
|
+
if c >= len(behaviorsList):
|
|
135
|
+
break
|
|
136
|
+
self.addWidget(behaviorsList[c][1], i, j)
|
|
137
|
+
c += 1
|
|
138
|
+
|
|
139
|
+
self.button_configuration()
|
|
140
|
+
|
|
141
|
+
def addWidget(self, behavior_code: str, i: int, j: int) -> None:
|
|
142
|
+
self.grid.addWidget(Button(), i, j)
|
|
143
|
+
index = self.grid.count() - 1
|
|
144
|
+
widget = self.grid.itemAt(index).widget()
|
|
145
|
+
|
|
146
|
+
if widget is not None:
|
|
147
|
+
widget.pushButton.setText(behavior_code)
|
|
148
|
+
widget.pushButton.clicked.connect(lambda: self.click(behavior_code))
|
|
149
|
+
|
|
150
|
+
def button_configuration(self):
|
|
151
|
+
"""
|
|
152
|
+
configure the font and color of buttons
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
state_behaviors_list = util.state_behavior_codes(self.pj[cfg.ETHOGRAM])
|
|
156
|
+
|
|
157
|
+
for index in range(self.grid.count()):
|
|
158
|
+
if self.grid.itemAt(index).widget() is None:
|
|
159
|
+
continue
|
|
160
|
+
behavior_code = self.grid.itemAt(index).widget().pushButton.text()
|
|
161
|
+
|
|
162
|
+
if self.preferences["button color"] == cfg.BEHAVIOR_CATEGORY:
|
|
163
|
+
behav_cat = [
|
|
164
|
+
self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]
|
|
165
|
+
for x in self.pj[cfg.ETHOGRAM]
|
|
166
|
+
if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == behavior_code
|
|
167
|
+
][0]
|
|
168
|
+
if cfg.BEHAVIORAL_CATEGORIES_CONF in self.pj:
|
|
169
|
+
behav_cat_color = util.behav_category_user_color(self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF], behav_cat)
|
|
170
|
+
else:
|
|
171
|
+
behav_cat_color = None
|
|
172
|
+
if behav_cat_color is None:
|
|
173
|
+
color = self.behavioral_category_colors[behav_cat]
|
|
174
|
+
else:
|
|
175
|
+
color = behav_cat_color
|
|
176
|
+
|
|
177
|
+
if self.preferences["button color"] == "behavior":
|
|
178
|
+
# behavioral categories are not defined
|
|
179
|
+
behavior_position = int(
|
|
180
|
+
[x for x in util.sorted_keys(self.pj[cfg.ETHOGRAM]) if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == behavior_code][0]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# behavior button color
|
|
184
|
+
behav_color = util.behavior_user_color(self.pj[cfg.ETHOGRAM], behavior_code)
|
|
185
|
+
|
|
186
|
+
if behav_color is not None:
|
|
187
|
+
color = behav_color
|
|
188
|
+
else:
|
|
189
|
+
color = self.behavior_colors_list[behavior_position % len(self.behavior_colors_list)].replace("tab:", "")
|
|
190
|
+
|
|
191
|
+
if self.preferences["button color"] == "no color":
|
|
192
|
+
color = ""
|
|
193
|
+
|
|
194
|
+
# set checkable if state behavior
|
|
195
|
+
self.grid.itemAt(index).widget().pushButton.setCheckable(behavior_code in state_behaviors_list)
|
|
196
|
+
self.grid.itemAt(index).widget().pushButton.setStyleSheet(self.button_css + (f"background-color: {color};" if color else ""))
|
|
197
|
+
font = QFont("Arial", self.preferences["button font size"])
|
|
198
|
+
self.grid.itemAt(index).widget().pushButton.setFont(font)
|
|
199
|
+
|
|
200
|
+
def resizeEvent(self, event):
|
|
201
|
+
"""
|
|
202
|
+
Resize event
|
|
203
|
+
button are redesigned with new font size
|
|
204
|
+
"""
|
|
205
|
+
self.button_configuration()
|
|
206
|
+
|
|
207
|
+
def click(self, behavior_code: str) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Button clicked
|
|
210
|
+
"""
|
|
211
|
+
self.clickSignal.emit(behavior_code)
|
|
212
|
+
|
|
213
|
+
def eventFilter(self, receiver, event) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
send event (if keypress) to main window
|
|
216
|
+
"""
|
|
217
|
+
if event.type() == QEvent.KeyPress:
|
|
218
|
+
self.sendEventSignal.emit(event)
|
|
219
|
+
return True
|
|
220
|
+
else:
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def closeEvent(self, event) -> None:
|
|
224
|
+
"""
|
|
225
|
+
send event for widget geometry memory
|
|
226
|
+
"""
|
|
227
|
+
self.close_signal.emit(self.geometry(), self.preferences)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def show_coding_pad(self):
|
|
231
|
+
"""
|
|
232
|
+
show coding pad window
|
|
233
|
+
"""
|
|
234
|
+
if self.playerType in cfg.VIEWERS:
|
|
235
|
+
QMessageBox.warning(self, cfg.programName, "The coding pad is not available in <b>VIEW</b> mode")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if hasattr(self, "codingpad"):
|
|
239
|
+
self.codingpad.filtered_behaviors = [self.twEthogram.item(i, 1).text() for i in range(self.twEthogram.rowCount())]
|
|
240
|
+
if not self.codingpad.filtered_behaviors:
|
|
241
|
+
QMessageBox.warning(self, cfg.programName, "No behaviors to show!")
|
|
242
|
+
return
|
|
243
|
+
self.codingpad.show()
|
|
244
|
+
|
|
245
|
+
# update colors
|
|
246
|
+
self.codingpad.behavior_colors_list = self.plot_colors
|
|
247
|
+
self.codingpad.behavioral_category_colors_list = self.behav_category_colors
|
|
248
|
+
|
|
249
|
+
else: # coding pad does not exist
|
|
250
|
+
filtered_behaviors = [self.twEthogram.item(i, 1).text() for i in range(self.twEthogram.rowCount())]
|
|
251
|
+
if not filtered_behaviors:
|
|
252
|
+
QMessageBox.warning(self, cfg.programName, "No behaviors to show!")
|
|
253
|
+
return
|
|
254
|
+
self.codingpad = CodingPad(self.pj, filtered_behaviors)
|
|
255
|
+
|
|
256
|
+
self.codingpad.behavior_colors_list = self.plot_colors
|
|
257
|
+
self.codingpad.behavioral_category_colors_list = self.behav_category_colors
|
|
258
|
+
|
|
259
|
+
self.codingpad.compose()
|
|
260
|
+
|
|
261
|
+
self.codingpad.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
262
|
+
self.codingpad.sendEventSignal.connect(self.signal_from_widget)
|
|
263
|
+
|
|
264
|
+
self.codingpad.clickSignal.connect(self.click_signal_from_coding_pad)
|
|
265
|
+
self.codingpad.close_signal.connect(self.close_signal_from_coding_pad)
|
|
266
|
+
self.codingpad.show()
|
|
267
|
+
|
|
268
|
+
if self.config_param.get(cfg.CODING_PAD_GEOMETRY, None):
|
|
269
|
+
self.codingpad.setGeometry(
|
|
270
|
+
self.config_param[cfg.CODING_PAD_GEOMETRY].x(),
|
|
271
|
+
self.config_param[cfg.CODING_PAD_GEOMETRY].y(),
|
|
272
|
+
self.config_param[cfg.CODING_PAD_GEOMETRY].width(),
|
|
273
|
+
self.config_param[cfg.CODING_PAD_GEOMETRY].height(),
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
self.codingpad.setGeometry(100, 100, 660, 500)
|
|
277
|
+
|
|
278
|
+
if self.config_param.get(cfg.CODING_PAD_CONFIG, {}):
|
|
279
|
+
self.codingpad.preferences = self.config_param[cfg.CODING_PAD_CONFIG]
|
|
280
|
+
self.codingpad.button_configuration()
|