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/otx_parser.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS 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 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS 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 see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Parse an OTX/ODX or OTB (compressed OTX) file and convert the ethogram, modifiers, subjects
|
|
23
|
+
and independent variables to BORIS format
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import datetime as dt
|
|
28
|
+
from decimal import Decimal as dec
|
|
29
|
+
import re
|
|
30
|
+
import zipfile
|
|
31
|
+
import pathlib as pl
|
|
32
|
+
from xml.dom import minidom
|
|
33
|
+
import logging
|
|
34
|
+
from typing import Tuple
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from . import config as cfg
|
|
38
|
+
except Exception:
|
|
39
|
+
import config as cfg
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def otx_to_boris(file_path: str) -> Tuple[dict, list]:
|
|
43
|
+
"""
|
|
44
|
+
convert otx/otb/odx file in a BORIS project
|
|
45
|
+
|
|
46
|
+
For ODX files ask to import observations
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
file_path (str): path to otx/otb/odx file
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
dict: BORIS project
|
|
53
|
+
list: list of errors during importation
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
if pl.Path(file_path).suffix == ".otb":
|
|
57
|
+
with zipfile.ZipFile(file_path) as file_zip:
|
|
58
|
+
files_list = file_zip.namelist()
|
|
59
|
+
if files_list:
|
|
60
|
+
try:
|
|
61
|
+
file_zip.extract(files_list[0])
|
|
62
|
+
except Exception:
|
|
63
|
+
return {"fatal": True}, ["Error when extracting file from OTB"]
|
|
64
|
+
else:
|
|
65
|
+
return {"fatal": True}, ["Error when extracting file"]
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
xmldoc = minidom.parse(files_list[0])
|
|
69
|
+
except Exception:
|
|
70
|
+
return {"fatal": True}, ["XML parsing error"]
|
|
71
|
+
|
|
72
|
+
elif pl.Path(file_path).suffix in (".odx", ".otx"):
|
|
73
|
+
try:
|
|
74
|
+
xmldoc = minidom.parse(file_path)
|
|
75
|
+
except Exception:
|
|
76
|
+
return {"fatal": True}, ["XML parsing error"]
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
return {"fatal": True}, ["The file must be in OTB, OTX or ODX format"]
|
|
80
|
+
|
|
81
|
+
flag_long_key: bool = False
|
|
82
|
+
error_list: list = []
|
|
83
|
+
|
|
84
|
+
# metadata
|
|
85
|
+
for item in xmldoc.getElementsByTagName("MET_METADATA"):
|
|
86
|
+
metadata = minidom.parseString(item.toxml())
|
|
87
|
+
try:
|
|
88
|
+
project_name = re.sub("<[^>]*>", "", metadata.getElementsByTagName("MET_PROJECT_NAME")[0].toxml())
|
|
89
|
+
except Exception:
|
|
90
|
+
project_name = ""
|
|
91
|
+
try:
|
|
92
|
+
project_description = re.sub("<[^>]*>", "", metadata.getElementsByTagName("MET_PROJECT_DESCRIPTION")[0].toxml())
|
|
93
|
+
except Exception:
|
|
94
|
+
project_description = ""
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
project_creation_date = re.sub("<[^>]*>", "", metadata.getElementsByTagName("MET_CREATION_DATETIME")[0].toxml())
|
|
98
|
+
except Exception:
|
|
99
|
+
project_creation_date = ""
|
|
100
|
+
|
|
101
|
+
# modifiers
|
|
102
|
+
modifiers: dict = {}
|
|
103
|
+
# modifiers_set = {}
|
|
104
|
+
itemlist = xmldoc.getElementsByTagName("CDS_MODIFIER")
|
|
105
|
+
for item in itemlist:
|
|
106
|
+
modif = minidom.parseString(item.toxml())
|
|
107
|
+
|
|
108
|
+
modif_code = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_NAME")[0].toxml())
|
|
109
|
+
|
|
110
|
+
modif_id = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_ID")[0].toxml())
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
modif_parent_id = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_PARENT_ID")[0].toxml())
|
|
114
|
+
except Exception:
|
|
115
|
+
modif_parent_id = ""
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
description = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_DESCRIPTION")[0].toxml())
|
|
119
|
+
except Exception:
|
|
120
|
+
description = ""
|
|
121
|
+
try:
|
|
122
|
+
key = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_START_KEYCODE")[0].toxml())
|
|
123
|
+
except Exception:
|
|
124
|
+
key = ""
|
|
125
|
+
|
|
126
|
+
if modif_parent_id:
|
|
127
|
+
modifiers[modif_parent_id]["values"].append(modif_code)
|
|
128
|
+
else:
|
|
129
|
+
if len(key) > 1:
|
|
130
|
+
key = ""
|
|
131
|
+
flag_long_key = True
|
|
132
|
+
modifiers[modif_id] = {"set_name": modif_code, "key": key, "description": description, "values": []}
|
|
133
|
+
|
|
134
|
+
logging.debug(modifiers)
|
|
135
|
+
|
|
136
|
+
# connect modifiers to behaviors
|
|
137
|
+
connections: dict = {}
|
|
138
|
+
itemlist = xmldoc.getElementsByTagName("CDS_CONNECTION")
|
|
139
|
+
for item in itemlist:
|
|
140
|
+
if item.attributes["CDS_ELEMENT_ID"].value not in connections:
|
|
141
|
+
connections[item.attributes["CDS_ELEMENT_ID"].value] = []
|
|
142
|
+
connections[item.attributes["CDS_ELEMENT_ID"].value].append(item.attributes["CDS_MODIFIER_ID"].value)
|
|
143
|
+
|
|
144
|
+
logging.debug(connections)
|
|
145
|
+
|
|
146
|
+
# behaviors
|
|
147
|
+
behaviors: dict = {}
|
|
148
|
+
behaviors_list: list = []
|
|
149
|
+
behav_category: list = []
|
|
150
|
+
mutually_exclusive_list: list = []
|
|
151
|
+
itemlist = xmldoc.getElementsByTagName("CDS_BEHAVIOR")
|
|
152
|
+
for item in itemlist:
|
|
153
|
+
behav = minidom.parseString(item.toxml())
|
|
154
|
+
|
|
155
|
+
behav_code = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_NAME")[0].toxml())
|
|
156
|
+
|
|
157
|
+
behav_id = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_ID")[0].toxml())
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
description = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_DESCRIPTION")[0].toxml())
|
|
161
|
+
except Exception:
|
|
162
|
+
description = ""
|
|
163
|
+
try:
|
|
164
|
+
key = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_START_KEYCODE")[0].toxml())
|
|
165
|
+
except Exception:
|
|
166
|
+
key = ""
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
stop_key = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_STOP_KEYCODE")[0].toxml())
|
|
170
|
+
except Exception:
|
|
171
|
+
stop_key = ""
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
parent_name = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_PARENT_NAME")[0].toxml())
|
|
175
|
+
except Exception:
|
|
176
|
+
parent_name = ""
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
mutually_exclusive = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_MUT_EXCLUSIVE")[0].toxml())
|
|
180
|
+
except Exception:
|
|
181
|
+
mutually_exclusive = ""
|
|
182
|
+
|
|
183
|
+
if mutually_exclusive == "Y" and parent_name:
|
|
184
|
+
mutually_exclusive_list.append(behav_code)
|
|
185
|
+
|
|
186
|
+
if behav_id in connections:
|
|
187
|
+
modifier_sets = [modifiers[modifier_set]["set_name"] for modifier_set in connections[behav_id]]
|
|
188
|
+
else:
|
|
189
|
+
modifier_sets = []
|
|
190
|
+
|
|
191
|
+
if parent_name: # behavior
|
|
192
|
+
if (not key or len(key) > 1) and stop_key:
|
|
193
|
+
key = stop_key
|
|
194
|
+
|
|
195
|
+
if len(key) > 1:
|
|
196
|
+
key = ""
|
|
197
|
+
flag_long_key = True
|
|
198
|
+
|
|
199
|
+
behaviors[str(len(behaviors))] = {
|
|
200
|
+
"id": int(behav_id),
|
|
201
|
+
"code": behav_code,
|
|
202
|
+
"key": key,
|
|
203
|
+
"description": description,
|
|
204
|
+
"modifiers": modifier_sets,
|
|
205
|
+
"category": parent_name,
|
|
206
|
+
}
|
|
207
|
+
behaviors_list.append(behav_code)
|
|
208
|
+
|
|
209
|
+
else: # behavioral category
|
|
210
|
+
behav_category.append(behav_code)
|
|
211
|
+
|
|
212
|
+
behaviors_boris: dict = {}
|
|
213
|
+
for k in behaviors:
|
|
214
|
+
behaviors_boris[k] = {
|
|
215
|
+
"code": behaviors[k]["code"],
|
|
216
|
+
"type": cfg.POINT_EVENT,
|
|
217
|
+
"key": behaviors[k]["key"],
|
|
218
|
+
"description": behaviors[k]["description"],
|
|
219
|
+
"category": behaviors[k]["category"],
|
|
220
|
+
"excluded": "",
|
|
221
|
+
"coding map": "",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if behaviors[k]["code"] in mutually_exclusive_list:
|
|
225
|
+
behaviors_boris[k]["excluded"] = ",".join([x for x in behaviors_list if x != behaviors[k]["code"]])
|
|
226
|
+
|
|
227
|
+
behaviors_boris[k]["modifiers"] = {}
|
|
228
|
+
if behaviors[k]["modifiers"]:
|
|
229
|
+
for modif_key in modifiers:
|
|
230
|
+
if modifiers[modif_key]["set_name"] in behaviors[k]["modifiers"]:
|
|
231
|
+
new_index = str(len(behaviors_boris[k]["modifiers"]))
|
|
232
|
+
behaviors_boris[k]["modifiers"][new_index] = {
|
|
233
|
+
"name": modifiers[modif_key]["set_name"],
|
|
234
|
+
"type": cfg.SINGLE_SELECTION,
|
|
235
|
+
"values": modifiers[modif_key]["values"],
|
|
236
|
+
"description": modifiers[modif_key]["description"],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
logging.debug(behaviors_boris)
|
|
240
|
+
|
|
241
|
+
# subjects
|
|
242
|
+
subjects = {}
|
|
243
|
+
itemlist = xmldoc.getElementsByTagName("CDS_SUBJECT")
|
|
244
|
+
for item in itemlist:
|
|
245
|
+
subject = minidom.parseString(item.toxml())
|
|
246
|
+
subject_name = re.sub("<[^>]*>", "", subject.getElementsByTagName("CDS_ELE_NAME")[0].toxml())
|
|
247
|
+
try:
|
|
248
|
+
key = re.sub("<[^>]*>", "", subject.getElementsByTagName("CDS_ELE_START_KEYCODE")[0].toxml())
|
|
249
|
+
except Exception:
|
|
250
|
+
key = ""
|
|
251
|
+
try:
|
|
252
|
+
parent_name = re.sub("<[^>]*>", "", subject.getElementsByTagName("CDS_ELE_PARENT_NAME")[0].toxml())
|
|
253
|
+
except Exception:
|
|
254
|
+
parent_name = ""
|
|
255
|
+
|
|
256
|
+
if parent_name:
|
|
257
|
+
if len(key) > 1:
|
|
258
|
+
key = ""
|
|
259
|
+
flag_long_key = True
|
|
260
|
+
subjects[str(len(subjects))] = {"key": key, "name": subject_name, "description": ""}
|
|
261
|
+
|
|
262
|
+
# independent variables
|
|
263
|
+
variables = {}
|
|
264
|
+
itemlist = xmldoc.getElementsByTagName("VL_VARIABLE")
|
|
265
|
+
for item in itemlist:
|
|
266
|
+
variable = minidom.parseString(item.toxml())
|
|
267
|
+
|
|
268
|
+
variable_label = re.sub("<[^>]*>", "", variable.getElementsByTagName("VL_LABEL")[0].toxml())
|
|
269
|
+
|
|
270
|
+
variable_id = re.sub("<[^>]*>", "", variable.getElementsByTagName("VL_ID")[0].toxml())
|
|
271
|
+
|
|
272
|
+
variable_type = re.sub("<[^>]*>", "", variable.getElementsByTagName("VL_TYPE")[0].toxml())
|
|
273
|
+
if variable_type.upper() == "TEXT":
|
|
274
|
+
variable_type = cfg.TEXT
|
|
275
|
+
if variable_type.upper() == "DOUBLE":
|
|
276
|
+
variable_type = cfg.NUMERIC
|
|
277
|
+
if variable_type.upper() == "FILEREFERENCE":
|
|
278
|
+
variable_type = cfg.TEXT
|
|
279
|
+
if variable_type.upper() == "DURATION":
|
|
280
|
+
variable_type = cfg.TEXT
|
|
281
|
+
if variable_type.upper() == "TIMESTAMP":
|
|
282
|
+
variable_type = cfg.TIMESTAMP
|
|
283
|
+
if variable_type.upper() == "BOOLEAN":
|
|
284
|
+
variable_type = cfg.TEXT
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
variable_description = re.sub("<[^>]*>", "", modif.getElementsByTagName("VL_DESCRIPTION")[0].toxml())
|
|
288
|
+
except Exception:
|
|
289
|
+
variable_description = ""
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
values = variable.getElementsByTagName("VL_VALUE")
|
|
293
|
+
values_list = []
|
|
294
|
+
for value in values:
|
|
295
|
+
values_list.append(re.sub("<[^>]*>", "", value.toxml()))
|
|
296
|
+
values_str = ",".join(values_list)
|
|
297
|
+
|
|
298
|
+
except Exception:
|
|
299
|
+
values_str = ""
|
|
300
|
+
|
|
301
|
+
variables[variable_id] = {
|
|
302
|
+
"label": variable_label,
|
|
303
|
+
"type": variable_type.lower(),
|
|
304
|
+
"description": variable_description,
|
|
305
|
+
}
|
|
306
|
+
if values_str:
|
|
307
|
+
variables[variable_id]["predefined_values"] = values_str
|
|
308
|
+
variables[variable_id]["type"] = "value from set"
|
|
309
|
+
|
|
310
|
+
variables_boris = {}
|
|
311
|
+
for k in variables:
|
|
312
|
+
variables_boris[k] = {
|
|
313
|
+
"label": variables[k]["label"],
|
|
314
|
+
"description": variables[k]["description"],
|
|
315
|
+
"type": variables[k]["type"],
|
|
316
|
+
"default value": "",
|
|
317
|
+
"possible values": variables[k]["predefined_values"] if "predefined_values" in variables[k] else "",
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# create empty project from template
|
|
321
|
+
project = dict(cfg.EMPTY_PROJECT)
|
|
322
|
+
project[cfg.OBSERVATIONS] = {}
|
|
323
|
+
|
|
324
|
+
observations = xmldoc.getElementsByTagName("OBS_OBSERVATION")
|
|
325
|
+
|
|
326
|
+
for OBS_OBSERVATION in observations:
|
|
327
|
+
# OBS_OBSERVATION = minidom.parseString(OBS_OBSERVATION.toxml())
|
|
328
|
+
|
|
329
|
+
obs_id = OBS_OBSERVATION.getAttribute("NAME")
|
|
330
|
+
|
|
331
|
+
project[cfg.OBSERVATIONS][obs_id] = dict(
|
|
332
|
+
{
|
|
333
|
+
"file": {},
|
|
334
|
+
"type": "LIVE",
|
|
335
|
+
"description": "",
|
|
336
|
+
"time offset": 0,
|
|
337
|
+
cfg.EVENTS: [],
|
|
338
|
+
"observation time interval": [0, 0],
|
|
339
|
+
"independent_variables": {},
|
|
340
|
+
"visualize_spectrogram": False,
|
|
341
|
+
"visualize_waveform": False,
|
|
342
|
+
"close_behaviors_between_videos": False,
|
|
343
|
+
"scan_sampling_time": 0,
|
|
344
|
+
"start_from_current_time": False,
|
|
345
|
+
"start_from_current_epoch_time": False,
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
OBS_EVENT_LOGS = OBS_OBSERVATION.getElementsByTagName("OBS_EVENT_LOGS")[0]
|
|
350
|
+
|
|
351
|
+
for OBS_EVENT_LOG in OBS_EVENT_LOGS.getElementsByTagName("OBS_EVENT_LOG"):
|
|
352
|
+
CREATION_DATETIME = OBS_EVENT_LOG.getAttribute("CREATION_DATETIME")
|
|
353
|
+
|
|
354
|
+
CREATION_DATETIME = CREATION_DATETIME.replace(" ", "T") # .split(".")[0]
|
|
355
|
+
|
|
356
|
+
logging.debug(f"{CREATION_DATETIME=}") # ex: 2022-05-18 10:04:09.474512"""
|
|
357
|
+
|
|
358
|
+
project[cfg.OBSERVATIONS][obs_id]["date"] = CREATION_DATETIME
|
|
359
|
+
|
|
360
|
+
for event in OBS_EVENT_LOG.getElementsByTagName("OBS_EVENT"):
|
|
361
|
+
OBS_EVENT_TIMESTAMP = event.getElementsByTagName("OBS_EVENT_TIMESTAMP")[0].childNodes[0].data
|
|
362
|
+
|
|
363
|
+
full_timestamp = dt.datetime.strptime(OBS_EVENT_TIMESTAMP, "%Y-%m-%d %H:%M:%S.%f").timestamp()
|
|
364
|
+
logging.debug(f"{full_timestamp=}")
|
|
365
|
+
|
|
366
|
+
# day_timestamp = dt.datetime.strptime(OBS_EVENT_TIMESTAMP.split(" ")[0], "%Y-%m-%d").timestamp()
|
|
367
|
+
# timestamp = dec(str(round(full_timestamp - day_timestamp, 3)))
|
|
368
|
+
timestamp = dec(full_timestamp).quantize(dec(".001"))
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
OBS_EVENT_SUBJECT = event.getElementsByTagName("OBS_EVENT_SUBJECT")[0].getAttribute("NAME")
|
|
372
|
+
except Exception:
|
|
373
|
+
OBS_EVENT_SUBJECT = ""
|
|
374
|
+
|
|
375
|
+
OBS_EVENT_BEHAVIOR = event.getElementsByTagName("OBS_EVENT_BEHAVIOR")[0].getAttribute("NAME")
|
|
376
|
+
logging.debug(f"{OBS_EVENT_BEHAVIOR=}")
|
|
377
|
+
if not OBS_EVENT_BEHAVIOR:
|
|
378
|
+
logging.warning(f"Behavior missing in observation {obs_id} at {timestamp}")
|
|
379
|
+
error_list.append(f"Behavior missing in observation {obs_id} at {timestamp}")
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# modifier
|
|
383
|
+
try:
|
|
384
|
+
OBS_EVENT_BEHAVIOR_MODIFIER = (
|
|
385
|
+
event.getElementsByTagName("OBS_EVENT_BEHAVIOR")[0]
|
|
386
|
+
.getElementsByTagName("OBS_EVENT_BEHAVIOR_MODIFIER")[0]
|
|
387
|
+
.childNodes[0]
|
|
388
|
+
.data
|
|
389
|
+
)
|
|
390
|
+
except Exception:
|
|
391
|
+
OBS_EVENT_BEHAVIOR_MODIFIER: str = ""
|
|
392
|
+
|
|
393
|
+
# comment
|
|
394
|
+
try:
|
|
395
|
+
OBS_EVENT_COMMENT: str = event.getElementsByTagName("OBS_EVENT_COMMENT")[0].childNodes[0].data
|
|
396
|
+
except Exception:
|
|
397
|
+
OBS_EVENT_COMMENT: str = ""
|
|
398
|
+
|
|
399
|
+
logging.debug(f"{timestamp=}")
|
|
400
|
+
logging.debug(f"{OBS_EVENT_SUBJECT=}")
|
|
401
|
+
logging.debug(f"{OBS_EVENT_BEHAVIOR=}")
|
|
402
|
+
logging.debug(f"{OBS_EVENT_BEHAVIOR_MODIFIER=}")
|
|
403
|
+
logging.debug(f"{OBS_EVENT_COMMENT=}")
|
|
404
|
+
|
|
405
|
+
project[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].append(
|
|
406
|
+
[
|
|
407
|
+
timestamp,
|
|
408
|
+
OBS_EVENT_SUBJECT,
|
|
409
|
+
OBS_EVENT_BEHAVIOR,
|
|
410
|
+
OBS_EVENT_BEHAVIOR_MODIFIER,
|
|
411
|
+
OBS_EVENT_COMMENT,
|
|
412
|
+
]
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
project[cfg.PROJECT_NAME] = project_name
|
|
416
|
+
project[cfg.PROJECT_DATE] = project_creation_date.replace(" ", "T")
|
|
417
|
+
project[cfg.ETHOGRAM] = behaviors_boris
|
|
418
|
+
project[cfg.PROJECT_DESCRIPTION] = project_description
|
|
419
|
+
project[cfg.BEHAVIORAL_CATEGORIES] = behav_category
|
|
420
|
+
project[cfg.SUBJECTS] = subjects
|
|
421
|
+
project[cfg.INDEPENDENT_VARIABLES] = variables_boris
|
|
422
|
+
|
|
423
|
+
if flag_long_key:
|
|
424
|
+
error_list.append("The keys longer than one char were deleted.")
|
|
425
|
+
logging.debug("The keys longer than one char were deleted.")
|
|
426
|
+
|
|
427
|
+
return project, error_list
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
import sys
|
|
432
|
+
import pprint
|
|
433
|
+
|
|
434
|
+
logging.basicConfig(
|
|
435
|
+
format="%(asctime)s,%(msecs)d %(module)s l.%(lineno)d %(levelname)s %(message)s",
|
|
436
|
+
datefmt="%H:%M:%S",
|
|
437
|
+
level=logging.DEBUG,
|
|
438
|
+
)
|
|
439
|
+
project, errors = otx_to_boris(sys.argv[1])
|
|
440
|
+
|
|
441
|
+
pprint.pprint(project)
|
|
442
|
+
pprint.pprint(errors)
|
boris/param_panel.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS 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 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS 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 see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from PySide6.QtCore import Qt
|
|
24
|
+
from PySide6.QtWidgets import QDialog
|
|
25
|
+
|
|
26
|
+
from . import config as cfg
|
|
27
|
+
from .param_panel_ui import Ui_Dialog
|
|
28
|
+
from . import dialog
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Param_panel(QDialog, Ui_Dialog):
|
|
32
|
+
def __init__(self, parent=None):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.setupUi(self)
|
|
35
|
+
|
|
36
|
+
self.media_duration = None
|
|
37
|
+
|
|
38
|
+
# insert duration widget for time offset
|
|
39
|
+
self.start_time = dialog.get_time_widget(0)
|
|
40
|
+
self.horizontalLayout.insertWidget(1, self.start_time)
|
|
41
|
+
self.end_time = dialog.get_time_widget(0)
|
|
42
|
+
self.horizontalLayout_6.insertWidget(1, self.end_time)
|
|
43
|
+
|
|
44
|
+
self.pbSelectAllSubjects.clicked.connect(lambda: self.subjects_button_clicked("select all"))
|
|
45
|
+
self.pbUnselectAllSubjects.clicked.connect(lambda: self.subjects_button_clicked("unselect all"))
|
|
46
|
+
self.pbReverseSubjectsSelection.clicked.connect(lambda: self.subjects_button_clicked("reverse selection"))
|
|
47
|
+
|
|
48
|
+
self.pbSelectAllBehaviors.clicked.connect(lambda: self.behaviors_button_clicked("select all"))
|
|
49
|
+
self.pbUnselectAllBehaviors.clicked.connect(lambda: self.behaviors_button_clicked("unselect all"))
|
|
50
|
+
self.pbReverseBehaviorsSelection.clicked.connect(lambda: self.behaviors_button_clicked("reverse selection"))
|
|
51
|
+
|
|
52
|
+
self.pbOK.clicked.connect(self.ok)
|
|
53
|
+
self.pbCancel.clicked.connect(self.reject)
|
|
54
|
+
|
|
55
|
+
self.lwBehaviors.itemClicked.connect(self.behavior_item_clicked)
|
|
56
|
+
|
|
57
|
+
self.rb_observed_events.setChecked(True)
|
|
58
|
+
|
|
59
|
+
self.rb_media_duration.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_FULL_OBS))
|
|
60
|
+
self.rb_observed_events.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_EVENTS))
|
|
61
|
+
self.rb_user_defined.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_ARBITRARY_INTERVAL))
|
|
62
|
+
self.rb_obs_interval.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_OBS_INTERVAL))
|
|
63
|
+
|
|
64
|
+
self.cbIncludeModifiers.stateChanged.connect(self.cb_exclude_non_coded_modifiers_visibility)
|
|
65
|
+
|
|
66
|
+
self.cb_exclude_non_coded_modifiers.setVisible(False)
|
|
67
|
+
|
|
68
|
+
def cb_exclude_non_coded_modifiers_visibility(self):
|
|
69
|
+
"""
|
|
70
|
+
set visibility of cb_exclude_non_coded_modifiers
|
|
71
|
+
"""
|
|
72
|
+
self.cb_exclude_non_coded_modifiers.setEnabled(self.cbIncludeModifiers.isChecked())
|
|
73
|
+
|
|
74
|
+
def rb_time_interval_selection(self, button):
|
|
75
|
+
"""
|
|
76
|
+
select the time interval for operation
|
|
77
|
+
"""
|
|
78
|
+
if button == cfg.TIME_ARBITRARY_INTERVAL:
|
|
79
|
+
self.frm_time_interval.setEnabled(True)
|
|
80
|
+
self.frm_time_interval.setVisible(True)
|
|
81
|
+
|
|
82
|
+
elif button == cfg.TIME_EVENTS and len(self.selectedObservations) == 1:
|
|
83
|
+
self.start_time.set_time(self.start_coding)
|
|
84
|
+
self.end_time.set_time(self.end_coding)
|
|
85
|
+
self.frm_time_interval.setEnabled(False)
|
|
86
|
+
self.frm_time_interval.setVisible(True)
|
|
87
|
+
|
|
88
|
+
elif button == cfg.TIME_FULL_OBS and len(self.selectedObservations) == 1 and self.media_duration is not None:
|
|
89
|
+
self.start_time.set_time(0)
|
|
90
|
+
self.end_time.set_time(self.media_duration)
|
|
91
|
+
self.frm_time_interval.setEnabled(False)
|
|
92
|
+
self.frm_time_interval.setVisible(True)
|
|
93
|
+
|
|
94
|
+
elif button == cfg.TIME_OBS_INTERVAL:
|
|
95
|
+
if not ((self.start_interval is None) or self.start_interval.is_nan()):
|
|
96
|
+
# Set start_time and end_time widgets values even if it is not shown with
|
|
97
|
+
# more than 1 observation as some analyses might use it (eg: advanced event filtering)
|
|
98
|
+
|
|
99
|
+
end_interval = self.end_interval if self.end_interval != 0 else self.media_duration
|
|
100
|
+
|
|
101
|
+
self.start_time.set_time(self.start_interval)
|
|
102
|
+
self.end_time.set_time(end_interval)
|
|
103
|
+
self.frm_time_interval.setEnabled(False)
|
|
104
|
+
if len(self.selectedObservations) == 1:
|
|
105
|
+
self.frm_time_interval.setVisible(True)
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
self.frm_time_interval.setVisible(False)
|
|
109
|
+
|
|
110
|
+
def subjects_button_clicked(self, command):
|
|
111
|
+
for idx in range(self.lwSubjects.count()):
|
|
112
|
+
cb = self.lwSubjects.itemWidget(self.lwSubjects.item(idx))
|
|
113
|
+
if command == "select all":
|
|
114
|
+
cb.setChecked(True)
|
|
115
|
+
if command == "unselect all":
|
|
116
|
+
cb.setChecked(False)
|
|
117
|
+
if command == "reverse selection":
|
|
118
|
+
cb.setChecked(not cb.isChecked())
|
|
119
|
+
|
|
120
|
+
def behaviors_button_clicked(self, command):
|
|
121
|
+
for idx in range(self.lwBehaviors.count()):
|
|
122
|
+
if self.lwBehaviors.item(idx).data(33) != "category":
|
|
123
|
+
if command == "select all":
|
|
124
|
+
self.lwBehaviors.item(idx).setCheckState(Qt.Checked)
|
|
125
|
+
|
|
126
|
+
if command == "unselect all":
|
|
127
|
+
self.lwBehaviors.item(idx).setCheckState(Qt.Unchecked)
|
|
128
|
+
|
|
129
|
+
if command == "reverse selection":
|
|
130
|
+
if self.lwBehaviors.item(idx).checkState() == Qt.Checked:
|
|
131
|
+
self.lwBehaviors.item(idx).setCheckState(Qt.Unchecked)
|
|
132
|
+
else:
|
|
133
|
+
self.lwBehaviors.item(idx).setCheckState(Qt.Checked)
|
|
134
|
+
|
|
135
|
+
def ok(self):
|
|
136
|
+
selectedSubjects = []
|
|
137
|
+
for idx in range(self.lwSubjects.count()):
|
|
138
|
+
cb = self.lwSubjects.itemWidget(self.lwSubjects.item(idx))
|
|
139
|
+
if cb.isChecked():
|
|
140
|
+
selectedSubjects.append(cb.text())
|
|
141
|
+
self.selectedSubjects = selectedSubjects
|
|
142
|
+
|
|
143
|
+
selectedBehaviors = []
|
|
144
|
+
for idx in range(self.lwBehaviors.count()):
|
|
145
|
+
if self.lwBehaviors.item(idx).checkState() == Qt.Checked:
|
|
146
|
+
selectedBehaviors.append(self.lwBehaviors.item(idx).text())
|
|
147
|
+
self.selectedBehaviors = selectedBehaviors
|
|
148
|
+
|
|
149
|
+
self.accept()
|
|
150
|
+
|
|
151
|
+
def behavior_item_clicked(self, item):
|
|
152
|
+
"""
|
|
153
|
+
check / uncheck behaviors belonging to the clicked category
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
if item.data(33) == "category":
|
|
157
|
+
category = item.data(34)
|
|
158
|
+
for i in range(self.lwBehaviors.count()):
|
|
159
|
+
if self.lwBehaviors.item(i).data(34) == category and self.lwBehaviors.item(i).data(33) != "category":
|
|
160
|
+
if item.data(35):
|
|
161
|
+
self.lwBehaviors.item(i).setCheckState(Qt.Unchecked)
|
|
162
|
+
else:
|
|
163
|
+
self.lwBehaviors.item(i).setCheckState(Qt.Checked)
|
|
164
|
+
|
|
165
|
+
item.setData(35, not item.data(35))
|
|
166
|
+
|
|
167
|
+
def extract_observed_behaviors(self, selected_observations, selected_subjects):
|
|
168
|
+
"""
|
|
169
|
+
extract unique behaviors codes from obs_id observation and selected subjects
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
observed_behaviors = []
|
|
173
|
+
|
|
174
|
+
# extract events from selected observations
|
|
175
|
+
all_events = [self.pj[cfg.OBSERVATIONS][x][cfg.EVENTS] for x in self.pj[cfg.OBSERVATIONS] if x in selected_observations]
|
|
176
|
+
|
|
177
|
+
for events in all_events:
|
|
178
|
+
for event in events:
|
|
179
|
+
if event[cfg.EVENT_SUBJECT_FIELD_IDX] in selected_subjects or (
|
|
180
|
+
not event[cfg.EVENT_SUBJECT_FIELD_IDX] and cfg.NO_FOCAL_SUBJECT in selected_subjects
|
|
181
|
+
):
|
|
182
|
+
observed_behaviors.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
183
|
+
|
|
184
|
+
# remove duplicate
|
|
185
|
+
return list(set(observed_behaviors))
|
|
186
|
+
|
|
187
|
+
def cb_changed(self):
|
|
188
|
+
selected_subjects: list = []
|
|
189
|
+
for idx in range(self.lwSubjects.count()):
|
|
190
|
+
cb = self.lwSubjects.itemWidget(self.lwSubjects.item(idx))
|
|
191
|
+
if cb and cb.isChecked():
|
|
192
|
+
selected_subjects.append(cb.text())
|
|
193
|
+
|
|
194
|
+
observed_behaviors = self.extract_observed_behaviors(self.selectedObservations, selected_subjects)
|
|
195
|
+
|
|
196
|
+
for idx in range(self.lwBehaviors.count()):
|
|
197
|
+
if self.lwBehaviors.item(idx).data(33) != "category":
|
|
198
|
+
if self.lwBehaviors.item(idx).text() in observed_behaviors:
|
|
199
|
+
self.lwBehaviors.item(idx).setCheckState(Qt.Checked)
|
|
200
|
+
else:
|
|
201
|
+
self.lwBehaviors.item(idx).setCheckState(Qt.Unchecked)
|