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
boris/export_events.py
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
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
|
+
import datetime as dt
|
|
24
|
+
import logging
|
|
25
|
+
import math
|
|
26
|
+
import os
|
|
27
|
+
import tablib
|
|
28
|
+
import pathlib as pl
|
|
29
|
+
from decimal import Decimal as dec
|
|
30
|
+
|
|
31
|
+
from . import observation_operations
|
|
32
|
+
from . import utilities as util
|
|
33
|
+
from . import config as cfg
|
|
34
|
+
from . import select_observations
|
|
35
|
+
from . import export_observation
|
|
36
|
+
from . import select_subj_behav
|
|
37
|
+
from . import project_functions
|
|
38
|
+
from . import dialog
|
|
39
|
+
from . import db_functions
|
|
40
|
+
|
|
41
|
+
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QInputDialog
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def export_events_as_behavioral_sequences(self, separated_subjects=False, timed=False):
|
|
45
|
+
"""
|
|
46
|
+
export events from selected observations by subject as behavioral sequences (plain text file)
|
|
47
|
+
behaviors are separated by character specified in self.behav_seq_separator (usually pipe |)
|
|
48
|
+
for use with Behatrix (see https://www.boris.unito.it/pages/behatrix)
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
separated_subjects (bool):
|
|
52
|
+
timed (bool):
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# ask user for observations to analyze
|
|
56
|
+
_, selected_observations = select_observations.select_observations2(
|
|
57
|
+
self, cfg.MULTIPLE, "Select observations to export as behavioral sequences"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not selected_observations:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# check if coded behaviors are defined in ethogram
|
|
64
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# check if state events are paired
|
|
68
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
69
|
+
if not_ok or not selected_observations:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if len(selected_observations) == 1:
|
|
73
|
+
max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
74
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
75
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
76
|
+
else:
|
|
77
|
+
max_media_duration_all_obs = None
|
|
78
|
+
start_coding, end_coding = dec("NaN"), dec("NaN")
|
|
79
|
+
start_interval, end_interval = None, None
|
|
80
|
+
|
|
81
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
82
|
+
self,
|
|
83
|
+
selected_observations,
|
|
84
|
+
start_coding=start_coding,
|
|
85
|
+
end_coding=end_coding,
|
|
86
|
+
start_interval=start_interval,
|
|
87
|
+
end_interval=end_interval,
|
|
88
|
+
maxTime=max_media_duration_all_obs,
|
|
89
|
+
show_include_modifiers=True,
|
|
90
|
+
show_exclude_non_coded_behaviors=False,
|
|
91
|
+
n_observations=len(selected_observations),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if parameters == {}:
|
|
95
|
+
return
|
|
96
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
97
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
file_name, _ = QFileDialog.getSaveFileName(self, "Export events as behavioral sequences", "", "Text files (*.txt);;All files (*)")
|
|
101
|
+
|
|
102
|
+
if not file_name:
|
|
103
|
+
return
|
|
104
|
+
r, msg = export_observation.observation_to_behavioral_sequences(
|
|
105
|
+
pj=self.pj,
|
|
106
|
+
selected_observations=selected_observations,
|
|
107
|
+
parameters=parameters,
|
|
108
|
+
behaviors_separator=self.behav_seq_separator,
|
|
109
|
+
separated_subjects=separated_subjects,
|
|
110
|
+
timed=timed,
|
|
111
|
+
file_name=file_name,
|
|
112
|
+
)
|
|
113
|
+
if not r:
|
|
114
|
+
logging.critical(f"Error while exporting events as behavioral sequences: {msg}")
|
|
115
|
+
QMessageBox.critical(
|
|
116
|
+
None,
|
|
117
|
+
cfg.programName,
|
|
118
|
+
f"Error while exporting events as behavioral sequences:<br>{msg}",
|
|
119
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
120
|
+
QMessageBox.NoButton,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def export_tabular_events(self, mode: str = "tabular") -> None:
|
|
125
|
+
"""
|
|
126
|
+
* select observations
|
|
127
|
+
* export events from the selected observations in various formats: TSV, CSV, ODS, XLSX, XLS, HTML
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
mode (str): export mode: must be ["tabular", "jwatcher"]
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
# ask user observations to analyze
|
|
134
|
+
_, selected_observations = select_observations.select_observations2(
|
|
135
|
+
self, cfg.MULTIPLE, windows_title="Select observations for exporting events"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if not selected_observations:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if mode == "jwatcher":
|
|
142
|
+
# check if images observation in list
|
|
143
|
+
max_obs_length, _ = observation_operations.observation_length(self.pj, selected_observations)
|
|
144
|
+
|
|
145
|
+
# exit with message if events do not have timestamp
|
|
146
|
+
if max_obs_length.is_nan():
|
|
147
|
+
QMessageBox.critical(
|
|
148
|
+
None,
|
|
149
|
+
cfg.programName,
|
|
150
|
+
("This function is not available for observations with events that do not have timestamp"),
|
|
151
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
152
|
+
QMessageBox.NoButton,
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# check if coded behaviors are defined in ethogram
|
|
157
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# check if state events are paired
|
|
161
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
162
|
+
if not_ok or not selected_observations:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if len(selected_observations) == 1:
|
|
166
|
+
max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
167
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
168
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
169
|
+
else:
|
|
170
|
+
max_media_duration_all_obs = None
|
|
171
|
+
start_coding, end_coding = dec("NaN"), dec("NaN")
|
|
172
|
+
start_interval, end_interval = None, None
|
|
173
|
+
|
|
174
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
175
|
+
self,
|
|
176
|
+
selected_observations,
|
|
177
|
+
start_coding=start_coding,
|
|
178
|
+
end_coding=end_coding,
|
|
179
|
+
start_interval=start_interval,
|
|
180
|
+
end_interval=end_interval,
|
|
181
|
+
maxTime=max_media_duration_all_obs,
|
|
182
|
+
show_include_modifiers=False,
|
|
183
|
+
show_exclude_non_coded_behaviors=False,
|
|
184
|
+
n_observations=len(selected_observations),
|
|
185
|
+
)
|
|
186
|
+
if parameters == {}:
|
|
187
|
+
return
|
|
188
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
189
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if mode == "tabular":
|
|
193
|
+
available_formats = (
|
|
194
|
+
cfg.TSV,
|
|
195
|
+
cfg.CSV,
|
|
196
|
+
cfg.ODS,
|
|
197
|
+
cfg.XLSX,
|
|
198
|
+
cfg.XLS,
|
|
199
|
+
cfg.HTML,
|
|
200
|
+
cfg.PANDAS_DF,
|
|
201
|
+
cfg.RDS,
|
|
202
|
+
)
|
|
203
|
+
if len(selected_observations) > 1: # choose directory for exporting observations
|
|
204
|
+
item, ok = QInputDialog.getItem(
|
|
205
|
+
self,
|
|
206
|
+
"Export events format",
|
|
207
|
+
"Available formats",
|
|
208
|
+
available_formats,
|
|
209
|
+
0,
|
|
210
|
+
False,
|
|
211
|
+
)
|
|
212
|
+
if not ok:
|
|
213
|
+
return
|
|
214
|
+
output_format = cfg.FILE_NAME_SUFFIX[item]
|
|
215
|
+
|
|
216
|
+
exportDir = QFileDialog().getExistingDirectory(
|
|
217
|
+
self,
|
|
218
|
+
"Choose a directory to export events",
|
|
219
|
+
os.path.expanduser("~"),
|
|
220
|
+
options=QFileDialog.ShowDirsOnly,
|
|
221
|
+
)
|
|
222
|
+
if not exportDir:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if len(selected_observations) == 1:
|
|
226
|
+
file_dialog_options = QFileDialog.Options()
|
|
227
|
+
file_dialog_options |= QFileDialog.DontConfirmOverwrite
|
|
228
|
+
|
|
229
|
+
file_name, filter_ = QFileDialog().getSaveFileName(
|
|
230
|
+
self, "Export events", "", ";;".join(available_formats), options=file_dialog_options
|
|
231
|
+
)
|
|
232
|
+
if not file_name:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
output_format = cfg.FILE_NAME_SUFFIX[filter_]
|
|
236
|
+
if pl.Path(file_name).suffix != "." + output_format:
|
|
237
|
+
file_name = str(pl.Path(file_name)) + "." + output_format
|
|
238
|
+
# check if file with new extension already exists
|
|
239
|
+
if pl.Path(file_name).exists():
|
|
240
|
+
if (
|
|
241
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
242
|
+
== cfg.CANCEL
|
|
243
|
+
):
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if mode == "jwatcher":
|
|
247
|
+
exportDir = QFileDialog().getExistingDirectory(
|
|
248
|
+
self, "Choose a directory to export events", os.path.expanduser("~"), options=QFileDialog.ShowDirsOnly
|
|
249
|
+
)
|
|
250
|
+
if not exportDir:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
output_format = "dat"
|
|
254
|
+
|
|
255
|
+
mem_command = "" # remember user choice when file already exists
|
|
256
|
+
for obs_id in selected_observations:
|
|
257
|
+
if len(selected_observations) > 1 or mode == "jwatcher":
|
|
258
|
+
file_name = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{output_format}"
|
|
259
|
+
# check if file with new extension already exists
|
|
260
|
+
if mem_command != cfg.OVERWRITE_ALL and pl.Path(file_name).is_file():
|
|
261
|
+
if mem_command == cfg.SKIP_ALL:
|
|
262
|
+
continue
|
|
263
|
+
mem_command = dialog.MessageDialog(
|
|
264
|
+
cfg.programName,
|
|
265
|
+
f"The file {file_name} already exists.",
|
|
266
|
+
[cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
|
|
267
|
+
)
|
|
268
|
+
if mem_command == cfg.CANCEL:
|
|
269
|
+
return
|
|
270
|
+
if mem_command in [cfg.SKIP, cfg.SKIP_ALL]:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
if mode == "tabular":
|
|
274
|
+
r, msg = export_observation.export_tabular_events(
|
|
275
|
+
self.pj,
|
|
276
|
+
parameters,
|
|
277
|
+
obs_id,
|
|
278
|
+
self.pj[cfg.OBSERVATIONS][obs_id],
|
|
279
|
+
self.pj[cfg.ETHOGRAM],
|
|
280
|
+
file_name,
|
|
281
|
+
output_format,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if mode == "jwatcher":
|
|
285
|
+
r, msg = export_observation.export_events_jwatcher(
|
|
286
|
+
parameters, obs_id, self.pj[cfg.OBSERVATIONS][obs_id], self.pj[cfg.ETHOGRAM], file_name, output_format
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if not r and msg:
|
|
290
|
+
QMessageBox.critical(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def export_aggregated_events(self):
|
|
294
|
+
"""
|
|
295
|
+
- select observations.
|
|
296
|
+
- select subjects and behaviors
|
|
297
|
+
- export events in aggregated format
|
|
298
|
+
|
|
299
|
+
Formats can be SQL (sql), SDIS (sds), Tabular format (tsv, csv, ods, xlsx, xls, html) or Pandas dataframe
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def fields_type(max_modif_number: int) -> dict:
|
|
303
|
+
fields_type_dict: dict = {
|
|
304
|
+
"Observation id": str,
|
|
305
|
+
"Observation date": dt.datetime,
|
|
306
|
+
"Description": str,
|
|
307
|
+
"Observation type": str,
|
|
308
|
+
"Source": str,
|
|
309
|
+
"Time offset (s)": str,
|
|
310
|
+
"Coding duration": float,
|
|
311
|
+
"Media duration (s)": str,
|
|
312
|
+
"FPS (frame/s)": str,
|
|
313
|
+
}
|
|
314
|
+
# TODO: "Media duration (s)" and "FPS (frame/s)" can be float for observation from 1 video
|
|
315
|
+
|
|
316
|
+
if cfg.INDEPENDENT_VARIABLES in self.pj:
|
|
317
|
+
for idx in util.sorted_keys(self.pj[cfg.INDEPENDENT_VARIABLES]):
|
|
318
|
+
if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "timestamp":
|
|
319
|
+
fields_type_dict[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = dt.datetime
|
|
320
|
+
elif self.pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "numeric":
|
|
321
|
+
fields_type_dict[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = float
|
|
322
|
+
else:
|
|
323
|
+
fields_type_dict[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = str
|
|
324
|
+
|
|
325
|
+
fields_type_dict.update(
|
|
326
|
+
{
|
|
327
|
+
"Subject": str,
|
|
328
|
+
"Observation duration by subject by observation": float,
|
|
329
|
+
"Behavior": str,
|
|
330
|
+
"Behavioral category": str,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# max number of modifiers
|
|
335
|
+
for i in range(max_modif_number):
|
|
336
|
+
fields_type_dict[f"Modifier #{i + 1}"] = str
|
|
337
|
+
|
|
338
|
+
fields_type_dict.update(
|
|
339
|
+
{
|
|
340
|
+
"Behavior type": str,
|
|
341
|
+
"Start (s)": float,
|
|
342
|
+
"Stop (s)": float,
|
|
343
|
+
"Duration (s)": float,
|
|
344
|
+
"Media file name": str,
|
|
345
|
+
"Image index start": float, # add image index and image file path to header
|
|
346
|
+
"Image index stop": float,
|
|
347
|
+
"Image file path start": str,
|
|
348
|
+
"Image file path stop": str,
|
|
349
|
+
"Comment start": str,
|
|
350
|
+
"Comment stop": str,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return fields_type_dict
|
|
355
|
+
|
|
356
|
+
_, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE, "Select observations for exporting events")
|
|
357
|
+
if not selected_observations:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
# check if coded behaviors are defined in ethogram
|
|
361
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
# check if state events are paired
|
|
365
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
366
|
+
if not_ok or not selected_observations:
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
if len(selected_observations) == 1:
|
|
370
|
+
max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
371
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
372
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
373
|
+
else:
|
|
374
|
+
max_media_duration_all_obs = None
|
|
375
|
+
start_coding, end_coding = dec("NaN"), dec("NaN")
|
|
376
|
+
start_interval, end_interval = None, None
|
|
377
|
+
|
|
378
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
379
|
+
self,
|
|
380
|
+
selected_observations,
|
|
381
|
+
start_coding=start_coding,
|
|
382
|
+
end_coding=end_coding,
|
|
383
|
+
start_interval=start_interval,
|
|
384
|
+
end_interval=end_interval,
|
|
385
|
+
maxTime=max_media_duration_all_obs,
|
|
386
|
+
show_include_modifiers=False,
|
|
387
|
+
show_exclude_non_coded_behaviors=False,
|
|
388
|
+
n_observations=len(selected_observations),
|
|
389
|
+
)
|
|
390
|
+
if parameters == {}:
|
|
391
|
+
return
|
|
392
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
393
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# check for grouping results
|
|
397
|
+
flag_group = True
|
|
398
|
+
if len(selected_observations) > 1:
|
|
399
|
+
flag_group = (
|
|
400
|
+
dialog.MessageDialog(cfg.programName, "Group events from selected observations in one file?", [cfg.YES, cfg.NO]) == cfg.YES
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if flag_group:
|
|
404
|
+
file_formats = (
|
|
405
|
+
cfg.TSV,
|
|
406
|
+
cfg.CSV,
|
|
407
|
+
cfg.ODS,
|
|
408
|
+
cfg.XLSX,
|
|
409
|
+
cfg.XLS,
|
|
410
|
+
cfg.HTML,
|
|
411
|
+
cfg.SDIS,
|
|
412
|
+
cfg.TBS,
|
|
413
|
+
cfg.SQL,
|
|
414
|
+
cfg.PANDAS_DF,
|
|
415
|
+
cfg.RDS,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
file_dialog_options = QFileDialog.Options()
|
|
419
|
+
file_dialog_options |= QFileDialog.DontConfirmOverwrite
|
|
420
|
+
|
|
421
|
+
fileName, filter_ = QFileDialog().getSaveFileName(
|
|
422
|
+
self, "Export aggregated events", "", ";;".join(file_formats), options=file_dialog_options
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if not fileName:
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
outputFormat = cfg.FILE_NAME_SUFFIX[filter_]
|
|
429
|
+
if pl.Path(fileName).suffix != "." + outputFormat:
|
|
430
|
+
# check if file with new extension already exists
|
|
431
|
+
fileName = str(pl.Path(fileName)) + "." + outputFormat
|
|
432
|
+
if pl.Path(fileName).exists():
|
|
433
|
+
if dialog.MessageDialog(cfg.programName, f"The file {fileName} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
else: # not grouping
|
|
437
|
+
file_formats = (
|
|
438
|
+
cfg.TSV,
|
|
439
|
+
cfg.CSV,
|
|
440
|
+
cfg.ODS,
|
|
441
|
+
cfg.XLSX,
|
|
442
|
+
cfg.XLS,
|
|
443
|
+
cfg.HTML,
|
|
444
|
+
cfg.SDIS,
|
|
445
|
+
cfg.TBS,
|
|
446
|
+
cfg.PANDAS_DF,
|
|
447
|
+
cfg.RDS,
|
|
448
|
+
)
|
|
449
|
+
item, ok = QInputDialog.getItem(self, "Export events format", "Available formats", file_formats, 0, False)
|
|
450
|
+
if not ok:
|
|
451
|
+
return
|
|
452
|
+
# read the output format code
|
|
453
|
+
outputFormat = cfg.FILE_NAME_SUFFIX[item]
|
|
454
|
+
|
|
455
|
+
exportDir = QFileDialog().getExistingDirectory(
|
|
456
|
+
self, "Choose a directory to export events", os.path.expanduser("~"), options=QFileDialog.ShowDirsOnly
|
|
457
|
+
)
|
|
458
|
+
if not exportDir:
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
if outputFormat == cfg.SQL_EXT:
|
|
462
|
+
_, _, conn = db_functions.load_aggregated_events_in_db(
|
|
463
|
+
self.pj, parameters[cfg.SELECTED_SUBJECTS], selected_observations, parameters[cfg.SELECTED_BEHAVIORS]
|
|
464
|
+
)
|
|
465
|
+
try:
|
|
466
|
+
with open(fileName, "w") as f:
|
|
467
|
+
for line in conn.iterdump():
|
|
468
|
+
f.write(f"{line}\n")
|
|
469
|
+
except Exception:
|
|
470
|
+
QMessageBox.critical(
|
|
471
|
+
None,
|
|
472
|
+
cfg.programName,
|
|
473
|
+
f"The file {fileName} can not be saved",
|
|
474
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
475
|
+
QMessageBox.NoButton,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
# compute the maximum number of modifiers
|
|
481
|
+
tot_max_modifiers: int = 0
|
|
482
|
+
for obs_id in selected_observations:
|
|
483
|
+
_, max_modifiers = export_observation.export_aggregated_events(self.pj, parameters, obs_id)
|
|
484
|
+
tot_max_modifiers = max(tot_max_modifiers, max_modifiers)
|
|
485
|
+
|
|
486
|
+
logging.debug(f"tot_max_modifiers: {tot_max_modifiers}")
|
|
487
|
+
|
|
488
|
+
data_grouped_obs = tablib.Dataset()
|
|
489
|
+
|
|
490
|
+
mem_command: str = "" # remember user choice when file already exists
|
|
491
|
+
header = list(fields_type(tot_max_modifiers).keys())
|
|
492
|
+
|
|
493
|
+
for obs_id in selected_observations:
|
|
494
|
+
logging.debug(f"Exporting aggregated events for obs Id: {obs_id}")
|
|
495
|
+
|
|
496
|
+
data_single_obs, _ = export_observation.export_aggregated_events(
|
|
497
|
+
self.pj, parameters, obs_id, force_number_modifiers=tot_max_modifiers
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
# order by start time
|
|
502
|
+
index = header.index("Start (s)")
|
|
503
|
+
if cfg.NA not in [x[index] for x in list(data_single_obs)]:
|
|
504
|
+
data_single_obs_sorted = tablib.Dataset(
|
|
505
|
+
*sorted(list(data_single_obs), key=lambda x: float(x[index])),
|
|
506
|
+
headers=list(fields_type(tot_max_modifiers).keys()),
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
# order by image index
|
|
510
|
+
index = header.index("Image index start")
|
|
511
|
+
data_single_obs_sorted = tablib.Dataset(
|
|
512
|
+
*sorted(list(data_single_obs), key=lambda x: float(x[index])),
|
|
513
|
+
headers=list(fields_type(tot_max_modifiers).keys()),
|
|
514
|
+
)
|
|
515
|
+
except Exception:
|
|
516
|
+
# if error no order
|
|
517
|
+
data_single_obs_sorted = tablib.Dataset(
|
|
518
|
+
*list(data_single_obs),
|
|
519
|
+
headers=list(fields_type(tot_max_modifiers).keys()),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
data_single_obs_sorted.title = obs_id
|
|
523
|
+
|
|
524
|
+
if (not flag_group) and (outputFormat not in (cfg.SDIS_EXT, cfg.TBS_EXT)):
|
|
525
|
+
fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
|
|
526
|
+
# check if file with new extension already exists
|
|
527
|
+
if mem_command != cfg.OVERWRITE_ALL and pl.Path(fileName).is_file():
|
|
528
|
+
if mem_command == cfg.SKIP_ALL:
|
|
529
|
+
continue
|
|
530
|
+
mem_command = dialog.MessageDialog(
|
|
531
|
+
cfg.programName,
|
|
532
|
+
f"The file {fileName} already exists.",
|
|
533
|
+
[cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
|
|
534
|
+
)
|
|
535
|
+
if mem_command == cfg.CANCEL:
|
|
536
|
+
return
|
|
537
|
+
if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
|
|
538
|
+
continue
|
|
539
|
+
|
|
540
|
+
r, msg = export_observation.dataset_write(data_single_obs_sorted, fileName, outputFormat, dtype=fields_type(max_modifiers))
|
|
541
|
+
if not r:
|
|
542
|
+
QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
|
|
543
|
+
|
|
544
|
+
"""
|
|
545
|
+
# disabled after introduction of the force_number_modifiers parameter in export_aggregated_events function
|
|
546
|
+
if len(data_single_obs_sorted) and max_modifiers < tot_max_modifiers:
|
|
547
|
+
for i in range(tot_max_modifiers - max_modifiers):
|
|
548
|
+
data_single_obs_sorted.insert_col(
|
|
549
|
+
14,
|
|
550
|
+
col=[""] * (len(list(data_single_obs_sorted))),
|
|
551
|
+
header=f"Modif #{i}",
|
|
552
|
+
)
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
data_grouped_obs.extend(data_single_obs_sorted)
|
|
556
|
+
|
|
557
|
+
data_grouped_obs_all = tablib.Dataset(headers=list(fields_type(tot_max_modifiers).keys()))
|
|
558
|
+
|
|
559
|
+
data_grouped_obs_all.extend(data_grouped_obs)
|
|
560
|
+
data_grouped_obs_all.title = "Aggregated events"
|
|
561
|
+
|
|
562
|
+
start_idx = header.index("Start (s)")
|
|
563
|
+
stop_idx = header.index("Stop (s)")
|
|
564
|
+
|
|
565
|
+
if outputFormat == cfg.TBS_EXT: # Timed behavioral sequences
|
|
566
|
+
out: str = ""
|
|
567
|
+
for obs_id in selected_observations:
|
|
568
|
+
# observation id
|
|
569
|
+
out += f"# {obs_id}\n"
|
|
570
|
+
|
|
571
|
+
for event in list(data_grouped_obs_all):
|
|
572
|
+
if event[0] == obs_id:
|
|
573
|
+
behavior = event[header.index("Behavior")]
|
|
574
|
+
subject = event[header.index("Subject")]
|
|
575
|
+
# replace various char by _
|
|
576
|
+
for char in (" ", "-", "/"):
|
|
577
|
+
behavior = behavior.replace(char, "_")
|
|
578
|
+
subject = subject.replace(char, "_")
|
|
579
|
+
event_start = f"{float(event[start_idx]):.3f}" # start event
|
|
580
|
+
if not event[stop_idx]: # stop event (from end)
|
|
581
|
+
event_stop = f"{float(event[start_idx]) + 0.001:.3f}"
|
|
582
|
+
else:
|
|
583
|
+
event_stop = f"{float(event[stop_idx]):.3f}"
|
|
584
|
+
|
|
585
|
+
bs_timed = [f"{subject}_{behavior}"] * round((float(event_stop) - float(event_start)) * 100)
|
|
586
|
+
out += "|".join(bs_timed)
|
|
587
|
+
|
|
588
|
+
out += "\n"
|
|
589
|
+
|
|
590
|
+
if not flag_group:
|
|
591
|
+
fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
|
|
592
|
+
with open(fileName, "wb") as f:
|
|
593
|
+
f.write(str.encode(out))
|
|
594
|
+
out = ""
|
|
595
|
+
|
|
596
|
+
if flag_group:
|
|
597
|
+
with open(fileName, "wb") as f:
|
|
598
|
+
f.write(str.encode(out))
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
if outputFormat == cfg.SDIS_EXT: # SDIS format
|
|
602
|
+
out: str = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
|
|
603
|
+
for obs_id in selected_observations:
|
|
604
|
+
# observation id
|
|
605
|
+
out += "\n<{}>\n".format(obs_id)
|
|
606
|
+
|
|
607
|
+
for event in list(data_grouped_obs_all):
|
|
608
|
+
if event[0] == obs_id:
|
|
609
|
+
behavior = event[header.index("Behavior")]
|
|
610
|
+
subject = event[header.index("Subject")]
|
|
611
|
+
# replace various char by _
|
|
612
|
+
for char in (" ", "-", "/"):
|
|
613
|
+
behavior = behavior.replace(char, "_")
|
|
614
|
+
subject = subject.replace(char, "_")
|
|
615
|
+
|
|
616
|
+
event_start = f"{float(event[start_idx]):.3f}" # start event
|
|
617
|
+
if not event[stop_idx]: # stop event (from end)
|
|
618
|
+
event_stop = f"{float(event[start_idx]) + 0.001:.3f}"
|
|
619
|
+
else:
|
|
620
|
+
event_stop = f"{float(event[stop_idx]):.3f}"
|
|
621
|
+
out += f"{subject}_{behavior},{event_start}-{event_stop} "
|
|
622
|
+
|
|
623
|
+
out += "/\n\n"
|
|
624
|
+
if not flag_group:
|
|
625
|
+
fileName = f"{pl.Path(exportDir) / util.safeFileName(obs_id)}.{outputFormat}"
|
|
626
|
+
with open(fileName, "wb") as f:
|
|
627
|
+
f.write(str.encode(out))
|
|
628
|
+
out = f"% SDIS file created by BORIS (www.boris.unito.it) at {util.datetime_iso8601(dt.datetime.now())}\nTimed <seconds>;\n"
|
|
629
|
+
|
|
630
|
+
if flag_group:
|
|
631
|
+
with open(fileName, "wb") as f:
|
|
632
|
+
f.write(str.encode(out))
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
if flag_group:
|
|
636
|
+
r, msg = export_observation.dataset_write(data_grouped_obs_all, fileName, outputFormat, dtype=fields_type(max_modifiers))
|
|
637
|
+
if not r:
|
|
638
|
+
QMessageBox.warning(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def export_events_as_textgrid(self) -> None:
|
|
642
|
+
"""
|
|
643
|
+
* select observations
|
|
644
|
+
* select subjects, behaviors and time interval
|
|
645
|
+
* export state events of selected observations as Praat textgrid
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
_, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
|
|
649
|
+
|
|
650
|
+
if not selected_observations:
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
# check if coded behaviors are defined in ethogram
|
|
654
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
# check if state events are paired
|
|
658
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
659
|
+
if not_ok or not selected_observations:
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
max_obs_length, _ = observation_operations.observation_length(self.pj, selected_observations)
|
|
663
|
+
|
|
664
|
+
# exit with message if events do not have timestamp
|
|
665
|
+
if max_obs_length.is_nan():
|
|
666
|
+
QMessageBox.critical(
|
|
667
|
+
None,
|
|
668
|
+
cfg.programName,
|
|
669
|
+
("This function is not available for observations with events that do not have timestamp"),
|
|
670
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
671
|
+
QMessageBox.NoButton,
|
|
672
|
+
)
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
676
|
+
|
|
677
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
678
|
+
|
|
679
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
680
|
+
self,
|
|
681
|
+
selected_observations,
|
|
682
|
+
start_coding=start_coding,
|
|
683
|
+
end_coding=end_coding,
|
|
684
|
+
# start_interval=start_interval,
|
|
685
|
+
# end_interval=end_interval,
|
|
686
|
+
start_interval=None,
|
|
687
|
+
end_interval=None,
|
|
688
|
+
show_include_modifiers=False,
|
|
689
|
+
show_exclude_non_coded_behaviors=False,
|
|
690
|
+
maxTime=max_obs_length,
|
|
691
|
+
n_observations=len(selected_observations),
|
|
692
|
+
)
|
|
693
|
+
if parameters == {}:
|
|
694
|
+
return
|
|
695
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
696
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to export")
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
export_dir = QFileDialog.getExistingDirectory(
|
|
700
|
+
self, "Export events as Praat TextGrid", os.path.expanduser("~"), options=QFileDialog.ShowDirsOnly
|
|
701
|
+
)
|
|
702
|
+
if not export_dir:
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
mem_command: str = ""
|
|
706
|
+
|
|
707
|
+
# see https://www.fon.hum.uva.nl/praat/manual/TextGrid_file_formats.html
|
|
708
|
+
|
|
709
|
+
interval_subject_header = (
|
|
710
|
+
" item [{subject_index}]:\n"
|
|
711
|
+
' class = "IntervalTier"\n'
|
|
712
|
+
' name = "{subject}"\n'
|
|
713
|
+
" xmin = 0.0\n"
|
|
714
|
+
" xmax = {intervalsMax}\n"
|
|
715
|
+
" intervals: size = {intervalsSize}\n"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
interval_template = ' intervals [{count}]:\n xmin = {xmin}\n xmax = {xmax}\n text = "{name}"\n'
|
|
719
|
+
|
|
720
|
+
point_subject_header = (
|
|
721
|
+
" item [{subject_index}]:\n"
|
|
722
|
+
' class = "TextTier"\n'
|
|
723
|
+
' name = "{subject}"\n'
|
|
724
|
+
" xmin = {intervalsMin}\n"
|
|
725
|
+
" xmax = {intervalsMax}\n"
|
|
726
|
+
" points: size = {intervalsSize}\n"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
point_template = ' points [{count}]:\n number = {number}\n mark = "{mark}"\n'
|
|
730
|
+
|
|
731
|
+
# widget for results
|
|
732
|
+
self.results = dialog.Results_dialog()
|
|
733
|
+
self.results.setWindowTitle(f"{cfg.programName} - Export events as Praat TextGrid")
|
|
734
|
+
self.results.show()
|
|
735
|
+
|
|
736
|
+
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(
|
|
737
|
+
self.pj, parameters[cfg.SELECTED_SUBJECTS], selected_observations, parameters[cfg.SELECTED_BEHAVIORS]
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if db_connector is None:
|
|
741
|
+
logging.critical("Error when loading aggregated events in DB")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
cursor = db_connector.cursor()
|
|
745
|
+
|
|
746
|
+
file_count: int = 0
|
|
747
|
+
|
|
748
|
+
for obs_id in selected_observations:
|
|
749
|
+
if parameters["time"] == cfg.TIME_EVENTS:
|
|
750
|
+
start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
|
|
751
|
+
if start_coding is None and end_coding is None: # no events
|
|
752
|
+
self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
|
|
753
|
+
QApplication.processEvents()
|
|
754
|
+
continue
|
|
755
|
+
|
|
756
|
+
if math.isnan(start_coding) or math.isnan(end_coding): # obs with no timestamp
|
|
757
|
+
self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have timestamp.")
|
|
758
|
+
QApplication.processEvents()
|
|
759
|
+
continue
|
|
760
|
+
|
|
761
|
+
min_time = float(start_coding)
|
|
762
|
+
max_time = float(end_coding)
|
|
763
|
+
|
|
764
|
+
if parameters["time"] == cfg.TIME_FULL_OBS:
|
|
765
|
+
if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
766
|
+
max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
|
|
767
|
+
min_time = float(0)
|
|
768
|
+
max_time = float(max_media_duration)
|
|
769
|
+
coding_duration = max_media_duration
|
|
770
|
+
|
|
771
|
+
if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
|
|
772
|
+
start_coding, end_coding, coding_duration = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], [obs_id])
|
|
773
|
+
if start_coding is None and end_coding is None: # no events
|
|
774
|
+
self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have events.")
|
|
775
|
+
QApplication.processEvents()
|
|
776
|
+
continue
|
|
777
|
+
if math.isnan(start_coding) or math.isnan(end_coding): # obs with no timestamp
|
|
778
|
+
self.results.ptText.appendHtml(f"The observation <b>{obs_id}</b> does not have timestamp.")
|
|
779
|
+
QApplication.processEvents()
|
|
780
|
+
continue
|
|
781
|
+
|
|
782
|
+
min_time = float(start_coding)
|
|
783
|
+
max_time = float(end_coding)
|
|
784
|
+
|
|
785
|
+
if parameters["time"] == cfg.TIME_ARBITRARY_INTERVAL:
|
|
786
|
+
min_time = float(parameters[cfg.START_TIME])
|
|
787
|
+
max_time = float(parameters[cfg.END_TIME])
|
|
788
|
+
|
|
789
|
+
if parameters["time"] == cfg.TIME_OBS_INTERVAL:
|
|
790
|
+
max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
|
|
791
|
+
obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
|
|
792
|
+
offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
|
|
793
|
+
min_time = float(obs_interval[0]) + offset
|
|
794
|
+
# Use max media duration for max time if no interval is defined (=0)
|
|
795
|
+
max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(max_media_duration)
|
|
796
|
+
|
|
797
|
+
# delete events outside time interval
|
|
798
|
+
|
|
799
|
+
cursor.execute(
|
|
800
|
+
"DELETE FROM aggregated_events WHERE observation = ? AND (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
|
|
801
|
+
(
|
|
802
|
+
obs_id,
|
|
803
|
+
min_time,
|
|
804
|
+
min_time,
|
|
805
|
+
max_time,
|
|
806
|
+
max_time,
|
|
807
|
+
),
|
|
808
|
+
)
|
|
809
|
+
cursor.execute(
|
|
810
|
+
"UPDATE aggregated_events SET start = ? WHERE observation = ? AND start < ? AND stop BETWEEN ? AND ?",
|
|
811
|
+
(
|
|
812
|
+
min_time,
|
|
813
|
+
obs_id,
|
|
814
|
+
min_time,
|
|
815
|
+
min_time,
|
|
816
|
+
max_time,
|
|
817
|
+
),
|
|
818
|
+
)
|
|
819
|
+
cursor.execute(
|
|
820
|
+
"UPDATE aggregated_events SET stop = ? WHERE observation = ? AND stop > ? AND start BETWEEN ? AND ?",
|
|
821
|
+
(
|
|
822
|
+
max_time,
|
|
823
|
+
obs_id,
|
|
824
|
+
max_time,
|
|
825
|
+
min_time,
|
|
826
|
+
max_time,
|
|
827
|
+
),
|
|
828
|
+
)
|
|
829
|
+
cursor.execute(
|
|
830
|
+
"UPDATE aggregated_events SET start = ?, stop = ? WHERE observation = ? AND start < ? AND stop > ?",
|
|
831
|
+
(
|
|
832
|
+
min_time,
|
|
833
|
+
max_time,
|
|
834
|
+
obs_id,
|
|
835
|
+
min_time,
|
|
836
|
+
max_time,
|
|
837
|
+
),
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
next_obs: bool = False
|
|
841
|
+
|
|
842
|
+
# number of items for size parameter
|
|
843
|
+
cursor.execute(
|
|
844
|
+
(
|
|
845
|
+
"SELECT COUNT(*) FROM (SELECT * FROM aggregated_events "
|
|
846
|
+
f"WHERE observation = ? AND subject IN ({','.join(['?'] * len(parameters[cfg.SELECTED_SUBJECTS]))}) GROUP BY subject, behavior) "
|
|
847
|
+
),
|
|
848
|
+
[obs_id] + parameters[cfg.SELECTED_SUBJECTS],
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
subjects_num = int(cursor.fetchone()[0])
|
|
852
|
+
subjects_max = max_time
|
|
853
|
+
|
|
854
|
+
out = (
|
|
855
|
+
'File type = "ooTextFile"\n'
|
|
856
|
+
'Object class = "TextGrid"\n'
|
|
857
|
+
"\n"
|
|
858
|
+
f"xmin = 0.0\n"
|
|
859
|
+
f"xmax = {subjects_max}\n"
|
|
860
|
+
"tiers? <exists>\n"
|
|
861
|
+
f"size = {subjects_num}\n"
|
|
862
|
+
"item []:\n"
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
subject_index: int = 0
|
|
866
|
+
for subject in parameters[cfg.SELECTED_SUBJECTS]:
|
|
867
|
+
if subject not in [
|
|
868
|
+
x[cfg.EVENT_SUBJECT_FIELD_IDX] if x[cfg.EVENT_SUBJECT_FIELD_IDX] else cfg.NO_FOCAL_SUBJECT
|
|
869
|
+
for x in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
|
|
870
|
+
]:
|
|
871
|
+
continue
|
|
872
|
+
|
|
873
|
+
intervalsMin = min_time
|
|
874
|
+
intervalsMax = max_time
|
|
875
|
+
|
|
876
|
+
# STATE events
|
|
877
|
+
cursor.execute(
|
|
878
|
+
(
|
|
879
|
+
"SELECT start, stop, behavior FROM aggregated_events "
|
|
880
|
+
"WHERE observation = ? AND subject = ? AND type = 'STATE' ORDER BY start"
|
|
881
|
+
),
|
|
882
|
+
(obs_id, subject),
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
rows = [
|
|
886
|
+
{"start": util.float2decimal(r["start"]), "stop": util.float2decimal(r["stop"]), "code": r["behavior"]}
|
|
887
|
+
for r in cursor.fetchall()
|
|
888
|
+
]
|
|
889
|
+
if rows:
|
|
890
|
+
out += interval_subject_header
|
|
891
|
+
|
|
892
|
+
count = 0
|
|
893
|
+
|
|
894
|
+
# check if 1st behavior starts at the beginning
|
|
895
|
+
if rows[0]["start"] > 0:
|
|
896
|
+
count += 1
|
|
897
|
+
out += interval_template.format(count=count, name="null", xmin=0.0, xmax=rows[0]["start"])
|
|
898
|
+
|
|
899
|
+
for idx, row in enumerate(rows):
|
|
900
|
+
# check if events are overlapping
|
|
901
|
+
if (idx + 1 < len(rows)) and (row["stop"] > rows[idx + 1]["start"]):
|
|
902
|
+
self.results.ptText.appendHtml(
|
|
903
|
+
(
|
|
904
|
+
f"The events overlap for subject <b>{subject}</b> in the observation <b>{obs_id}</b>. "
|
|
905
|
+
"It is not possible to create the Praat TextGrid file."
|
|
906
|
+
)
|
|
907
|
+
)
|
|
908
|
+
QApplication.processEvents()
|
|
909
|
+
|
|
910
|
+
next_obs = True
|
|
911
|
+
break
|
|
912
|
+
|
|
913
|
+
count += 1
|
|
914
|
+
|
|
915
|
+
if (idx + 1 < len(rows)) and (rows[idx + 1]["start"] - dec("0.001") <= row["stop"] < rows[idx + 1]["start"]):
|
|
916
|
+
xmax = rows[idx + 1]["start"]
|
|
917
|
+
else:
|
|
918
|
+
xmax = row["stop"]
|
|
919
|
+
|
|
920
|
+
out += interval_template.format(count=count, name=row["code"], xmin=row["start"], xmax=xmax)
|
|
921
|
+
|
|
922
|
+
# check if no behavior
|
|
923
|
+
if (idx + 1 < len(rows)) and (row["stop"] < rows[idx + 1]["start"] - dec("0.001")):
|
|
924
|
+
count += 1
|
|
925
|
+
out += interval_template.format(
|
|
926
|
+
count=count,
|
|
927
|
+
name="null",
|
|
928
|
+
xmin=row["stop"],
|
|
929
|
+
xmax=rows[idx + 1]["start"],
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
if next_obs:
|
|
933
|
+
break
|
|
934
|
+
|
|
935
|
+
# check if last event ends at the end of media file
|
|
936
|
+
if rows[-1]["stop"] < max_time:
|
|
937
|
+
count += 1
|
|
938
|
+
out += interval_template.format(count=count, name="null", xmin=rows[-1]["stop"], xmax=max_time)
|
|
939
|
+
|
|
940
|
+
# add info
|
|
941
|
+
subject_index += 1
|
|
942
|
+
out = out.format(
|
|
943
|
+
subject_index=subject_index,
|
|
944
|
+
subject=subject,
|
|
945
|
+
intervalsSize=count,
|
|
946
|
+
intervalsMin=intervalsMin,
|
|
947
|
+
intervalsMax=intervalsMax,
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
# POINT events
|
|
951
|
+
cursor.execute(
|
|
952
|
+
("SELECT start, behavior FROM aggregated_events WHERE observation = ? AND subject = ? AND type = 'POINT' ORDER BY start"),
|
|
953
|
+
(obs_id, subject),
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
rows = [{"start": util.float2decimal(r["start"]), "code": r["behavior"]} for r in cursor.fetchall()]
|
|
957
|
+
if not rows:
|
|
958
|
+
continue
|
|
959
|
+
|
|
960
|
+
out += point_subject_header
|
|
961
|
+
|
|
962
|
+
count = 0
|
|
963
|
+
|
|
964
|
+
for idx, row in enumerate(rows):
|
|
965
|
+
count += 1
|
|
966
|
+
out += point_template.format(count=count, mark=row["code"], number=row["start"])
|
|
967
|
+
|
|
968
|
+
# add info
|
|
969
|
+
subject_index += 1
|
|
970
|
+
out = out.format(
|
|
971
|
+
subject_index=subject_index,
|
|
972
|
+
subject=subject,
|
|
973
|
+
intervalsSize=count,
|
|
974
|
+
intervalsMin=intervalsMin,
|
|
975
|
+
intervalsMax=intervalsMax,
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
if next_obs:
|
|
979
|
+
continue
|
|
980
|
+
|
|
981
|
+
# check if file already exists
|
|
982
|
+
if mem_command != cfg.OVERWRITE_ALL and pl.Path(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid").is_file():
|
|
983
|
+
if mem_command == cfg.SKIP_ALL:
|
|
984
|
+
continue
|
|
985
|
+
mem_command = dialog.MessageDialog(
|
|
986
|
+
cfg.programName,
|
|
987
|
+
f"The file <b>{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid</b> already exists.",
|
|
988
|
+
[cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
|
|
989
|
+
)
|
|
990
|
+
if mem_command == cfg.CANCEL:
|
|
991
|
+
return
|
|
992
|
+
if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
|
|
993
|
+
continue
|
|
994
|
+
|
|
995
|
+
try:
|
|
996
|
+
with open(f"{pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid", "w") as f:
|
|
997
|
+
f.write(out)
|
|
998
|
+
file_count += 1
|
|
999
|
+
self.results.ptText.appendHtml(f"File {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid was created.")
|
|
1000
|
+
QApplication.processEvents()
|
|
1001
|
+
except Exception:
|
|
1002
|
+
self.results.ptText.appendHtml(f"The file {pl.Path(export_dir) / util.safeFileName(obs_id)}.TextGrid can not be created.")
|
|
1003
|
+
QApplication.processEvents()
|
|
1004
|
+
|
|
1005
|
+
self.results.ptText.appendHtml(f"Done. {file_count} file(s) were created in {export_dir}.")
|
|
1006
|
+
QApplication.processEvents()
|