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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- 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
|
@@ -0,0 +1,2041 @@
|
|
|
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
|
+
import gzip
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import numpy as np
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
import sys
|
|
29
|
+
from decimal import Decimal as dec
|
|
30
|
+
from shutil import copyfile
|
|
31
|
+
from typing import List, Tuple, Dict
|
|
32
|
+
|
|
33
|
+
import tablib
|
|
34
|
+
from PySide6.QtWidgets import QMessageBox, QTableWidgetItem, QAbstractItemView
|
|
35
|
+
from PySide6.QtCore import Qt
|
|
36
|
+
|
|
37
|
+
from . import config as cfg
|
|
38
|
+
from . import db_functions
|
|
39
|
+
from . import dialog
|
|
40
|
+
from . import observation_operations
|
|
41
|
+
from . import portion as I
|
|
42
|
+
from . import utilities as util
|
|
43
|
+
from . import version
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_observation_exhaustivity(
|
|
47
|
+
events: List[list],
|
|
48
|
+
state_events_list: list = [],
|
|
49
|
+
) -> float:
|
|
50
|
+
"""
|
|
51
|
+
calculate the observation exhaustivity
|
|
52
|
+
if ethogram not empty state events list is determined else
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
events (List[list]): events
|
|
56
|
+
ethogram (list):
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def interval_len(interval: I) -> dec:
|
|
60
|
+
""" "
|
|
61
|
+
returns duration of an interval or a set of intervals
|
|
62
|
+
"""
|
|
63
|
+
if interval.empty:
|
|
64
|
+
return dec(0)
|
|
65
|
+
else:
|
|
66
|
+
return dec(sum([x.upper - x.lower for x in interval]))
|
|
67
|
+
|
|
68
|
+
events_interval: dict = {}
|
|
69
|
+
mem_events_interval: dict = {}
|
|
70
|
+
|
|
71
|
+
for event in events:
|
|
72
|
+
if event[cfg.EVENT_SUBJECT_FIELD_IDX] not in events_interval:
|
|
73
|
+
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]] = {}
|
|
74
|
+
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]] = {}
|
|
75
|
+
|
|
76
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]]:
|
|
77
|
+
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = I.empty()
|
|
78
|
+
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
|
|
79
|
+
|
|
80
|
+
# state event
|
|
81
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_events_list:
|
|
82
|
+
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]].append(
|
|
83
|
+
event[cfg.EVENT_TIME_FIELD_IDX]
|
|
84
|
+
)
|
|
85
|
+
if len(mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]]) == 2:
|
|
86
|
+
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.closedopen(
|
|
87
|
+
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][0],
|
|
88
|
+
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][1],
|
|
89
|
+
)
|
|
90
|
+
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
|
|
91
|
+
# point event
|
|
92
|
+
else:
|
|
93
|
+
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.singleton(
|
|
94
|
+
event[cfg.EVENT_TIME_FIELD_IDX]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if events:
|
|
98
|
+
# coding duration
|
|
99
|
+
event_timestamps = [event[cfg.EVENT_TIME_FIELD_IDX] for event in events]
|
|
100
|
+
obs_theo_dur = max(event_timestamps) - min(event_timestamps)
|
|
101
|
+
else:
|
|
102
|
+
obs_theo_dur = dec("0")
|
|
103
|
+
|
|
104
|
+
total_duration = 0
|
|
105
|
+
for subject in events_interval:
|
|
106
|
+
tot_behav_for_subject = I.empty()
|
|
107
|
+
for behav in events_interval[subject]:
|
|
108
|
+
tot_behav_for_subject |= events_interval[subject][behav]
|
|
109
|
+
|
|
110
|
+
obs_real_dur = interval_len(tot_behav_for_subject)
|
|
111
|
+
|
|
112
|
+
if obs_real_dur >= obs_theo_dur:
|
|
113
|
+
obs_real_dur = obs_theo_dur
|
|
114
|
+
|
|
115
|
+
total_duration += obs_real_dur
|
|
116
|
+
|
|
117
|
+
if len(events_interval) and obs_theo_dur:
|
|
118
|
+
exhausivity_percent = total_duration / (len(events_interval) * obs_theo_dur) * 100
|
|
119
|
+
else:
|
|
120
|
+
exhausivity_percent = 0
|
|
121
|
+
|
|
122
|
+
return round(exhausivity_percent, 1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_observation_exhaustivity_pictures(obs) -> float:
|
|
126
|
+
"""
|
|
127
|
+
check exhaustivity of coding for observations from pictures
|
|
128
|
+
"""
|
|
129
|
+
if obs[cfg.TYPE] != cfg.IMAGES:
|
|
130
|
+
return -1
|
|
131
|
+
tot_images_number = 0
|
|
132
|
+
|
|
133
|
+
for dir_path in obs.get(cfg.DIRECTORIES_LIST, []):
|
|
134
|
+
result = util.dir_images_number(dir_path)
|
|
135
|
+
tot_images_number += result.get("number of images", 0)
|
|
136
|
+
|
|
137
|
+
if not tot_images_number:
|
|
138
|
+
return "No pictures found"
|
|
139
|
+
|
|
140
|
+
# list of paths of coded images
|
|
141
|
+
coded_images_number = len(set([x[cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_PATH]] for x in obs[cfg.EVENTS]]))
|
|
142
|
+
|
|
143
|
+
return round(coded_images_number / tot_images_number * 100, 1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def behavior_category(ethogram: dict) -> Dict[str, str]:
|
|
147
|
+
"""
|
|
148
|
+
returns a dictionary containing the behavioral category of each behavior
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
ethogram (dict): ethogram
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
dict: dictionary containing behavioral category (value) for each behavior code (key)
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
behavioral_category = {}
|
|
158
|
+
for idx in ethogram:
|
|
159
|
+
if cfg.BEHAVIOR_CATEGORY in ethogram[idx]:
|
|
160
|
+
behavioral_category[ethogram[idx][cfg.BEHAVIOR_CODE]] = ethogram[idx][cfg.BEHAVIOR_CATEGORY]
|
|
161
|
+
else:
|
|
162
|
+
behavioral_category[ethogram[idx][cfg.BEHAVIOR_CODE]] = ""
|
|
163
|
+
return behavioral_category
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_if_media_available(observation: dict, project_file_name: str) -> Tuple[bool, str]:
|
|
167
|
+
"""
|
|
168
|
+
check if media files available for media and images observations
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
observation (dict): observation to be checked
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
bool: True if media files found or for live observation
|
|
175
|
+
else False
|
|
176
|
+
str: error message
|
|
177
|
+
"""
|
|
178
|
+
if observation[cfg.TYPE] == cfg.LIVE:
|
|
179
|
+
return (True, "")
|
|
180
|
+
|
|
181
|
+
# TODO: check all files before returning False
|
|
182
|
+
if observation[cfg.TYPE] == cfg.IMAGES:
|
|
183
|
+
for img_dir in observation.get(cfg.DIRECTORIES_LIST, []):
|
|
184
|
+
if not full_path(img_dir, project_file_name):
|
|
185
|
+
return (False, f"The images directory <b>{img_dir}</b> was not found")
|
|
186
|
+
return (True, "")
|
|
187
|
+
|
|
188
|
+
if observation[cfg.TYPE] == cfg.MEDIA:
|
|
189
|
+
for nplayer in cfg.ALL_PLAYERS:
|
|
190
|
+
if nplayer in observation.get(cfg.FILE, {}):
|
|
191
|
+
if not isinstance(observation[cfg.FILE][nplayer], list):
|
|
192
|
+
return (False, "error")
|
|
193
|
+
for media_file in observation[cfg.FILE][nplayer]:
|
|
194
|
+
if not full_path(media_file, project_file_name):
|
|
195
|
+
return (False, f"Media file <b>{media_file}</b> was not found")
|
|
196
|
+
return (True, "")
|
|
197
|
+
|
|
198
|
+
return (False, "Observation type not found")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def check_directories_availability(observation: dict, project_file_name: str) -> Tuple[bool, str]:
|
|
202
|
+
"""
|
|
203
|
+
check if directories are available
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
observation (dict): observation to be checked
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
bool: True if all directories were found or for live observation
|
|
210
|
+
else False
|
|
211
|
+
str: error message
|
|
212
|
+
"""
|
|
213
|
+
if observation[cfg.TYPE] == cfg.LIVE:
|
|
214
|
+
return (True, "")
|
|
215
|
+
|
|
216
|
+
for dir_path in observation.get(cfg.DIRECTORIES_LIST, []):
|
|
217
|
+
if not full_path(dir_path, project_file_name):
|
|
218
|
+
return (False, f"Directory <b>{dir_path}</b> not found")
|
|
219
|
+
|
|
220
|
+
return (True, "")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def check_coded_behaviors_in_obs_list(pj: dict, observations_list: list) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
check if coded behaviors in a list of observations are defined in the ethogram
|
|
226
|
+
"""
|
|
227
|
+
out = ""
|
|
228
|
+
ethogram_behavior_codes = {pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in pj[cfg.ETHOGRAM]}
|
|
229
|
+
behaviors_not_defined = []
|
|
230
|
+
out = "" # will contain the output
|
|
231
|
+
for obs_id in observations_list:
|
|
232
|
+
for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
|
|
233
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in ethogram_behavior_codes:
|
|
234
|
+
behaviors_not_defined.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
235
|
+
if set(sorted(behaviors_not_defined)):
|
|
236
|
+
out += f"The following behaviors are not defined in the ethogram: <b>{', '.join(set(sorted(behaviors_not_defined)))}</b><br><br>"
|
|
237
|
+
results = dialog.Results_dialog()
|
|
238
|
+
results.setWindowTitle(f"{cfg.programName} - Check selected observations")
|
|
239
|
+
results.ptText.setReadOnly(True)
|
|
240
|
+
results.ptText.appendHtml(out)
|
|
241
|
+
results.pbSave.setVisible(False)
|
|
242
|
+
results.pbCancel.setVisible(True)
|
|
243
|
+
if not results.exec_():
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def get_modifiers_of_behavior(ethogram, behavior: str) -> list:
|
|
249
|
+
"""
|
|
250
|
+
get all modifiers for a behavior (if any)
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
return [
|
|
254
|
+
[ethogram[x][cfg.MODIFIERS][y]["values"] for y in ethogram[x][cfg.MODIFIERS]]
|
|
255
|
+
for x in ethogram
|
|
256
|
+
if ethogram[x][cfg.BEHAVIOR_CODE] == behavior
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def check_coded_behaviors(pj: dict) -> set:
|
|
261
|
+
"""
|
|
262
|
+
check if behaviors coded in events are defined in ethogram for all observations
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
pj (dict): project dictionary
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
set: behaviors present in observations that are not defined in ethogram
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
# set of behaviors defined in ethogram
|
|
272
|
+
ethogram_behavior_codes = {pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] for idx in pj[cfg.ETHOGRAM]}
|
|
273
|
+
behaviors_not_defined = []
|
|
274
|
+
|
|
275
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
276
|
+
for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
|
|
277
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in ethogram_behavior_codes:
|
|
278
|
+
behaviors_not_defined.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
279
|
+
return set(sorted(behaviors_not_defined))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS) -> Tuple[bool, str]:
|
|
283
|
+
"""
|
|
284
|
+
check state events for the observation obsId
|
|
285
|
+
check if behaviors in observation are defined in ethogram
|
|
286
|
+
check if number is odd
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
obsId (str): id of observation to check
|
|
290
|
+
ethogram (dict): ethogram of project
|
|
291
|
+
observation (dict): observation to be checked
|
|
292
|
+
time_format (str): time format
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
tuple (bool, str): if OK True else False , message
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
out: str = ""
|
|
299
|
+
|
|
300
|
+
# check if behaviors are defined as "state event"
|
|
301
|
+
event_types = {ethogram[idx]["type"] for idx in ethogram}
|
|
302
|
+
|
|
303
|
+
if not event_types or event_types == {"Point event"}:
|
|
304
|
+
return (True, "No behavior is defined as `State event`")
|
|
305
|
+
|
|
306
|
+
subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
|
|
307
|
+
ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
308
|
+
state_behaviors = set(util.state_behavior_codes(ethogram))
|
|
309
|
+
|
|
310
|
+
for subject in sorted(set(subjects)):
|
|
311
|
+
behaviors = [
|
|
312
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
for behavior in sorted(set(behaviors)):
|
|
316
|
+
if behavior not in ethogram_behaviors:
|
|
317
|
+
# return (False, "The behaviour <b>{}</b> is not defined in the ethogram.<br>".format(behavior))
|
|
318
|
+
continue
|
|
319
|
+
else:
|
|
320
|
+
if behavior not in state_behaviors:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
lst: list = []
|
|
324
|
+
memTime: dict = {}
|
|
325
|
+
for event in [
|
|
326
|
+
event
|
|
327
|
+
for event in observation[cfg.EVENTS]
|
|
328
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
329
|
+
]:
|
|
330
|
+
behav_modif = [
|
|
331
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
332
|
+
event[cfg.EVENT_MODIFIER_FIELD_IDX],
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
if behav_modif in lst:
|
|
336
|
+
lst.remove(behav_modif)
|
|
337
|
+
del memTime[str(behav_modif)]
|
|
338
|
+
else:
|
|
339
|
+
lst.append(behav_modif)
|
|
340
|
+
memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
|
|
341
|
+
|
|
342
|
+
for event in lst:
|
|
343
|
+
out += (
|
|
344
|
+
f"The behavior <b>{behavior}</b> "
|
|
345
|
+
f"{('(modifier ' + event[1] + ') ') if event[1] else ''} is not PAIRED "
|
|
346
|
+
f'for subject "<b>{subject if subject else cfg.NO_FOCAL_SUBJECT}</b>" at '
|
|
347
|
+
f"<b>{memTime[str(event)] if time_format == cfg.S else util.seconds2time(memTime[str(event)])}</b><br>"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return (False, out) if out else (True, "No problem detected")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
|
|
354
|
+
"""
|
|
355
|
+
check if state events are paired in a list of observations
|
|
356
|
+
use check_state_events_obs function
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
logging.info("Check state events function")
|
|
360
|
+
|
|
361
|
+
out = ""
|
|
362
|
+
not_paired_obs_list = []
|
|
363
|
+
for obs_id in observations_list:
|
|
364
|
+
r, msg = check_state_events_obs(obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id])
|
|
365
|
+
|
|
366
|
+
if not r:
|
|
367
|
+
out += f"Observation: <strong>{obs_id}</strong><br>{msg}<br>"
|
|
368
|
+
not_paired_obs_list.append(obs_id)
|
|
369
|
+
|
|
370
|
+
if out:
|
|
371
|
+
out = f"The observations with UNPAIRED state events will be removed from the analysis<br><br>{out}"
|
|
372
|
+
results = dialog.Results_dialog()
|
|
373
|
+
results.setWindowTitle(f"{cfg.programName} - Check selected observations")
|
|
374
|
+
results.ptText.setReadOnly(True)
|
|
375
|
+
results.ptText.appendHtml(out)
|
|
376
|
+
results.pbSave.setVisible(False)
|
|
377
|
+
results.pbCancel.setVisible(True)
|
|
378
|
+
if not results.exec_():
|
|
379
|
+
return True, []
|
|
380
|
+
|
|
381
|
+
# remove observations with unpaired state events
|
|
382
|
+
new_observations_list = [x for x in observations_list if x not in not_paired_obs_list]
|
|
383
|
+
if not new_observations_list:
|
|
384
|
+
QMessageBox.warning(None, cfg.programName, "The observation list is empty")
|
|
385
|
+
|
|
386
|
+
logging.info("Check state events done")
|
|
387
|
+
|
|
388
|
+
return False, new_observations_list # no state events are unpaired
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def check_project_integrity(
|
|
392
|
+
pj: dict,
|
|
393
|
+
time_format: str,
|
|
394
|
+
project_file_name: str,
|
|
395
|
+
media_file_available: bool = True,
|
|
396
|
+
) -> str:
|
|
397
|
+
"""
|
|
398
|
+
check project integrity:
|
|
399
|
+
* check if coded behaviors are defined in ethogram
|
|
400
|
+
* check unpaired state events
|
|
401
|
+
* check if timestamp between -2147483647 and 2147483647 (2**31 - 1)
|
|
402
|
+
* check if behavior belong to behavioral category that do not more exist
|
|
403
|
+
* check for leading and trailing spaces and special chars in modifiers
|
|
404
|
+
* check if media file are available (optional)
|
|
405
|
+
* check if media length available
|
|
406
|
+
* check independent variables
|
|
407
|
+
* check if coded subjects are defined
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
pj (dict): BORIS project
|
|
411
|
+
time_format (str): time format
|
|
412
|
+
project_file_name (str): project file name
|
|
413
|
+
media_file_access(bool): check if media file are available
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
str: message
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
TODO: implement check on order of events (for live and media)
|
|
420
|
+
|
|
421
|
+
"""
|
|
422
|
+
out: str = ""
|
|
423
|
+
|
|
424
|
+
# check if coded behaviors are defined in ethogram
|
|
425
|
+
if check_coded_behaviors(pj):
|
|
426
|
+
out += f"The following behaviors are not defined in the ethogram: <b>{', '.join(r)}</b><br>"
|
|
427
|
+
flag_all_behaviors_defined = False
|
|
428
|
+
else:
|
|
429
|
+
flag_all_behaviors_defined = True
|
|
430
|
+
|
|
431
|
+
# check for unpaired state events
|
|
432
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
433
|
+
ok, msg = check_state_events_obs(obs_id, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs_id], time_format)
|
|
434
|
+
if not ok:
|
|
435
|
+
out += "<br><br>" if out else ""
|
|
436
|
+
out += f"Observation: <b>{obs_id}</b><br>{msg}"
|
|
437
|
+
|
|
438
|
+
# check if behavior belong to category that is not in categories list
|
|
439
|
+
for idx in pj[cfg.ETHOGRAM]:
|
|
440
|
+
if cfg.BEHAVIOR_CATEGORY in pj[cfg.ETHOGRAM][idx]:
|
|
441
|
+
if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY]:
|
|
442
|
+
if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] not in pj[cfg.BEHAVIORAL_CATEGORIES]:
|
|
443
|
+
out += "<br><br>" if out else ""
|
|
444
|
+
out += (
|
|
445
|
+
f"The behavior <b>{pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE]}</b> belongs "
|
|
446
|
+
f"to the behavioral category <b>{pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY]}</b> "
|
|
447
|
+
"that is no more in behavioral categories list."
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# check for leading/trailing spaces/special chars in modifiers defined in ethogram
|
|
451
|
+
for idx in pj[cfg.ETHOGRAM]:
|
|
452
|
+
for k in pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]:
|
|
453
|
+
for value in pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS][k]["values"]:
|
|
454
|
+
modifier_code = value.split(" (")[0]
|
|
455
|
+
if modifier_code.strip() != modifier_code:
|
|
456
|
+
out += "<br><br>" if out else ""
|
|
457
|
+
out += (
|
|
458
|
+
"The following <b>modifier</b> defined in ethogram "
|
|
459
|
+
"has leading/trailing spaces or special chars: "
|
|
460
|
+
f"<b>{util.replace_leading_trailing_chars(modifier_code, old_char=' ', new_char='█')}</b>"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# check if all media are available
|
|
464
|
+
if media_file_available:
|
|
465
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
466
|
+
ok, msg = check_if_media_available(pj[cfg.OBSERVATIONS][obs_id], project_file_name)
|
|
467
|
+
if not ok:
|
|
468
|
+
out += "<br><br>" if out else ""
|
|
469
|
+
out += f"Observation: <b>{obs_id}</b><br>{msg}"
|
|
470
|
+
|
|
471
|
+
out_events: str = ""
|
|
472
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
473
|
+
# check if timestamp between -2147483647 and 2147483647
|
|
474
|
+
for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
|
|
475
|
+
timestamp = event[cfg.PJ_OBS_FIELDS[pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]][cfg.TIME]]
|
|
476
|
+
if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
|
|
477
|
+
out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
|
|
478
|
+
|
|
479
|
+
"""
|
|
480
|
+
# check if media length available
|
|
481
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
482
|
+
for nplayer in cfg.ALL_PLAYERS:
|
|
483
|
+
if nplayer in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
484
|
+
for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
|
|
485
|
+
try:
|
|
486
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
|
|
487
|
+
except KeyError:
|
|
488
|
+
out += "<br><br>" if out else ""
|
|
489
|
+
out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
out += "<br><br>" if out else ""
|
|
493
|
+
out += out_events
|
|
494
|
+
|
|
495
|
+
# check for leading/trailing spaces/special chars in observation id
|
|
496
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
497
|
+
if obs_id != obs_id.strip():
|
|
498
|
+
out += "<br><br>" if out else ""
|
|
499
|
+
out += (
|
|
500
|
+
"The following <b>observation id</b> "
|
|
501
|
+
"has leading/trailing spaces or special chars: "
|
|
502
|
+
f"<b>{util.replace_leading_trailing_chars(obs_id, ' ', '█')}</b>"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# check independent variables present in observations are defined
|
|
506
|
+
defined_var_label = [pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})]
|
|
507
|
+
not_defined: dict = {}
|
|
508
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
509
|
+
if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
|
|
510
|
+
continue
|
|
511
|
+
for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
|
|
512
|
+
if var_label not in defined_var_label:
|
|
513
|
+
if var_label not in not_defined:
|
|
514
|
+
not_defined[var_label] = [obs_id]
|
|
515
|
+
else:
|
|
516
|
+
not_defined[var_label].append(obs_id)
|
|
517
|
+
if not_defined:
|
|
518
|
+
out += "<br><br>" if out else ""
|
|
519
|
+
for var_label in not_defined:
|
|
520
|
+
out += (
|
|
521
|
+
f"The independent variable <b>{util.replace_leading_trailing_chars(var_label, ' ', '█')}</b> "
|
|
522
|
+
f"present in {len(not_defined[var_label])} observation(s) is not defined.<br>"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# check values of independent variables
|
|
526
|
+
defined_set_var_label: dict = dict(
|
|
527
|
+
[
|
|
528
|
+
(
|
|
529
|
+
pj[cfg.INDEPENDENT_VARIABLES][idx]["label"],
|
|
530
|
+
pj[cfg.INDEPENDENT_VARIABLES][idx]["possible values"],
|
|
531
|
+
)
|
|
532
|
+
for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})
|
|
533
|
+
if pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "value from set"
|
|
534
|
+
]
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
tmp_out: str = ""
|
|
538
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
539
|
+
if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
|
|
540
|
+
continue
|
|
541
|
+
for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
|
|
542
|
+
if var_label in defined_set_var_label:
|
|
543
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
|
|
544
|
+
tmp_out += (
|
|
545
|
+
f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
|
|
546
|
+
f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
|
|
547
|
+
)
|
|
548
|
+
if tmp_out:
|
|
549
|
+
out += "<br><br>" if out else ""
|
|
550
|
+
out += tmp_out
|
|
551
|
+
|
|
552
|
+
# check if coded subjects are defined in the subjects list
|
|
553
|
+
tmp_out: str = ""
|
|
554
|
+
subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
|
|
555
|
+
coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
|
|
556
|
+
|
|
557
|
+
for subject in coded_subjects:
|
|
558
|
+
if subject and subject not in subjects_list:
|
|
559
|
+
tmp_out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
|
|
560
|
+
if tmp_out:
|
|
561
|
+
out += "<br><br>" if out else ""
|
|
562
|
+
out += tmp_out
|
|
563
|
+
|
|
564
|
+
# check if media file have info in media_info section of project
|
|
565
|
+
tmp_out: str = ""
|
|
566
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
567
|
+
for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
568
|
+
for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
|
|
569
|
+
for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
|
|
570
|
+
if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO].get(info, {}):
|
|
571
|
+
tmp_out += f"Observation <b>{obs_id}</b>:<br>"
|
|
572
|
+
tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
|
|
573
|
+
if tmp_out:
|
|
574
|
+
tmp_out += "<br>You should repick the media file to fix this issue."
|
|
575
|
+
out += "<br><br>" if out else ""
|
|
576
|
+
out += tmp_out
|
|
577
|
+
|
|
578
|
+
# check if the number of coded modifiers correspond to the number of sets of modifier
|
|
579
|
+
obs_results: dict = {}
|
|
580
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
581
|
+
for event_idx, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
582
|
+
for idx in pj[cfg.ETHOGRAM]:
|
|
583
|
+
if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == event[cfg.EVENT_BEHAVIOR_FIELD_IDX]:
|
|
584
|
+
break
|
|
585
|
+
else:
|
|
586
|
+
# behavior not defined in ethogram
|
|
587
|
+
continue
|
|
588
|
+
|
|
589
|
+
if (not event[cfg.EVENT_MODIFIER_FIELD_IDX]) and not pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]: # no modifiers
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
if len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split("|")) != len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]):
|
|
593
|
+
# print("behavior", event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
594
|
+
# print(f"modifier(s) #{event[cfg.EVENT_MODIFIER_FIELD_IDX]}#", len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split("|")))
|
|
595
|
+
# print(pj[cfg.ETHOGRAM][idx]["code"], pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])
|
|
596
|
+
# print()
|
|
597
|
+
if obs_id not in obs_results:
|
|
598
|
+
obs_results[obs_id] = []
|
|
599
|
+
|
|
600
|
+
obs_results[obs_id].append(
|
|
601
|
+
(
|
|
602
|
+
f"Event #{event_idx}: the coded modifiers for {event[cfg.EVENT_BEHAVIOR_FIELD_IDX]} are {len(event[cfg.EVENT_MODIFIER_FIELD_IDX].split('|'))} "
|
|
603
|
+
f"but {len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])} sets were defined in ethogram."
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if obs_results:
|
|
608
|
+
out += "<br><br>" if out else ""
|
|
609
|
+
for o in obs_results:
|
|
610
|
+
out += f"<br>Observation <b>{o}</b>:<br>"
|
|
611
|
+
out += "<br>".join(obs_results[o])
|
|
612
|
+
out += "<br><br>"
|
|
613
|
+
|
|
614
|
+
return out
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def create_subtitles(pj: dict, selected_observations: list, parameters: dict, export_dir: str) -> Tuple[bool, str]:
|
|
618
|
+
"""
|
|
619
|
+
create subtitles for selected observations, subjects and behaviors
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
pj (dict): project
|
|
623
|
+
selected_observations (list): list of observations
|
|
624
|
+
parameters (dict):
|
|
625
|
+
export_dir (str): directory to save subtitles
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
bool: True if OK else False
|
|
629
|
+
str: error message
|
|
630
|
+
"""
|
|
631
|
+
|
|
632
|
+
def subject_color(subject: str) -> Tuple[str, str]:
|
|
633
|
+
"""
|
|
634
|
+
subject color
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
subject (str): subject name
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
str: HTML tag for color font (beginning)
|
|
641
|
+
str: HTML tag for color font (closing)
|
|
642
|
+
"""
|
|
643
|
+
if subject == cfg.NO_FOCAL_SUBJECT:
|
|
644
|
+
return "", ""
|
|
645
|
+
else:
|
|
646
|
+
return (
|
|
647
|
+
f"""<font color="{
|
|
648
|
+
cfg.subtitlesColors[parameters[cfg.SELECTED_SUBJECTS].index(row["subject"]) % len(cfg.subtitlesColors)]
|
|
649
|
+
}">""",
|
|
650
|
+
"</font>",
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(
|
|
654
|
+
pj,
|
|
655
|
+
parameters[cfg.SELECTED_SUBJECTS],
|
|
656
|
+
selected_observations,
|
|
657
|
+
parameters[cfg.SELECTED_BEHAVIORS],
|
|
658
|
+
)
|
|
659
|
+
if not ok:
|
|
660
|
+
return False, msg
|
|
661
|
+
|
|
662
|
+
cursor = db_connector.cursor()
|
|
663
|
+
flag_ok = True
|
|
664
|
+
msg = ""
|
|
665
|
+
mem_command = ""
|
|
666
|
+
for obs_id in selected_observations:
|
|
667
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.LIVE:
|
|
668
|
+
out = ""
|
|
669
|
+
if parameters["time"] in (cfg.TIME_EVENTS, cfg.TIME_FULL_OBS):
|
|
670
|
+
cursor.execute(
|
|
671
|
+
(
|
|
672
|
+
"SELECT subject, behavior, start, stop, type, modifiers FROM aggregated_events "
|
|
673
|
+
"WHERE observation = ? "
|
|
674
|
+
"AND subject in ({}) "
|
|
675
|
+
"AND behavior in ({}) "
|
|
676
|
+
"ORDER BY start"
|
|
677
|
+
).format(
|
|
678
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
|
|
679
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
|
|
680
|
+
),
|
|
681
|
+
[
|
|
682
|
+
obs_id,
|
|
683
|
+
]
|
|
684
|
+
+ parameters[cfg.SELECTED_SUBJECTS]
|
|
685
|
+
+ parameters[cfg.SELECTED_BEHAVIORS],
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
else: # arbitrary 'time interval'
|
|
689
|
+
cursor.execute(
|
|
690
|
+
(
|
|
691
|
+
"SELECT subject, behavior, start, stop, type, modifiers FROM aggregated_events "
|
|
692
|
+
"WHERE observation = ? "
|
|
693
|
+
"AND (start BETWEEN ? AND ?) "
|
|
694
|
+
"AND subject in ({}) "
|
|
695
|
+
"AND behavior in ({}) "
|
|
696
|
+
"ORDER BY start"
|
|
697
|
+
).format(
|
|
698
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
|
|
699
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
|
|
700
|
+
),
|
|
701
|
+
[
|
|
702
|
+
obs_id,
|
|
703
|
+
float(parameters[cfg.START_TIME]),
|
|
704
|
+
float(parameters[cfg.END_TIME]),
|
|
705
|
+
]
|
|
706
|
+
+ parameters[cfg.SELECTED_SUBJECTS]
|
|
707
|
+
+ parameters[cfg.SELECTED_BEHAVIORS],
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
for idx, row in enumerate(cursor.fetchall()):
|
|
711
|
+
col1, col2 = subject_color(row["subject"])
|
|
712
|
+
if parameters["include modifiers"]:
|
|
713
|
+
modifiers_str = f"\n{row['modifiers'].replace('|', ', ')}"
|
|
714
|
+
else:
|
|
715
|
+
modifiers_str = ""
|
|
716
|
+
out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
|
|
717
|
+
idx=idx + 1,
|
|
718
|
+
start=util.seconds2time(row["start"]).replace(".", ","),
|
|
719
|
+
stop=util.seconds2time(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION).replace(
|
|
720
|
+
".", ","
|
|
721
|
+
),
|
|
722
|
+
col1=col1,
|
|
723
|
+
col2=col2,
|
|
724
|
+
subject=row["subject"],
|
|
725
|
+
behavior=row["behavior"],
|
|
726
|
+
modifiers=modifiers_str,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
file_name = Path(export_dir) / Path(util.safeFileName(obs_id)).with_suffix(".srt")
|
|
730
|
+
|
|
731
|
+
if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
|
|
732
|
+
mem_command = dialog.MessageDialog(
|
|
733
|
+
cfg.programName,
|
|
734
|
+
f"The file {file_name} already exists.",
|
|
735
|
+
[
|
|
736
|
+
cfg.OVERWRITE,
|
|
737
|
+
cfg.OVERWRITE_ALL,
|
|
738
|
+
cfg.SKIP,
|
|
739
|
+
cfg.SKIP_ALL,
|
|
740
|
+
cfg.CANCEL,
|
|
741
|
+
],
|
|
742
|
+
)
|
|
743
|
+
if mem_command == cfg.CANCEL:
|
|
744
|
+
return False, ""
|
|
745
|
+
if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
with file_name.open("w", encoding="utf-8") as f_out:
|
|
750
|
+
f_out.write(out)
|
|
751
|
+
except Exception:
|
|
752
|
+
flag_ok = False
|
|
753
|
+
msg += f"observation: {obs_id}\ngave the following error:\n{str(sys.exc_info()[1])}\n"
|
|
754
|
+
|
|
755
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
756
|
+
for nplayer in cfg.ALL_PLAYERS:
|
|
757
|
+
if not pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
|
|
758
|
+
continue
|
|
759
|
+
init = 0
|
|
760
|
+
for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
|
|
761
|
+
try:
|
|
762
|
+
end = init + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
|
|
763
|
+
except KeyError:
|
|
764
|
+
return (
|
|
765
|
+
False,
|
|
766
|
+
f"The length for media file {media_file} is not available",
|
|
767
|
+
)
|
|
768
|
+
out = ""
|
|
769
|
+
|
|
770
|
+
if parameters["time"] in (cfg.TIME_EVENTS, cfg.TIME_FULL_OBS):
|
|
771
|
+
cursor.execute(
|
|
772
|
+
(
|
|
773
|
+
"SELECT subject, behavior, start, stop, type, modifiers FROM aggregated_events "
|
|
774
|
+
"WHERE observation = ? "
|
|
775
|
+
"AND (start BETWEEN ? AND ?) "
|
|
776
|
+
"AND subject in ({}) "
|
|
777
|
+
"AND behavior in ({}) "
|
|
778
|
+
"ORDER BY start"
|
|
779
|
+
).format(
|
|
780
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
|
|
781
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
|
|
782
|
+
),
|
|
783
|
+
[
|
|
784
|
+
obs_id,
|
|
785
|
+
init,
|
|
786
|
+
end,
|
|
787
|
+
]
|
|
788
|
+
+ parameters[cfg.SELECTED_SUBJECTS]
|
|
789
|
+
+ parameters[cfg.SELECTED_BEHAVIORS],
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
else: # arbitrary 'time interval'
|
|
793
|
+
cursor.execute(
|
|
794
|
+
(
|
|
795
|
+
"SELECT subject, behavior, type, start, stop, modifiers FROM aggregated_events "
|
|
796
|
+
"WHERE observation = ? "
|
|
797
|
+
"AND (start BETWEEN ? AND ?) "
|
|
798
|
+
"AND (start BETWEEN ? AND ?) "
|
|
799
|
+
"AND subject in ({}) "
|
|
800
|
+
"AND behavior in ({}) "
|
|
801
|
+
"ORDER BY start"
|
|
802
|
+
).format(
|
|
803
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
|
|
804
|
+
",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
|
|
805
|
+
),
|
|
806
|
+
[
|
|
807
|
+
obs_id,
|
|
808
|
+
init,
|
|
809
|
+
end,
|
|
810
|
+
float(parameters[cfg.START_TIME]),
|
|
811
|
+
float(parameters[cfg.END_TIME]),
|
|
812
|
+
]
|
|
813
|
+
+ parameters[cfg.SELECTED_SUBJECTS]
|
|
814
|
+
+ parameters[cfg.SELECTED_BEHAVIORS],
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
for idx, row in enumerate(cursor.fetchall()):
|
|
818
|
+
col1, col2 = subject_color(row["subject"])
|
|
819
|
+
if parameters["include modifiers"]:
|
|
820
|
+
modifiers_str = f"\n{row['modifiers'].replace('|', ', ')}"
|
|
821
|
+
else:
|
|
822
|
+
modifiers_str = ""
|
|
823
|
+
|
|
824
|
+
out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
|
|
825
|
+
idx=idx + 1,
|
|
826
|
+
start=util.seconds2time(row["start"] - init).replace(".", ","),
|
|
827
|
+
stop=util.seconds2time(
|
|
828
|
+
(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION) - init
|
|
829
|
+
).replace(".", ","),
|
|
830
|
+
col1=col1,
|
|
831
|
+
col2=col2,
|
|
832
|
+
subject=row["subject"],
|
|
833
|
+
behavior=row["behavior"],
|
|
834
|
+
modifiers=modifiers_str,
|
|
835
|
+
)
|
|
836
|
+
file_name = Path(export_dir) / Path(Path(media_file).stem).with_suffix(".srt")
|
|
837
|
+
|
|
838
|
+
if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
|
|
839
|
+
mem_command = dialog.MessageDialog(
|
|
840
|
+
cfg.programName,
|
|
841
|
+
f"The file {file_name} already exists.",
|
|
842
|
+
[
|
|
843
|
+
cfg.OVERWRITE,
|
|
844
|
+
cfg.OVERWRITE_ALL,
|
|
845
|
+
cfg.SKIP,
|
|
846
|
+
cfg.SKIP_ALL,
|
|
847
|
+
cfg.CANCEL,
|
|
848
|
+
],
|
|
849
|
+
)
|
|
850
|
+
if mem_command == cfg.CANCEL:
|
|
851
|
+
return False, ""
|
|
852
|
+
if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
|
|
853
|
+
continue
|
|
854
|
+
try:
|
|
855
|
+
with file_name.open("w", encoding="utf-8") as f_out:
|
|
856
|
+
f_out.write(out)
|
|
857
|
+
except Exception:
|
|
858
|
+
flag_ok = False
|
|
859
|
+
msg += f"observation: {obs_id}\ngave the following error:\n{sys.exc_info()[1]}\n"
|
|
860
|
+
|
|
861
|
+
init = end
|
|
862
|
+
|
|
863
|
+
return flag_ok, msg
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def export_observations_list(pj: dict, selected_observations: list, file_name: str, output_format: str) -> bool:
|
|
867
|
+
"""
|
|
868
|
+
create file with a list of selected observations
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
pj (dict): project dictionary
|
|
872
|
+
selected_observations (list): list of observations to export
|
|
873
|
+
file_name (str): path of file to save list of observations
|
|
874
|
+
output_format (str): format output
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
bool: True of OK else False
|
|
878
|
+
"""
|
|
879
|
+
|
|
880
|
+
data = tablib.Dataset()
|
|
881
|
+
data.headers = [
|
|
882
|
+
"Observation id",
|
|
883
|
+
"Date",
|
|
884
|
+
"Description",
|
|
885
|
+
"Subjects",
|
|
886
|
+
"Media files/Live observation",
|
|
887
|
+
]
|
|
888
|
+
|
|
889
|
+
indep_var_header = []
|
|
890
|
+
if cfg.INDEPENDENT_VARIABLES in pj:
|
|
891
|
+
for idx in util.sorted_keys(pj[cfg.INDEPENDENT_VARIABLES]):
|
|
892
|
+
indep_var_header.append(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"])
|
|
893
|
+
data.headers.extend(indep_var_header)
|
|
894
|
+
|
|
895
|
+
for obs_id in selected_observations:
|
|
896
|
+
subjects_list = sorted(list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]])))
|
|
897
|
+
if "" in subjects_list:
|
|
898
|
+
subjects_list = [cfg.NO_FOCAL_SUBJECT] + subjects_list
|
|
899
|
+
subjects_list.remove("")
|
|
900
|
+
subjects = ", ".join(subjects_list)
|
|
901
|
+
|
|
902
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.LIVE:
|
|
903
|
+
media_files = ["Live observation"]
|
|
904
|
+
elif pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
905
|
+
media_files = []
|
|
906
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
907
|
+
for player in sorted(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE].keys()):
|
|
908
|
+
for media in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
|
|
909
|
+
media_files.append(f"#{player}: {media}")
|
|
910
|
+
|
|
911
|
+
# independent variables
|
|
912
|
+
indep_var = []
|
|
913
|
+
if cfg.INDEPENDENT_VARIABLES in pj[cfg.OBSERVATIONS][obs_id]:
|
|
914
|
+
for var_label in indep_var_header:
|
|
915
|
+
if var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
|
|
916
|
+
indep_var.append(pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label])
|
|
917
|
+
else:
|
|
918
|
+
indep_var.append("")
|
|
919
|
+
|
|
920
|
+
data.append(
|
|
921
|
+
[
|
|
922
|
+
obs_id,
|
|
923
|
+
pj[cfg.OBSERVATIONS][obs_id]["date"],
|
|
924
|
+
pj[cfg.OBSERVATIONS][obs_id]["description"],
|
|
925
|
+
subjects,
|
|
926
|
+
", ".join(media_files),
|
|
927
|
+
]
|
|
928
|
+
+ indep_var
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
if output_format in (cfg.TSV_EXT, cfg.CSV_EXT, cfg.HTML_EXT):
|
|
932
|
+
try:
|
|
933
|
+
with open(file_name, "wb") as f:
|
|
934
|
+
f.write(str.encode(data.export(output_format)))
|
|
935
|
+
except Exception:
|
|
936
|
+
return False
|
|
937
|
+
if output_format in [cfg.ODS_EXT, cfg.XLS_EXT, cfg.XLSX_EXT]:
|
|
938
|
+
try:
|
|
939
|
+
with open(file_name, "wb") as f:
|
|
940
|
+
f.write(data.export(output_format))
|
|
941
|
+
except Exception:
|
|
942
|
+
return False
|
|
943
|
+
|
|
944
|
+
return True
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) -> bool:
|
|
948
|
+
"""
|
|
949
|
+
set path from media files and path of images directory relative to the project directory
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
pj (dict): project
|
|
953
|
+
project_file_name (str): path of the project file
|
|
954
|
+
|
|
955
|
+
Returns:
|
|
956
|
+
bool: True if project changed else False
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
# chek if media and images dir are relative to project dir
|
|
960
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
961
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
962
|
+
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
963
|
+
try:
|
|
964
|
+
Path(img_dir).relative_to(Path(project_file_name).parent)
|
|
965
|
+
except ValueError:
|
|
966
|
+
if Path(img_dir).is_absolute() or not (Path(project_file_name).parent / Path(img_dir)).is_dir():
|
|
967
|
+
QMessageBox.critical(
|
|
968
|
+
None,
|
|
969
|
+
cfg.programName,
|
|
970
|
+
f"Observation <b>{obs_id}</b>:<br>the path of <b>{img_dir}</b> is not relative to <b>{project_file_name}</b>.",
|
|
971
|
+
)
|
|
972
|
+
return False
|
|
973
|
+
|
|
974
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
975
|
+
for n_player in cfg.ALL_PLAYERS:
|
|
976
|
+
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
977
|
+
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
978
|
+
try:
|
|
979
|
+
Path(media_file).relative_to(Path(project_file_name).parent)
|
|
980
|
+
except ValueError:
|
|
981
|
+
if Path(media_file).is_absolute() or not (Path(project_file_name).parent / Path(media_file)).is_file():
|
|
982
|
+
QMessageBox.critical(
|
|
983
|
+
None,
|
|
984
|
+
cfg.programName,
|
|
985
|
+
(
|
|
986
|
+
f"Observation <b>{obs_id}</b>:"
|
|
987
|
+
f"<br>the path of <b>{media_file}</b> is not relative to <b>{project_file_name}</b>"
|
|
988
|
+
),
|
|
989
|
+
)
|
|
990
|
+
return False
|
|
991
|
+
|
|
992
|
+
# set media path and image dir relative to project dir
|
|
993
|
+
flag_changed = False
|
|
994
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
995
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
996
|
+
new_dir_list = []
|
|
997
|
+
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
998
|
+
try:
|
|
999
|
+
new_dir_list.append(str(Path(img_dir).relative_to(Path(project_file_name).parent)))
|
|
1000
|
+
except ValueError:
|
|
1001
|
+
if not Path(img_dir).is_absolute() and (Path(project_file_name).parent / Path(img_dir)).is_dir():
|
|
1002
|
+
new_dir_list.append(img_dir)
|
|
1003
|
+
|
|
1004
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] != new_dir_list:
|
|
1005
|
+
flag_changed = True
|
|
1006
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] = new_dir_list
|
|
1007
|
+
|
|
1008
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
1009
|
+
for n_player in cfg.ALL_PLAYERS:
|
|
1010
|
+
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
1011
|
+
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
1012
|
+
try:
|
|
1013
|
+
p = str(Path(media_file).relative_to(Path(project_file_name).parent))
|
|
1014
|
+
except ValueError:
|
|
1015
|
+
if not Path(media_file).is_absolute() and (Path(project_file_name).parent / Path(media_file)).is_file():
|
|
1016
|
+
p = media_file
|
|
1017
|
+
if p != media_file:
|
|
1018
|
+
flag_changed = True
|
|
1019
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
|
|
1020
|
+
if cfg.MEDIA_INFO in pj[cfg.OBSERVATIONS][obs_id]:
|
|
1021
|
+
for info in [
|
|
1022
|
+
cfg.LENGTH,
|
|
1023
|
+
cfg.HAS_AUDIO,
|
|
1024
|
+
cfg.HAS_VIDEO,
|
|
1025
|
+
cfg.FPS,
|
|
1026
|
+
]:
|
|
1027
|
+
if (
|
|
1028
|
+
info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
|
|
1029
|
+
and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
|
|
1030
|
+
):
|
|
1031
|
+
# add new file path
|
|
1032
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
|
|
1033
|
+
cfg.MEDIA_INFO
|
|
1034
|
+
][info][media_file]
|
|
1035
|
+
# remove old path
|
|
1036
|
+
del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
|
|
1037
|
+
return flag_changed
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) -> bool:
|
|
1041
|
+
"""
|
|
1042
|
+
set path from media files and path of images directory relative to the project directory
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
pj (dict): project
|
|
1046
|
+
project_file_name (str): path of the project file
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
bool: True if project changed else False
|
|
1050
|
+
"""
|
|
1051
|
+
# chek if data paths are relative to project dir
|
|
1052
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1053
|
+
for _, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
|
|
1054
|
+
if cfg.FILE_PATH in v:
|
|
1055
|
+
try:
|
|
1056
|
+
Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent)
|
|
1057
|
+
except ValueError:
|
|
1058
|
+
# check if file is in project dir
|
|
1059
|
+
if Path(v[cfg.FILE_PATH]).is_absolute() or not (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
|
|
1060
|
+
QMessageBox.critical(
|
|
1061
|
+
None,
|
|
1062
|
+
cfg.programName,
|
|
1063
|
+
(
|
|
1064
|
+
f"Observation <b>{obs_id}</b>:"
|
|
1065
|
+
f"<br>the path of <b>{v[cfg.FILE_PATH]}</b> "
|
|
1066
|
+
f"is not relative to <b>{project_file_name}</b>."
|
|
1067
|
+
),
|
|
1068
|
+
)
|
|
1069
|
+
return False
|
|
1070
|
+
|
|
1071
|
+
flag_changed = False
|
|
1072
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1073
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
|
|
1074
|
+
continue
|
|
1075
|
+
for idx, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
|
|
1076
|
+
if cfg.FILE_PATH in v:
|
|
1077
|
+
try:
|
|
1078
|
+
p = str(Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent))
|
|
1079
|
+
except ValueError:
|
|
1080
|
+
# check if file is in project dir
|
|
1081
|
+
if not Path(v[cfg.FILE_PATH]).is_absolute() and (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
|
|
1082
|
+
p = v[cfg.FILE_PATH]
|
|
1083
|
+
|
|
1084
|
+
if p != v[cfg.FILE_PATH]:
|
|
1085
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx][cfg.FILE_PATH] = p
|
|
1086
|
+
flag_changed = True
|
|
1087
|
+
|
|
1088
|
+
return flag_changed
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def remove_data_files_path(pj: dict) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
remove path from data files
|
|
1094
|
+
|
|
1095
|
+
Args:
|
|
1096
|
+
pj (dict): project file
|
|
1097
|
+
|
|
1098
|
+
Returns:
|
|
1099
|
+
None
|
|
1100
|
+
"""
|
|
1101
|
+
|
|
1102
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1103
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
|
|
1104
|
+
continue
|
|
1105
|
+
if cfg.PLOT_DATA in pj[cfg.OBSERVATIONS][obs_id]:
|
|
1106
|
+
for idx in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA]:
|
|
1107
|
+
if "file_path" in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]:
|
|
1108
|
+
p = str(Path(pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]).name)
|
|
1109
|
+
if p != pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]:
|
|
1110
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"] = p
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
|
|
1114
|
+
"""
|
|
1115
|
+
remove path from media files and from images directory
|
|
1116
|
+
tested
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
pj (dict): project file
|
|
1120
|
+
|
|
1121
|
+
Returns:
|
|
1122
|
+
None
|
|
1123
|
+
"""
|
|
1124
|
+
|
|
1125
|
+
file_not_found = []
|
|
1126
|
+
# check if media and images dir
|
|
1127
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1128
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
1129
|
+
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
1130
|
+
if full_path(Path(img_dir).name, project_file_name) == "":
|
|
1131
|
+
file_not_found.append(img_dir)
|
|
1132
|
+
|
|
1133
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
1134
|
+
for n_player in cfg.ALL_PLAYERS:
|
|
1135
|
+
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
1136
|
+
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
1137
|
+
if full_path(Path(media_file).name, project_file_name) == "":
|
|
1138
|
+
file_not_found.append(media_file)
|
|
1139
|
+
|
|
1140
|
+
file_not_found = set(file_not_found)
|
|
1141
|
+
if file_not_found:
|
|
1142
|
+
if (
|
|
1143
|
+
dialog.MessageDialog(
|
|
1144
|
+
cfg.programName,
|
|
1145
|
+
(
|
|
1146
|
+
"Some media files / images directories will not be found after this operation:<br><br>"
|
|
1147
|
+
f"{',<br>'.join(file_not_found)}"
|
|
1148
|
+
"<br><br>Are you sure to continue?"
|
|
1149
|
+
),
|
|
1150
|
+
[cfg.YES, cfg.NO],
|
|
1151
|
+
)
|
|
1152
|
+
== cfg.NO
|
|
1153
|
+
):
|
|
1154
|
+
return False
|
|
1155
|
+
|
|
1156
|
+
flag_changed = False
|
|
1157
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1158
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
1159
|
+
new_img_dir_list = []
|
|
1160
|
+
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
1161
|
+
if img_dir != Path(img_dir).name:
|
|
1162
|
+
flag_changed = True
|
|
1163
|
+
new_img_dir_list.append(str(Path(img_dir).name))
|
|
1164
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] = new_img_dir_list
|
|
1165
|
+
|
|
1166
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
1167
|
+
for n_player in cfg.ALL_PLAYERS:
|
|
1168
|
+
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
1169
|
+
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
1170
|
+
p = Path(media_file).name
|
|
1171
|
+
if p != media_file:
|
|
1172
|
+
flag_changed = True
|
|
1173
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
|
|
1174
|
+
if cfg.MEDIA_INFO in pj[cfg.OBSERVATIONS][obs_id]:
|
|
1175
|
+
for info in [
|
|
1176
|
+
cfg.LENGTH,
|
|
1177
|
+
cfg.HAS_AUDIO,
|
|
1178
|
+
cfg.HAS_VIDEO,
|
|
1179
|
+
cfg.FPS,
|
|
1180
|
+
]:
|
|
1181
|
+
if (
|
|
1182
|
+
info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
|
|
1183
|
+
and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
|
|
1184
|
+
):
|
|
1185
|
+
# add new file path
|
|
1186
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
|
|
1187
|
+
cfg.MEDIA_INFO
|
|
1188
|
+
][info][media_file]
|
|
1189
|
+
# remove old path
|
|
1190
|
+
del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
|
|
1191
|
+
|
|
1192
|
+
return flag_changed
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def full_path(path: str, project_file_name: str) -> str:
|
|
1196
|
+
"""
|
|
1197
|
+
returns the media/data full path or the images directory full path
|
|
1198
|
+
add path of BORIS project if media/data/pictures dir with relative path
|
|
1199
|
+
|
|
1200
|
+
Args:
|
|
1201
|
+
path (str): file path or images directory path
|
|
1202
|
+
project_file_name (str): project file name
|
|
1203
|
+
|
|
1204
|
+
Returns:
|
|
1205
|
+
str: full path
|
|
1206
|
+
"""
|
|
1207
|
+
|
|
1208
|
+
source_path = Path(path)
|
|
1209
|
+
if source_path.exists():
|
|
1210
|
+
return str(source_path)
|
|
1211
|
+
else:
|
|
1212
|
+
# check relative path (to project path)
|
|
1213
|
+
project_path = Path(project_file_name)
|
|
1214
|
+
if (project_path.parent / source_path).exists():
|
|
1215
|
+
return str(project_path.parent / source_path)
|
|
1216
|
+
else:
|
|
1217
|
+
return ""
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def observed_interval(observation: dict) -> Tuple[dec, dec]:
|
|
1221
|
+
"""
|
|
1222
|
+
Observed interval for observation
|
|
1223
|
+
|
|
1224
|
+
Args:
|
|
1225
|
+
observation (dict): observation dictionary
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
Tuple of 2 Decimals: time of first observed event, time of last observed event
|
|
1229
|
+
"""
|
|
1230
|
+
if not observation[cfg.EVENTS]:
|
|
1231
|
+
return (dec("0.0"), dec("0.0"))
|
|
1232
|
+
|
|
1233
|
+
if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
1234
|
+
event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
|
|
1235
|
+
|
|
1236
|
+
return (
|
|
1237
|
+
min(event_timestamp),
|
|
1238
|
+
max(event_timestamp),
|
|
1239
|
+
)
|
|
1240
|
+
if observation[cfg.TYPE] == cfg.IMAGES:
|
|
1241
|
+
events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
|
|
1242
|
+
# test if indexes contain NA
|
|
1243
|
+
try:
|
|
1244
|
+
dec(min(events))
|
|
1245
|
+
return (dec(min(events)), dec(max(events)))
|
|
1246
|
+
except Exception:
|
|
1247
|
+
return (dec("NaN"), dec("NaN"))
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
|
|
1251
|
+
"""
|
|
1252
|
+
returns events with status (START/STOP or POINT)
|
|
1253
|
+
|
|
1254
|
+
Args:
|
|
1255
|
+
events (list): list of events
|
|
1256
|
+
|
|
1257
|
+
Returns:
|
|
1258
|
+
list: list of events with type (POINT or STATE)
|
|
1259
|
+
"""
|
|
1260
|
+
|
|
1261
|
+
state_events_list = util.state_behavior_codes(ethogram)
|
|
1262
|
+
|
|
1263
|
+
events_flagged: list = []
|
|
1264
|
+
for idx, event in enumerate(events):
|
|
1265
|
+
_, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
|
|
1266
|
+
|
|
1267
|
+
# check if code is state
|
|
1268
|
+
if code in state_events_list:
|
|
1269
|
+
# how many code before with same subject?
|
|
1270
|
+
if (
|
|
1271
|
+
len(
|
|
1272
|
+
[
|
|
1273
|
+
x[cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
1274
|
+
for idx1, x in enumerate(events)
|
|
1275
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == code
|
|
1276
|
+
and idx1 < idx
|
|
1277
|
+
and x[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1278
|
+
and x[cfg.EVENT_MODIFIER_FIELD_IDX] == modifier
|
|
1279
|
+
]
|
|
1280
|
+
)
|
|
1281
|
+
% 2
|
|
1282
|
+
): # test if odd
|
|
1283
|
+
flag = cfg.STOP
|
|
1284
|
+
else:
|
|
1285
|
+
flag = cfg.START
|
|
1286
|
+
else:
|
|
1287
|
+
flag = cfg.POINT
|
|
1288
|
+
|
|
1289
|
+
# no frame_index
|
|
1290
|
+
if obs_type == cfg.MEDIA and len(event) == 5:
|
|
1291
|
+
events_flagged.append(
|
|
1292
|
+
tuple(event)
|
|
1293
|
+
+ (
|
|
1294
|
+
cfg.NA,
|
|
1295
|
+
flag,
|
|
1296
|
+
)
|
|
1297
|
+
)
|
|
1298
|
+
else:
|
|
1299
|
+
events_flagged.append(tuple(event) + (flag,))
|
|
1300
|
+
|
|
1301
|
+
return events_flagged
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
|
|
1305
|
+
"""
|
|
1306
|
+
extract unique subjects present in observations list
|
|
1307
|
+
|
|
1308
|
+
return: list
|
|
1309
|
+
"""
|
|
1310
|
+
|
|
1311
|
+
observed_subjects = []
|
|
1312
|
+
|
|
1313
|
+
# extract events from selected observations
|
|
1314
|
+
for events in [pj[cfg.OBSERVATIONS][x][cfg.EVENTS] for x in pj[cfg.OBSERVATIONS] if x in selected_observations]:
|
|
1315
|
+
for event in events:
|
|
1316
|
+
observed_subjects.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
|
|
1317
|
+
|
|
1318
|
+
# remove duplicate
|
|
1319
|
+
return list(set(observed_subjects))
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
def open_project_json(project_file_name: str) -> tuple:
|
|
1323
|
+
"""
|
|
1324
|
+
open BORIS project file in json format or GZ compressed json format
|
|
1325
|
+
|
|
1326
|
+
Args:
|
|
1327
|
+
projectFileName (str): path of project
|
|
1328
|
+
|
|
1329
|
+
Returns:
|
|
1330
|
+
str: project path
|
|
1331
|
+
bool: True if project changed
|
|
1332
|
+
dict: BORIS project
|
|
1333
|
+
str: message
|
|
1334
|
+
"""
|
|
1335
|
+
|
|
1336
|
+
logging.debug(f"open_project_json function: {project_file_name}")
|
|
1337
|
+
|
|
1338
|
+
projectChanged: bool = False
|
|
1339
|
+
msg: str = ""
|
|
1340
|
+
|
|
1341
|
+
if not Path(project_file_name).is_file():
|
|
1342
|
+
return (
|
|
1343
|
+
project_file_name,
|
|
1344
|
+
projectChanged,
|
|
1345
|
+
{"error": f"File {project_file_name} not found"},
|
|
1346
|
+
msg,
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
try:
|
|
1350
|
+
if project_file_name.endswith(".boris.gz"):
|
|
1351
|
+
file_in = gzip.open(project_file_name, mode="rt", encoding="utf-8")
|
|
1352
|
+
else:
|
|
1353
|
+
file_in = open(project_file_name, "r")
|
|
1354
|
+
file_content = file_in.read()
|
|
1355
|
+
except PermissionError:
|
|
1356
|
+
return (
|
|
1357
|
+
project_file_name,
|
|
1358
|
+
projectChanged,
|
|
1359
|
+
{"error": f"File {project_file_name}: Permission denied"},
|
|
1360
|
+
msg,
|
|
1361
|
+
)
|
|
1362
|
+
except Exception:
|
|
1363
|
+
return (
|
|
1364
|
+
project_file_name,
|
|
1365
|
+
projectChanged,
|
|
1366
|
+
{"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
|
|
1367
|
+
msg,
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
try:
|
|
1371
|
+
pj = json.loads(file_content)
|
|
1372
|
+
except json.decoder.JSONDecodeError:
|
|
1373
|
+
return (
|
|
1374
|
+
project_file_name,
|
|
1375
|
+
projectChanged,
|
|
1376
|
+
{"error": "This project file seems corrupted"},
|
|
1377
|
+
msg,
|
|
1378
|
+
)
|
|
1379
|
+
except Exception:
|
|
1380
|
+
return (
|
|
1381
|
+
project_file_name,
|
|
1382
|
+
projectChanged,
|
|
1383
|
+
{"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
|
|
1384
|
+
msg,
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
# transform time to decimal
|
|
1388
|
+
pj = util.convert_time_to_decimal(pj)
|
|
1389
|
+
|
|
1390
|
+
# add coding_map key to old project files
|
|
1391
|
+
if "coding_map" not in pj:
|
|
1392
|
+
pj["coding_map"] = {}
|
|
1393
|
+
projectChanged = True
|
|
1394
|
+
|
|
1395
|
+
# add subject description
|
|
1396
|
+
if cfg.PROJECT_VERSION in pj:
|
|
1397
|
+
for idx in [x for x in pj[cfg.SUBJECTS]]:
|
|
1398
|
+
if "description" not in pj[cfg.SUBJECTS][idx]:
|
|
1399
|
+
pj[cfg.SUBJECTS][idx]["description"] = ""
|
|
1400
|
+
projectChanged = True
|
|
1401
|
+
|
|
1402
|
+
# check if project file version is newer than current BORIS project file version
|
|
1403
|
+
if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(version.__version__):
|
|
1404
|
+
return (
|
|
1405
|
+
project_file_name,
|
|
1406
|
+
projectChanged,
|
|
1407
|
+
{
|
|
1408
|
+
"error": (
|
|
1409
|
+
"This project file was created with a more recent version of BORIS.<br>"
|
|
1410
|
+
f"You must update BORIS to <b>v. >= {pj[cfg.PROJECT_VERSION]}</b> to open this project"
|
|
1411
|
+
)
|
|
1412
|
+
},
|
|
1413
|
+
msg,
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
# check if old version v. 0 *.obs
|
|
1417
|
+
if cfg.PROJECT_VERSION not in pj:
|
|
1418
|
+
# convert VIDEO, AUDIO -> MEDIA
|
|
1419
|
+
pj[cfg.PROJECT_VERSION] = cfg.project_format_version
|
|
1420
|
+
projectChanged = True
|
|
1421
|
+
|
|
1422
|
+
for obs in [x for x in pj[cfg.OBSERVATIONS]]:
|
|
1423
|
+
# remove 'replace audio' key
|
|
1424
|
+
if "replace audio" in pj[cfg.OBSERVATIONS][obs]:
|
|
1425
|
+
del pj[cfg.OBSERVATIONS][obs]["replace audio"]
|
|
1426
|
+
|
|
1427
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] in ["VIDEO", "AUDIO"]:
|
|
1428
|
+
pj[cfg.OBSERVATIONS][obs][cfg.TYPE] = cfg.MEDIA
|
|
1429
|
+
|
|
1430
|
+
# convert old media list in new one
|
|
1431
|
+
d1: dict = {}
|
|
1432
|
+
if len(pj[cfg.OBSERVATIONS][obs][cfg.FILE]):
|
|
1433
|
+
d1 = {cfg.PLAYER1: [pj[cfg.OBSERVATIONS][obs][cfg.FILE][0]]}
|
|
1434
|
+
|
|
1435
|
+
if len(pj[cfg.OBSERVATIONS][obs][cfg.FILE]) == 2:
|
|
1436
|
+
d1[cfg.PLAYER2] = [pj[cfg.OBSERVATIONS][obs][cfg.FILE][1]]
|
|
1437
|
+
|
|
1438
|
+
pj[cfg.OBSERVATIONS][obs][cfg.FILE] = d1
|
|
1439
|
+
|
|
1440
|
+
# convert VIDEO, AUDIO -> MEDIA
|
|
1441
|
+
for idx in [x for x in pj[cfg.SUBJECTS]]:
|
|
1442
|
+
key, name = pj[cfg.SUBJECTS][idx]
|
|
1443
|
+
pj[cfg.SUBJECTS][idx] = {"key": key, "name": name, "description": ""}
|
|
1444
|
+
|
|
1445
|
+
msg = (
|
|
1446
|
+
f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
|
|
1447
|
+
"Choose a new file name for saving it."
|
|
1448
|
+
)
|
|
1449
|
+
project_file_name = ""
|
|
1450
|
+
|
|
1451
|
+
# update modifiers to JSON format
|
|
1452
|
+
|
|
1453
|
+
# check if project format version < 4 (modifiers were str)
|
|
1454
|
+
project_lowerthan4 = False
|
|
1455
|
+
if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) < util.versiontuple("4.0"):
|
|
1456
|
+
for idx in pj[cfg.ETHOGRAM]:
|
|
1457
|
+
if pj[cfg.ETHOGRAM][idx]["modifiers"]:
|
|
1458
|
+
if isinstance(pj[cfg.ETHOGRAM][idx]["modifiers"], str):
|
|
1459
|
+
project_lowerthan4 = True
|
|
1460
|
+
modif_set_list = pj[cfg.ETHOGRAM][idx]["modifiers"].split("|")
|
|
1461
|
+
modif_set_dict = {}
|
|
1462
|
+
for modif_set in modif_set_list:
|
|
1463
|
+
modif_set_dict[str(len(modif_set_dict))] = {
|
|
1464
|
+
"name": "",
|
|
1465
|
+
"type": cfg.SINGLE_SELECTION,
|
|
1466
|
+
"values": modif_set.split(","),
|
|
1467
|
+
}
|
|
1468
|
+
pj[cfg.ETHOGRAM][idx]["modifiers"] = dict(modif_set_dict)
|
|
1469
|
+
else:
|
|
1470
|
+
pj[cfg.ETHOGRAM][idx]["modifiers"] = {}
|
|
1471
|
+
|
|
1472
|
+
if not project_lowerthan4:
|
|
1473
|
+
msg = "The project version was updated from {} to {}".format(pj[cfg.PROJECT_VERSION], cfg.project_format_version)
|
|
1474
|
+
pj[cfg.PROJECT_VERSION] = cfg.project_format_version
|
|
1475
|
+
projectChanged = True
|
|
1476
|
+
|
|
1477
|
+
# check if behavioral categories are stored as a list
|
|
1478
|
+
if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
|
|
1479
|
+
if isinstance(pj[cfg.BEHAVIORAL_CATEGORIES_CONF], list):
|
|
1480
|
+
# convert to dict
|
|
1481
|
+
pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {str(idx): {"name": bc} for idx, bc in enumerate(pj[cfg.BEHAVIORAL_CATEGORIES_CONF])}
|
|
1482
|
+
logging.info("Behavioral categories was converted from a list to a dictionary")
|
|
1483
|
+
projectChanged = True
|
|
1484
|
+
else:
|
|
1485
|
+
pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict()
|
|
1486
|
+
projectChanged = True
|
|
1487
|
+
|
|
1488
|
+
# add category key if not found
|
|
1489
|
+
for idx in pj[cfg.ETHOGRAM]:
|
|
1490
|
+
if cfg.BEHAVIOR_CATEGORY not in pj[cfg.ETHOGRAM][idx]:
|
|
1491
|
+
pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] = ""
|
|
1492
|
+
|
|
1493
|
+
# if one file is present in player #1 -> set "media_info" key with value of media_file_info
|
|
1494
|
+
for obs in pj[cfg.OBSERVATIONS]:
|
|
1495
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] in [cfg.MEDIA] and cfg.MEDIA_INFO not in pj[cfg.OBSERVATIONS][obs]:
|
|
1496
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
|
|
1497
|
+
cfg.LENGTH: {},
|
|
1498
|
+
cfg.FPS: {},
|
|
1499
|
+
cfg.HAS_VIDEO: {},
|
|
1500
|
+
cfg.HAS_AUDIO: {},
|
|
1501
|
+
}
|
|
1502
|
+
for player in (cfg.PLAYER1, cfg.PLAYER2):
|
|
1503
|
+
# fix bug Anne Maijer 2017-07-17
|
|
1504
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.FILE] == []:
|
|
1505
|
+
pj[cfg.OBSERVATIONS][obs][cfg.FILE] = {"1": [], "2": []}
|
|
1506
|
+
|
|
1507
|
+
for media_file_path in pj[cfg.OBSERVATIONS][obs]["file"][player]:
|
|
1508
|
+
# FIX: ffmpeg path
|
|
1509
|
+
ret, ffmpeg_bin = util.check_ffmpeg_path()
|
|
1510
|
+
if not ret:
|
|
1511
|
+
return (
|
|
1512
|
+
project_file_name,
|
|
1513
|
+
projectChanged,
|
|
1514
|
+
{"error": "FFmpeg path not found"},
|
|
1515
|
+
"",
|
|
1516
|
+
)
|
|
1517
|
+
else:
|
|
1518
|
+
ffmpeg_bin = msg
|
|
1519
|
+
|
|
1520
|
+
r = util.accurate_media_analysis(ffmpeg_bin, media_file_path)
|
|
1521
|
+
|
|
1522
|
+
if "duration" in r and r["duration"]:
|
|
1523
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.LENGTH][media_file_path] = float(r["duration"])
|
|
1524
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][media_file_path] = float(r["fps"])
|
|
1525
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][media_file_path] = r["has_video"]
|
|
1526
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path] = r["has_audio"]
|
|
1527
|
+
projectChanged = True
|
|
1528
|
+
else: # file path not found
|
|
1529
|
+
if (
|
|
1530
|
+
cfg.MEDIA_FILE_INFO in pj[cfg.OBSERVATIONS][obs]
|
|
1531
|
+
and len(pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO]) == 1
|
|
1532
|
+
and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER1]) == 1
|
|
1533
|
+
and len(pj[cfg.OBSERVATIONS][obs][cfg.FILE][cfg.PLAYER2]) == 0
|
|
1534
|
+
):
|
|
1535
|
+
media_md5_key = list(pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO].keys())[0]
|
|
1536
|
+
# duration
|
|
1537
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
|
|
1538
|
+
cfg.LENGTH: {
|
|
1539
|
+
media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
projectChanged = True
|
|
1543
|
+
|
|
1544
|
+
# FPS
|
|
1545
|
+
if "nframe" in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]:
|
|
1546
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
|
|
1547
|
+
media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["nframe"]
|
|
1548
|
+
/ (pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000)
|
|
1549
|
+
}
|
|
1550
|
+
else:
|
|
1551
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {media_file_path: 0}
|
|
1552
|
+
|
|
1553
|
+
# update project to v.7 for time offset second player
|
|
1554
|
+
project_lowerthan7 = False
|
|
1555
|
+
for obs in pj[cfg.OBSERVATIONS]:
|
|
1556
|
+
if "time offset second player" in pj[cfg.OBSERVATIONS][obs]:
|
|
1557
|
+
if cfg.MEDIA_INFO not in pj[cfg.OBSERVATIONS][obs]:
|
|
1558
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {}
|
|
1559
|
+
if cfg.OFFSET not in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO]:
|
|
1560
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET] = {}
|
|
1561
|
+
for player in pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
|
|
1562
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET][player] = 0.0
|
|
1563
|
+
if pj[cfg.OBSERVATIONS][obs]["time offset second player"]:
|
|
1564
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(pj[cfg.OBSERVATIONS][obs]["time offset second player"])
|
|
1565
|
+
|
|
1566
|
+
del pj[cfg.OBSERVATIONS][obs]["time offset second player"]
|
|
1567
|
+
project_lowerthan7 = True
|
|
1568
|
+
|
|
1569
|
+
msg = (
|
|
1570
|
+
f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
|
|
1571
|
+
f"Please note that this new version will NOT be compatible with previous BORIS versions "
|
|
1572
|
+
f"(< v. {cfg.project_format_version}).<br>"
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
projectChanged = True
|
|
1576
|
+
|
|
1577
|
+
if project_lowerthan7:
|
|
1578
|
+
msg = f"The project was updated to the current project version ({cfg.project_format_version})."
|
|
1579
|
+
|
|
1580
|
+
try:
|
|
1581
|
+
old_project_file_name = project_file_name.replace(".boris", f".v{pj['project_format_version']}.boris")
|
|
1582
|
+
copyfile(project_file_name, old_project_file_name)
|
|
1583
|
+
msg += f"\n\nThe old file project was saved as {old_project_file_name}"
|
|
1584
|
+
except Exception:
|
|
1585
|
+
QMessageBox.critical(cfg.programName, f"Error saving old project to {old_project_file_name}")
|
|
1586
|
+
|
|
1587
|
+
pj[cfg.PROJECT_VERSION] = cfg.project_format_version
|
|
1588
|
+
|
|
1589
|
+
# sort events by time asc
|
|
1590
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1591
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
|
|
1592
|
+
# sort events list using the first 3 items (time, subject, behavior)
|
|
1593
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
1594
|
+
|
|
1595
|
+
return project_file_name, projectChanged, pj, msg
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
def event_type(code: str, ethogram: dict) -> str | None:
|
|
1599
|
+
"""
|
|
1600
|
+
returns type of event for behavior code
|
|
1601
|
+
|
|
1602
|
+
Args:
|
|
1603
|
+
ethogram (dict); ethogram of project
|
|
1604
|
+
code (str): behavior code
|
|
1605
|
+
|
|
1606
|
+
Returns:
|
|
1607
|
+
str: behavior type
|
|
1608
|
+
"""
|
|
1609
|
+
|
|
1610
|
+
for idx in ethogram:
|
|
1611
|
+
if ethogram[idx][cfg.BEHAVIOR_CODE] == code:
|
|
1612
|
+
return ethogram[idx][cfg.TYPE]
|
|
1613
|
+
return None
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: dec) -> list:
|
|
1617
|
+
"""
|
|
1618
|
+
fix unpaired state events in observation
|
|
1619
|
+
|
|
1620
|
+
Args:
|
|
1621
|
+
ethogram (dict): ethogram dictionary
|
|
1622
|
+
observation (dict): observation dictionary
|
|
1623
|
+
fix_at_time (Decimal): time to fix the unpaired events
|
|
1624
|
+
|
|
1625
|
+
Returns:
|
|
1626
|
+
list: list of events with state events fixed
|
|
1627
|
+
"""
|
|
1628
|
+
|
|
1629
|
+
closing_events_to_add: list = []
|
|
1630
|
+
subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
|
|
1631
|
+
ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
1632
|
+
|
|
1633
|
+
for subject in sorted(set(subjects)):
|
|
1634
|
+
behaviors: list = [
|
|
1635
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1636
|
+
]
|
|
1637
|
+
|
|
1638
|
+
for behavior in sorted(set(behaviors)):
|
|
1639
|
+
if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
|
|
1640
|
+
lst, memTime = [], {}
|
|
1641
|
+
for event in [
|
|
1642
|
+
event
|
|
1643
|
+
for event in observation[cfg.EVENTS]
|
|
1644
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1645
|
+
]:
|
|
1646
|
+
behav_modif = [
|
|
1647
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
1648
|
+
event[cfg.EVENT_MODIFIER_FIELD_IDX],
|
|
1649
|
+
]
|
|
1650
|
+
|
|
1651
|
+
if behav_modif in lst:
|
|
1652
|
+
lst.remove(behav_modif)
|
|
1653
|
+
del memTime[str(behav_modif)]
|
|
1654
|
+
else:
|
|
1655
|
+
lst.append(behav_modif)
|
|
1656
|
+
memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
|
|
1657
|
+
|
|
1658
|
+
for event in lst:
|
|
1659
|
+
last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
|
|
1660
|
+
|
|
1661
|
+
closing_events_to_add.append(
|
|
1662
|
+
[
|
|
1663
|
+
last_event_time + dec("0.001"),
|
|
1664
|
+
subject,
|
|
1665
|
+
behavior,
|
|
1666
|
+
event[1], # modifiers
|
|
1667
|
+
"Event automatically added by the fix unpaired state events function",
|
|
1668
|
+
cfg.NA, # frame index
|
|
1669
|
+
]
|
|
1670
|
+
)
|
|
1671
|
+
|
|
1672
|
+
return closing_events_to_add
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -> list:
|
|
1676
|
+
"""
|
|
1677
|
+
fix unpaired state events in events list
|
|
1678
|
+
|
|
1679
|
+
Args:
|
|
1680
|
+
ethogram (dict): ethogram dictionary
|
|
1681
|
+
events (list): list of events
|
|
1682
|
+
fix_at_time (Decimal): time to fix the unpaired events
|
|
1683
|
+
|
|
1684
|
+
Returns:
|
|
1685
|
+
list: list of events with state events fixed
|
|
1686
|
+
"""
|
|
1687
|
+
|
|
1688
|
+
logging.debug("fix_unpaired_state_events2 function")
|
|
1689
|
+
|
|
1690
|
+
closing_events_to_add: list = []
|
|
1691
|
+
subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
|
|
1692
|
+
ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
1693
|
+
|
|
1694
|
+
for subject in sorted(set(subjects)):
|
|
1695
|
+
behaviors: list = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in events if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject]
|
|
1696
|
+
|
|
1697
|
+
for behavior in sorted(set(behaviors)):
|
|
1698
|
+
if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
|
|
1699
|
+
lst: list = []
|
|
1700
|
+
memTime: dict = {}
|
|
1701
|
+
for event in [
|
|
1702
|
+
event
|
|
1703
|
+
for event in events
|
|
1704
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1705
|
+
]:
|
|
1706
|
+
behav_modif = [
|
|
1707
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
1708
|
+
event[cfg.EVENT_MODIFIER_FIELD_IDX],
|
|
1709
|
+
]
|
|
1710
|
+
|
|
1711
|
+
if behav_modif in lst:
|
|
1712
|
+
lst.remove(behav_modif)
|
|
1713
|
+
del memTime[str(behav_modif)]
|
|
1714
|
+
else:
|
|
1715
|
+
lst.append(behav_modif)
|
|
1716
|
+
memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
|
|
1717
|
+
|
|
1718
|
+
for event in lst:
|
|
1719
|
+
last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
|
|
1720
|
+
|
|
1721
|
+
closing_events_to_add.append(
|
|
1722
|
+
[
|
|
1723
|
+
# last_event_time + dec("0.001"),
|
|
1724
|
+
last_event_time,
|
|
1725
|
+
subject,
|
|
1726
|
+
behavior,
|
|
1727
|
+
event[1], # modifiers
|
|
1728
|
+
"Event automatically added by the fix unpaired state events function",
|
|
1729
|
+
cfg.NA, # frame index
|
|
1730
|
+
]
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
return closing_events_to_add
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def has_audio(observation: dict, media_file_path: str) -> bool:
|
|
1737
|
+
"""
|
|
1738
|
+
check if media file has audio
|
|
1739
|
+
"""
|
|
1740
|
+
if cfg.HAS_AUDIO in observation[cfg.MEDIA_INFO]:
|
|
1741
|
+
if media_file_path in observation[cfg.MEDIA_INFO][cfg.HAS_AUDIO]:
|
|
1742
|
+
if observation[cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path]:
|
|
1743
|
+
return True
|
|
1744
|
+
return False
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
def explore_project(self) -> None:
|
|
1748
|
+
"""
|
|
1749
|
+
search various elements (subjects, behaviors, modifiers, comments) in all observations
|
|
1750
|
+
"""
|
|
1751
|
+
|
|
1752
|
+
def double_click_explore_project(obs_id, event_idx):
|
|
1753
|
+
"""
|
|
1754
|
+
manage double-click on tablewidget of explore project results
|
|
1755
|
+
"""
|
|
1756
|
+
observation_operations.load_observation(self, obs_id, cfg.VIEW)
|
|
1757
|
+
|
|
1758
|
+
self.tv_events.selectRow(event_idx - 1)
|
|
1759
|
+
index = self.tv_events.model().index(event_idx - 1, 0)
|
|
1760
|
+
self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
|
|
1761
|
+
# self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
|
|
1762
|
+
|
|
1763
|
+
elements_list = ("Subject", "Behavior", "Modifier", "Comment")
|
|
1764
|
+
elements = []
|
|
1765
|
+
for element in elements_list:
|
|
1766
|
+
elements.append(("le", element))
|
|
1767
|
+
elements.append(("cb", "Case sensitive", False))
|
|
1768
|
+
|
|
1769
|
+
explore_dlg = dialog.Input_dialog(
|
|
1770
|
+
label_caption="Search in all observations",
|
|
1771
|
+
elements_list=elements,
|
|
1772
|
+
title="Explore project",
|
|
1773
|
+
)
|
|
1774
|
+
explore_dlg.pbOK.setText("Find")
|
|
1775
|
+
if not explore_dlg.exec_():
|
|
1776
|
+
return
|
|
1777
|
+
|
|
1778
|
+
nb_fields: int = 0
|
|
1779
|
+
results: list = []
|
|
1780
|
+
for element in elements_list:
|
|
1781
|
+
nb_fields += explore_dlg.elements[element].text() != ""
|
|
1782
|
+
|
|
1783
|
+
for obs_id in sorted(self.pj[cfg.OBSERVATIONS]):
|
|
1784
|
+
for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
1785
|
+
nb_results = 0
|
|
1786
|
+
for text, idx in (
|
|
1787
|
+
(explore_dlg.elements["Subject"].text(), cfg.EVENT_SUBJECT_FIELD_IDX),
|
|
1788
|
+
(explore_dlg.elements["Behavior"].text(), cfg.EVENT_BEHAVIOR_FIELD_IDX),
|
|
1789
|
+
(explore_dlg.elements["Modifier"].text(), cfg.EVENT_MODIFIER_FIELD_IDX),
|
|
1790
|
+
(explore_dlg.elements["Comment"].text(), cfg.EVENT_COMMENT_FIELD_IDX),
|
|
1791
|
+
):
|
|
1792
|
+
if text:
|
|
1793
|
+
if any(
|
|
1794
|
+
(
|
|
1795
|
+
(explore_dlg.elements["Case sensitive"].isChecked() and text in event[idx]),
|
|
1796
|
+
(not explore_dlg.elements["Case sensitive"].isChecked() and text.upper() in event[idx].upper()),
|
|
1797
|
+
)
|
|
1798
|
+
):
|
|
1799
|
+
nb_results += 1
|
|
1800
|
+
|
|
1801
|
+
if nb_results == nb_fields:
|
|
1802
|
+
results.append((obs_id, event_idx + 1))
|
|
1803
|
+
|
|
1804
|
+
if results:
|
|
1805
|
+
self.results_dialog = dialog.View_explore_project_results()
|
|
1806
|
+
self.results_dialog.setWindowTitle("Explore project results")
|
|
1807
|
+
self.results_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
1808
|
+
self.results_dialog.double_click_signal.connect(double_click_explore_project)
|
|
1809
|
+
txt = f"<b>{len(results)}</b> events"
|
|
1810
|
+
txt2 = ""
|
|
1811
|
+
for element in elements_list:
|
|
1812
|
+
if explore_dlg.elements[element].text():
|
|
1813
|
+
txt2 += f"<b>{explore_dlg.elements[element].text()}</b> in {element}<br>"
|
|
1814
|
+
if txt2:
|
|
1815
|
+
txt += " for<br>"
|
|
1816
|
+
self.results_dialog.lb.setText(txt + txt2)
|
|
1817
|
+
self.results_dialog.tw.setColumnCount(2)
|
|
1818
|
+
self.results_dialog.tw.setRowCount(len(results))
|
|
1819
|
+
self.results_dialog.tw.setHorizontalHeaderLabels(["Observation id", "row index"])
|
|
1820
|
+
|
|
1821
|
+
for row, result in enumerate(results):
|
|
1822
|
+
for i in range(0, 2):
|
|
1823
|
+
self.results_dialog.tw.setItem(row, i, QTableWidgetItem(str(result[i])))
|
|
1824
|
+
self.results_dialog.tw.item(row, i).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
|
1825
|
+
|
|
1826
|
+
self.results_dialog.show()
|
|
1827
|
+
|
|
1828
|
+
else:
|
|
1829
|
+
QMessageBox.information(self, cfg.programName, "No events found")
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
def project2dataframe(pj: dict, observations_list: list = []) -> Tuple[str, pd.DataFrame]:
|
|
1833
|
+
"""
|
|
1834
|
+
returns a pandas dataframe containing observations data
|
|
1835
|
+
"""
|
|
1836
|
+
# print(pj.keys())
|
|
1837
|
+
|
|
1838
|
+
# print(pj["independent_variables"])
|
|
1839
|
+
|
|
1840
|
+
# indep_var = [pj["independent_variables"][idx]["label"] for idx in pj["independent_variables"]]
|
|
1841
|
+
|
|
1842
|
+
indep_variables = dict(
|
|
1843
|
+
[(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"], pj[cfg.INDEPENDENT_VARIABLES][idx]["type"]) for idx in pj[cfg.INDEPENDENT_VARIABLES]]
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
# print()
|
|
1847
|
+
# print(f"{indep_variables=}")
|
|
1848
|
+
|
|
1849
|
+
# n_max_set_modifiers = max([len(pj["behaviors_conf"][behavior_id]["modifiers"]) for behavior_id in pj["behaviors_conf"]])
|
|
1850
|
+
|
|
1851
|
+
# behavioral_categories
|
|
1852
|
+
behavioral_category = dict(
|
|
1853
|
+
[(pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE], pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]) for x in pj[cfg.ETHOGRAM]]
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
# print(f"{pj["behaviors_conf"]=}")
|
|
1857
|
+
|
|
1858
|
+
# check all modifiers
|
|
1859
|
+
all_modifier_sets: list = []
|
|
1860
|
+
for behavior_id in pj[cfg.ETHOGRAM]:
|
|
1861
|
+
modifier_names: list = []
|
|
1862
|
+
set_count = 0
|
|
1863
|
+
if pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS] == "":
|
|
1864
|
+
continue
|
|
1865
|
+
for modifier in pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS].values():
|
|
1866
|
+
if modifier["name"]:
|
|
1867
|
+
modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], modifier["name"]))
|
|
1868
|
+
else:
|
|
1869
|
+
set_count += 1
|
|
1870
|
+
modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], f"set #{set_count}"))
|
|
1871
|
+
|
|
1872
|
+
# print(modifier_names)
|
|
1873
|
+
if modifier_names:
|
|
1874
|
+
all_modifier_sets.extend(modifier_names)
|
|
1875
|
+
|
|
1876
|
+
# print()
|
|
1877
|
+
# print(f"{all_modifier_sets=}")
|
|
1878
|
+
|
|
1879
|
+
# create df
|
|
1880
|
+
|
|
1881
|
+
data = {
|
|
1882
|
+
"Observation id": [],
|
|
1883
|
+
"Observation date": [],
|
|
1884
|
+
"Description": [],
|
|
1885
|
+
"Observation type": [],
|
|
1886
|
+
"Observation interval start": [],
|
|
1887
|
+
"Observation interval stop": [],
|
|
1888
|
+
# "Source": [],
|
|
1889
|
+
# "Time offset (s)": [],
|
|
1890
|
+
# "Coding duration": [],
|
|
1891
|
+
# "Media duration (s)": [],
|
|
1892
|
+
# "FPS (frame/s)": [],
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
for indep_var in indep_variables:
|
|
1896
|
+
data[f"independent variable '{indep_var}'"] = []
|
|
1897
|
+
|
|
1898
|
+
data = data | {
|
|
1899
|
+
"Subject": [],
|
|
1900
|
+
"Observation duration by subject by observation": [],
|
|
1901
|
+
"Behavior": [],
|
|
1902
|
+
"Behavioral category": [],
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
for modifier_set in all_modifier_sets:
|
|
1906
|
+
data[modifier_set] = []
|
|
1907
|
+
|
|
1908
|
+
data = data | {
|
|
1909
|
+
"Behavior type": [],
|
|
1910
|
+
"Start (s)": [],
|
|
1911
|
+
"Stop (s)": [],
|
|
1912
|
+
"Duration (s)": [],
|
|
1913
|
+
# "Media file name": [],
|
|
1914
|
+
# "Image index start": [],
|
|
1915
|
+
# "Image index stop": [],
|
|
1916
|
+
# "Image file path start": [],
|
|
1917
|
+
# "Image file path stop": [],
|
|
1918
|
+
"Comment start": [],
|
|
1919
|
+
"Comment stop": [],
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
#
|
|
1923
|
+
|
|
1924
|
+
type_ = {
|
|
1925
|
+
"Observation id": "string",
|
|
1926
|
+
"Observation date": "string",
|
|
1927
|
+
"Description": "string",
|
|
1928
|
+
"Observation type": "string",
|
|
1929
|
+
"Observation interval start": "float64",
|
|
1930
|
+
"Observation interval stop": "float64",
|
|
1931
|
+
# "Source": "string",
|
|
1932
|
+
# "Time offset (s)": "string",
|
|
1933
|
+
# "Coding duration": "float64",
|
|
1934
|
+
# "Media duration (s)": "string",
|
|
1935
|
+
# "FPS (frame/s)": "float64",
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
# TODO: set correct type in base of the var type
|
|
1939
|
+
for indep_var in indep_variables:
|
|
1940
|
+
type_[f"independent variable '{indep_var}'"] = "float64" if indep_variables[indep_var] == cfg.NUMERIC else "string"
|
|
1941
|
+
|
|
1942
|
+
type_ = type_ | {
|
|
1943
|
+
"Subject": "string",
|
|
1944
|
+
"Observation duration by subject by observation": "float64",
|
|
1945
|
+
"Behavior": "string",
|
|
1946
|
+
"Behavioral category": "string",
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
for modifer_set in all_modifier_sets:
|
|
1950
|
+
type_[modifer_set] = "string"
|
|
1951
|
+
|
|
1952
|
+
type_ = type_ | {
|
|
1953
|
+
"Behavior type": "string",
|
|
1954
|
+
"Start (s)": "float64",
|
|
1955
|
+
"Stop (s)": "float64",
|
|
1956
|
+
"Duration (s)": "float64",
|
|
1957
|
+
# "Media file name": "string",
|
|
1958
|
+
# "Image index start": "float64",
|
|
1959
|
+
# "Image index stop": "float64",
|
|
1960
|
+
# "Image file path start": "string",
|
|
1961
|
+
# "Image file path stop": "string",
|
|
1962
|
+
"Comment start": "string",
|
|
1963
|
+
"Comment stop": "string",
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
state_behaviors = util.state_behavior_codes(pj[cfg.ETHOGRAM])
|
|
1967
|
+
|
|
1968
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1969
|
+
if observations_list and obs_id not in observations_list:
|
|
1970
|
+
continue
|
|
1971
|
+
# print(obs_id)
|
|
1972
|
+
stop_event_idx = set()
|
|
1973
|
+
for idx_event, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
1974
|
+
if idx_event in stop_event_idx:
|
|
1975
|
+
continue
|
|
1976
|
+
data["Observation id"].append(obs_id)
|
|
1977
|
+
data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
|
|
1978
|
+
data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
|
|
1979
|
+
data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
|
|
1980
|
+
|
|
1981
|
+
data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
|
|
1982
|
+
data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
|
|
1983
|
+
|
|
1984
|
+
# data["Source"].append("")
|
|
1985
|
+
# data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
|
|
1986
|
+
# data["Coding duration"].append("")
|
|
1987
|
+
# data["Media duration (s)"].append("")
|
|
1988
|
+
# data["FPS (frame/s)"].append("")
|
|
1989
|
+
|
|
1990
|
+
for indep_var in indep_variables:
|
|
1991
|
+
data[f"independent variable '{indep_var}'"].append(
|
|
1992
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES].get(indep_var, None)
|
|
1993
|
+
)
|
|
1994
|
+
|
|
1995
|
+
data["Subject"].append(event[cfg.EVENT_SUBJECT_FIELD_IDX] if event[cfg.EVENT_SUBJECT_FIELD_IDX] != "" else cfg.NO_FOCAL_SUBJECT)
|
|
1996
|
+
data["Observation duration by subject by observation"].append(-1)
|
|
1997
|
+
data["Behavior"].append(event[2])
|
|
1998
|
+
data["Behavioral category"].append(behavioral_category[event[2]])
|
|
1999
|
+
|
|
2000
|
+
count_set = 0
|
|
2001
|
+
for modifier_set in all_modifier_sets:
|
|
2002
|
+
if event[2] == modifier_set[0]:
|
|
2003
|
+
try:
|
|
2004
|
+
data[modifier_set].append(event[3].split("|")[count_set])
|
|
2005
|
+
except Exception:
|
|
2006
|
+
return f"Modifier error for {event[2]} in observation {obs_id}", pd.DataFrame()
|
|
2007
|
+
count_set += 1
|
|
2008
|
+
else:
|
|
2009
|
+
data[modifier_set].append(np.nan)
|
|
2010
|
+
|
|
2011
|
+
data["Behavior type"].append(cfg.STATE_EVENT if event[2] in state_behaviors else cfg.POINT_EVENT)
|
|
2012
|
+
data["Start (s)"].append(float(event[0]))
|
|
2013
|
+
if event[2] in state_behaviors:
|
|
2014
|
+
# search stop
|
|
2015
|
+
# print(f"==> {idx_event=} {event[1:4]=}")
|
|
2016
|
+
for idx_event2, event2 in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx_event + 1 :], start=idx_event + 1):
|
|
2017
|
+
# print(f"{idx_event2=} {event2[1:4]=}")
|
|
2018
|
+
if event2[1:4] == event[1:4]:
|
|
2019
|
+
# print("found")
|
|
2020
|
+
stop_event_idx.add(idx_event2)
|
|
2021
|
+
data["Stop (s)"].append(float(event2[0]))
|
|
2022
|
+
data["Duration (s)"].append(float(event2[0] - event[0]))
|
|
2023
|
+
data["Comment start"].append(event[4])
|
|
2024
|
+
data["Comment stop"].append(event2[4])
|
|
2025
|
+
break
|
|
2026
|
+
else:
|
|
2027
|
+
return f"Some events are not paired in {obs_id}", pd.DataFrame()
|
|
2028
|
+
|
|
2029
|
+
else: # point
|
|
2030
|
+
data["Stop (s)"].append(float(event[0]))
|
|
2031
|
+
data["Duration (s)"].append(np.nan)
|
|
2032
|
+
data["Comment start"].append(event[4])
|
|
2033
|
+
data["Comment stop"].append(event[4])
|
|
2034
|
+
|
|
2035
|
+
# Set the display option to show all rows and columns
|
|
2036
|
+
pd.set_option("display.max_rows", None)
|
|
2037
|
+
pd.set_option("display.max_columns", None)
|
|
2038
|
+
|
|
2039
|
+
pd.DataFrame(data).info()
|
|
2040
|
+
|
|
2041
|
+
return "", pd.DataFrame(data)
|