boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__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 +1 -1
- boris/__main__.py +1 -1
- boris/about.py +28 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- 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 +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +42 -49
- boris/config.py +141 -65
- boris/config_file.py +58 -67
- boris/connections.py +107 -61
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2373 -1786
- boris/core_qrc.py +15895 -10743
- boris/core_ui.py +943 -798
- boris/db_functions.py +17 -42
- boris/dev.py +109 -8
- boris/dialog.py +482 -236
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +408 -293
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +184 -223
- boris/export_observation.py +74 -100
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +644 -290
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +20 -57
- boris/latency.py +31 -24
- boris/measurement_widget.py +14 -18
- boris/media_file.py +17 -19
- boris/menu_options.py +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +533 -221
- boris/observation_operations.py +1025 -390
- boris/observation_ui.py +572 -362
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -68
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +25 -33
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +306 -83
- boris/preferences_ui.py +684 -227
- boris/project.py +448 -293
- boris/project_functions.py +671 -238
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -198
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +52 -35
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +627 -236
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +95 -29
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/converters.ui +0 -289
- boris/core.qrc +0 -36
- boris/core.ui +0 -1556
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.12.dist-info/METADATA +0 -128
- boris_behav_obs-8.12.dist-info/RECORD +0 -108
- boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/project_functions.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -22,24 +22,25 @@ Copyright 2012-2023 Olivier Friard
|
|
|
22
22
|
import gzip
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
-
import
|
|
26
|
-
import
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import numpy as np
|
|
27
|
+
from pathlib import Path
|
|
27
28
|
import sys
|
|
28
29
|
from decimal import Decimal as dec
|
|
29
30
|
from shutil import copyfile
|
|
30
31
|
from typing import List, Tuple, Dict
|
|
31
32
|
|
|
32
33
|
import tablib
|
|
33
|
-
from
|
|
34
|
-
from
|
|
34
|
+
from PySide6.QtWidgets import QMessageBox, QTableWidgetItem, QAbstractItemView
|
|
35
|
+
from PySide6.QtCore import Qt
|
|
35
36
|
|
|
36
37
|
from . import config as cfg
|
|
37
38
|
from . import db_functions
|
|
38
39
|
from . import dialog
|
|
40
|
+
from . import observation_operations
|
|
39
41
|
from . import portion as I
|
|
40
42
|
from . import utilities as util
|
|
41
43
|
from . import version
|
|
42
|
-
from . import observation_operations
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def check_observation_exhaustivity(
|
|
@@ -56,6 +57,9 @@ def check_observation_exhaustivity(
|
|
|
56
57
|
"""
|
|
57
58
|
|
|
58
59
|
def interval_len(interval: I) -> dec:
|
|
60
|
+
""" "
|
|
61
|
+
returns duration of an interval or a set of intervals
|
|
62
|
+
"""
|
|
59
63
|
if interval.empty:
|
|
60
64
|
return dec(0)
|
|
61
65
|
else:
|
|
@@ -73,18 +77,18 @@ def check_observation_exhaustivity(
|
|
|
73
77
|
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = I.empty()
|
|
74
78
|
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
|
|
75
79
|
|
|
80
|
+
# state event
|
|
76
81
|
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_events_list:
|
|
77
82
|
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]].append(
|
|
78
83
|
event[cfg.EVENT_TIME_FIELD_IDX]
|
|
79
84
|
)
|
|
80
85
|
if len(mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]]) == 2:
|
|
81
|
-
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][
|
|
82
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
83
|
-
] |= I.closedopen(
|
|
86
|
+
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.closedopen(
|
|
84
87
|
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][0],
|
|
85
88
|
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]][1],
|
|
86
89
|
)
|
|
87
90
|
mem_events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = []
|
|
91
|
+
# point event
|
|
88
92
|
else:
|
|
89
93
|
events_interval[event[cfg.EVENT_SUBJECT_FIELD_IDX]][event[cfg.EVENT_BEHAVIOR_FIELD_IDX]] |= I.singleton(
|
|
90
94
|
event[cfg.EVENT_TIME_FIELD_IDX]
|
|
@@ -92,13 +96,13 @@ def check_observation_exhaustivity(
|
|
|
92
96
|
|
|
93
97
|
if events:
|
|
94
98
|
# coding duration
|
|
95
|
-
|
|
99
|
+
event_timestamps = [event[cfg.EVENT_TIME_FIELD_IDX] for event in events]
|
|
100
|
+
obs_theo_dur = max(event_timestamps) - min(event_timestamps)
|
|
96
101
|
else:
|
|
97
102
|
obs_theo_dur = dec("0")
|
|
98
103
|
|
|
99
104
|
total_duration = 0
|
|
100
105
|
for subject in events_interval:
|
|
101
|
-
|
|
102
106
|
tot_behav_for_subject = I.empty()
|
|
103
107
|
for behav in events_interval[subject]:
|
|
104
108
|
tot_behav_for_subject |= events_interval[subject][behav]
|
|
@@ -241,6 +245,18 @@ def check_coded_behaviors_in_obs_list(pj: dict, observations_list: list) -> bool
|
|
|
241
245
|
return False
|
|
242
246
|
|
|
243
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
|
+
|
|
244
260
|
def check_coded_behaviors(pj: dict) -> set:
|
|
245
261
|
"""
|
|
246
262
|
check if behaviors coded in events are defined in ethogram for all observations
|
|
@@ -249,7 +265,7 @@ def check_coded_behaviors(pj: dict) -> set:
|
|
|
249
265
|
pj (dict): project dictionary
|
|
250
266
|
|
|
251
267
|
Returns:
|
|
252
|
-
set: behaviors present in observations that are not
|
|
268
|
+
set: behaviors present in observations that are not defined in ethogram
|
|
253
269
|
"""
|
|
254
270
|
|
|
255
271
|
# set of behaviors defined in ethogram
|
|
@@ -263,9 +279,7 @@ def check_coded_behaviors(pj: dict) -> set:
|
|
|
263
279
|
return set(sorted(behaviors_not_defined))
|
|
264
280
|
|
|
265
281
|
|
|
266
|
-
def check_state_events_obs(
|
|
267
|
-
obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS
|
|
268
|
-
) -> Tuple[bool, str]:
|
|
282
|
+
def check_state_events_obs(obsId: str, ethogram: dict, observation: dict, time_format: str = cfg.HHMMSS) -> Tuple[bool, str]:
|
|
269
283
|
"""
|
|
270
284
|
check state events for the observation obsId
|
|
271
285
|
check if behaviors in observation are defined in ethogram
|
|
@@ -281,7 +295,7 @@ def check_state_events_obs(
|
|
|
281
295
|
tuple (bool, str): if OK True else False , message
|
|
282
296
|
"""
|
|
283
297
|
|
|
284
|
-
out = ""
|
|
298
|
+
out: str = ""
|
|
285
299
|
|
|
286
300
|
# check if behaviors are defined as "state event"
|
|
287
301
|
event_types = {ethogram[idx]["type"] for idx in ethogram}
|
|
@@ -289,16 +303,13 @@ def check_state_events_obs(
|
|
|
289
303
|
if not event_types or event_types == {"Point event"}:
|
|
290
304
|
return (True, "No behavior is defined as `State event`")
|
|
291
305
|
|
|
292
|
-
flagStateEvent = False
|
|
293
306
|
subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
|
|
294
307
|
ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
308
|
+
state_behaviors = set(util.state_behavior_codes(ethogram))
|
|
295
309
|
|
|
296
310
|
for subject in sorted(set(subjects)):
|
|
297
|
-
|
|
298
311
|
behaviors = [
|
|
299
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
300
|
-
for event in observation[cfg.EVENTS]
|
|
301
|
-
if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
312
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
302
313
|
]
|
|
303
314
|
|
|
304
315
|
for behavior in sorted(set(behaviors)):
|
|
@@ -306,32 +317,35 @@ def check_state_events_obs(
|
|
|
306
317
|
# return (False, "The behaviour <b>{}</b> is not defined in the ethogram.<br>".format(behavior))
|
|
307
318
|
continue
|
|
308
319
|
else:
|
|
309
|
-
if
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
]
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
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
|
+
)
|
|
335
349
|
|
|
336
350
|
return (False, out) if out else (True, "No problem detected")
|
|
337
351
|
|
|
@@ -342,6 +356,8 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
|
|
|
342
356
|
use check_state_events_obs function
|
|
343
357
|
"""
|
|
344
358
|
|
|
359
|
+
logging.info("Check state events function")
|
|
360
|
+
|
|
345
361
|
out = ""
|
|
346
362
|
not_paired_obs_list = []
|
|
347
363
|
for obs_id in observations_list:
|
|
@@ -367,21 +383,28 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
|
|
|
367
383
|
if not new_observations_list:
|
|
368
384
|
QMessageBox.warning(None, cfg.programName, "The observation list is empty")
|
|
369
385
|
|
|
386
|
+
logging.info("Check state events done")
|
|
387
|
+
|
|
370
388
|
return False, new_observations_list # no state events are unpaired
|
|
371
389
|
|
|
372
390
|
|
|
373
391
|
def check_project_integrity(
|
|
374
|
-
pj: dict,
|
|
392
|
+
pj: dict,
|
|
393
|
+
time_format: str,
|
|
394
|
+
project_file_name: str,
|
|
395
|
+
media_file_available: bool = True,
|
|
375
396
|
) -> str:
|
|
376
397
|
"""
|
|
377
|
-
check project integrity
|
|
378
|
-
check if behaviors
|
|
379
|
-
check unpaired state events
|
|
380
|
-
check if
|
|
381
|
-
check
|
|
382
|
-
check
|
|
383
|
-
check if media
|
|
384
|
-
check
|
|
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
|
|
385
408
|
|
|
386
409
|
Args:
|
|
387
410
|
pj (dict): BORIS project
|
|
@@ -391,8 +414,12 @@ def check_project_integrity(
|
|
|
391
414
|
|
|
392
415
|
Returns:
|
|
393
416
|
str: message
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
TODO: implement check on order of events (for live and media)
|
|
420
|
+
|
|
394
421
|
"""
|
|
395
|
-
out = ""
|
|
422
|
+
out: str = ""
|
|
396
423
|
|
|
397
424
|
# check if coded behaviors are defined in ethogram
|
|
398
425
|
r = check_coded_behaviors(pj)
|
|
@@ -428,7 +455,7 @@ def check_project_integrity(
|
|
|
428
455
|
out += (
|
|
429
456
|
"The following <b>modifier</b> defined in ethogram "
|
|
430
457
|
"has leading/trailing spaces or special chars: "
|
|
431
|
-
f"<b>{util.replace_leading_trailing_chars(modifier_code
|
|
458
|
+
f"<b>{util.replace_leading_trailing_chars(modifier_code, old_char=' ', new_char='█')}</b>"
|
|
432
459
|
)
|
|
433
460
|
|
|
434
461
|
# check if all media are available
|
|
@@ -439,13 +466,16 @@ def check_project_integrity(
|
|
|
439
466
|
out += "<br><br>" if out else ""
|
|
440
467
|
out += f"Observation: <b>{obs_id}</b><br>{msg}"
|
|
441
468
|
|
|
442
|
-
|
|
469
|
+
out_events: str = ""
|
|
443
470
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
471
|
+
# check if timestamp between -2147483647 and 2147483647
|
|
472
|
+
for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
|
|
473
|
+
timestamp = event[cfg.PJ_OBS_FIELDS[pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE]][cfg.TIME]]
|
|
474
|
+
if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
|
|
475
|
+
out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
|
|
444
476
|
|
|
445
|
-
|
|
446
|
-
if
|
|
447
|
-
continue
|
|
448
|
-
|
|
477
|
+
"""
|
|
478
|
+
# check if media length available
|
|
449
479
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
450
480
|
for nplayer in cfg.ALL_PLAYERS:
|
|
451
481
|
if nplayer in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
@@ -455,6 +485,10 @@ def check_project_integrity(
|
|
|
455
485
|
except KeyError:
|
|
456
486
|
out += "<br><br>" if out else ""
|
|
457
487
|
out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
out += "<br><br>" if out else ""
|
|
491
|
+
out += out_events
|
|
458
492
|
|
|
459
493
|
# check for leading/trailing spaces/special chars in observation id
|
|
460
494
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
@@ -481,28 +515,98 @@ def check_project_integrity(
|
|
|
481
515
|
if not_defined:
|
|
482
516
|
out += "<br><br>" if out else ""
|
|
483
517
|
for var_label in not_defined:
|
|
484
|
-
out +=
|
|
518
|
+
out += (
|
|
519
|
+
f"The independent variable <b>{util.replace_leading_trailing_chars(var_label, ' ', '█')}</b> "
|
|
520
|
+
f"present in {len(not_defined[var_label])} observation(s) is not defined.<br>"
|
|
521
|
+
)
|
|
485
522
|
|
|
486
523
|
# check values of independent variables
|
|
487
524
|
defined_set_var_label: dict = dict(
|
|
488
525
|
[
|
|
489
|
-
(
|
|
526
|
+
(
|
|
527
|
+
pj[cfg.INDEPENDENT_VARIABLES][idx]["label"],
|
|
528
|
+
pj[cfg.INDEPENDENT_VARIABLES][idx]["possible values"],
|
|
529
|
+
)
|
|
490
530
|
for idx in pj.get(cfg.INDEPENDENT_VARIABLES, {})
|
|
491
531
|
if pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "value from set"
|
|
492
532
|
]
|
|
493
533
|
)
|
|
494
534
|
|
|
495
|
-
|
|
535
|
+
tmp_out: str = ""
|
|
496
536
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
497
537
|
if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
|
|
498
538
|
continue
|
|
499
539
|
for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
|
|
500
540
|
if var_label in defined_set_var_label:
|
|
501
|
-
if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[
|
|
502
|
-
|
|
503
|
-
|
|
541
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
|
|
542
|
+
tmp_out += (
|
|
543
|
+
f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
|
|
544
|
+
f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
|
|
545
|
+
)
|
|
546
|
+
if tmp_out:
|
|
547
|
+
out += "<br><br>" if out else ""
|
|
548
|
+
out += tmp_out
|
|
549
|
+
|
|
550
|
+
# check if coded subjects are defined in the subjects list
|
|
551
|
+
tmp_out: str = ""
|
|
552
|
+
subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
|
|
553
|
+
coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
|
|
554
|
+
|
|
555
|
+
for subject in coded_subjects:
|
|
556
|
+
if subject and subject not in subjects_list:
|
|
557
|
+
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>"
|
|
558
|
+
if tmp_out:
|
|
559
|
+
out += "<br><br>" if out else ""
|
|
560
|
+
out += tmp_out
|
|
504
561
|
|
|
505
|
-
|
|
562
|
+
# check if media file have info in media_info section of project
|
|
563
|
+
tmp_out: str = ""
|
|
564
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
565
|
+
for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
566
|
+
for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
|
|
567
|
+
for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
|
|
568
|
+
if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO].get(info, {}):
|
|
569
|
+
tmp_out += f"Observation <b>{obs_id}</b>:<br>"
|
|
570
|
+
tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
|
|
571
|
+
if tmp_out:
|
|
572
|
+
tmp_out += "<br>You should repick the media file to fix this issue."
|
|
573
|
+
out += "<br><br>" if out else ""
|
|
574
|
+
out += tmp_out
|
|
575
|
+
|
|
576
|
+
# check if the number of coded modifiers correspond to the number of sets of modifier
|
|
577
|
+
obs_results: dict = {}
|
|
578
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
579
|
+
for event_idx, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
580
|
+
# event[2]
|
|
581
|
+
for idx in pj[cfg.ETHOGRAM]:
|
|
582
|
+
if pj[cfg.ETHOGRAM][idx]["code"] == event[2]:
|
|
583
|
+
break
|
|
584
|
+
else:
|
|
585
|
+
raise
|
|
586
|
+
if (not event[3]) and not pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]:
|
|
587
|
+
continue
|
|
588
|
+
|
|
589
|
+
if len(event[3].split("|")) != len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]):
|
|
590
|
+
print("behavior", event[2])
|
|
591
|
+
print(f"modifier(s) #{event[3]}#", len(event[3].split("|")))
|
|
592
|
+
print(pj[cfg.ETHOGRAM][idx]["code"], pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])
|
|
593
|
+
print()
|
|
594
|
+
if obs_id not in obs_results:
|
|
595
|
+
obs_results[obs_id] = []
|
|
596
|
+
|
|
597
|
+
obs_results[obs_id].append(
|
|
598
|
+
(
|
|
599
|
+
f"Event #{event_idx}: the coded modifiers for {event[2]} are {len(event[3].split('|'))} "
|
|
600
|
+
f"but {len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])} sets were defined in ethogram."
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
if obs_results:
|
|
605
|
+
out += "<br><br>" if out else ""
|
|
606
|
+
for o in obs_results:
|
|
607
|
+
out += f"<br>Observation <b>{o}</b>:<br>"
|
|
608
|
+
out += "<br>".join(obs_results[o])
|
|
609
|
+
out += "<br><br>"
|
|
506
610
|
|
|
507
611
|
return out
|
|
508
612
|
|
|
@@ -537,14 +641,17 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
537
641
|
return "", ""
|
|
538
642
|
else:
|
|
539
643
|
return (
|
|
540
|
-
f"""<font color="{
|
|
541
|
-
|
|
542
|
-
|
|
644
|
+
f"""<font color="{
|
|
645
|
+
cfg.subtitlesColors[parameters[cfg.SELECTED_SUBJECTS].index(row["subject"]) % len(cfg.subtitlesColors)]
|
|
646
|
+
}">""",
|
|
543
647
|
"</font>",
|
|
544
648
|
)
|
|
545
649
|
|
|
546
650
|
ok, msg, db_connector = db_functions.load_aggregated_events_in_db(
|
|
547
|
-
pj,
|
|
651
|
+
pj,
|
|
652
|
+
parameters[cfg.SELECTED_SUBJECTS],
|
|
653
|
+
selected_observations,
|
|
654
|
+
parameters[cfg.SELECTED_BEHAVIORS],
|
|
548
655
|
)
|
|
549
656
|
if not ok:
|
|
550
657
|
return False, msg
|
|
@@ -588,7 +695,11 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
588
695
|
",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
|
|
589
696
|
",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
|
|
590
697
|
),
|
|
591
|
-
[
|
|
698
|
+
[
|
|
699
|
+
obs_id,
|
|
700
|
+
float(parameters[cfg.START_TIME]),
|
|
701
|
+
float(parameters[cfg.END_TIME]),
|
|
702
|
+
]
|
|
592
703
|
+ parameters[cfg.SELECTED_SUBJECTS]
|
|
593
704
|
+ parameters[cfg.SELECTED_BEHAVIORS],
|
|
594
705
|
)
|
|
@@ -599,14 +710,12 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
599
710
|
modifiers_str = f"\n{row['modifiers'].replace('|', ', ')}"
|
|
600
711
|
else:
|
|
601
712
|
modifiers_str = ""
|
|
602
|
-
out += (
|
|
603
|
-
"{idx}\n" "{start} --> {stop}\n" "{col1}{subject}: {behavior}" "{modifiers}" "{col2}\n\n"
|
|
604
|
-
).format(
|
|
713
|
+
out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
|
|
605
714
|
idx=idx + 1,
|
|
606
715
|
start=util.seconds2time(row["start"]).replace(".", ","),
|
|
607
|
-
stop=util.seconds2time(
|
|
608
|
-
|
|
609
|
-
)
|
|
716
|
+
stop=util.seconds2time(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION).replace(
|
|
717
|
+
".", ","
|
|
718
|
+
),
|
|
610
719
|
col1=col1,
|
|
611
720
|
col2=col2,
|
|
612
721
|
subject=row["subject"],
|
|
@@ -614,13 +723,19 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
614
723
|
modifiers=modifiers_str,
|
|
615
724
|
)
|
|
616
725
|
|
|
617
|
-
file_name =
|
|
726
|
+
file_name = Path(export_dir) / Path(util.safeFileName(obs_id)).with_suffix(".srt")
|
|
618
727
|
|
|
619
728
|
if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
|
|
620
729
|
mem_command = dialog.MessageDialog(
|
|
621
730
|
cfg.programName,
|
|
622
731
|
f"The file {file_name} already exists.",
|
|
623
|
-
[
|
|
732
|
+
[
|
|
733
|
+
cfg.OVERWRITE,
|
|
734
|
+
cfg.OVERWRITE_ALL,
|
|
735
|
+
cfg.SKIP,
|
|
736
|
+
cfg.SKIP_ALL,
|
|
737
|
+
cfg.CANCEL,
|
|
738
|
+
],
|
|
624
739
|
)
|
|
625
740
|
if mem_command == cfg.CANCEL:
|
|
626
741
|
return False, ""
|
|
@@ -635,7 +750,6 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
635
750
|
msg += f"observation: {obs_id}\ngave the following error:\n{str(sys.exc_info()[1])}\n"
|
|
636
751
|
|
|
637
752
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
638
|
-
|
|
639
753
|
for nplayer in cfg.ALL_PLAYERS:
|
|
640
754
|
if not pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer]:
|
|
641
755
|
continue
|
|
@@ -644,7 +758,10 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
644
758
|
try:
|
|
645
759
|
end = init + pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][cfg.LENGTH][media_file]
|
|
646
760
|
except KeyError:
|
|
647
|
-
return
|
|
761
|
+
return (
|
|
762
|
+
False,
|
|
763
|
+
f"The length for media file {media_file} is not available",
|
|
764
|
+
)
|
|
648
765
|
out = ""
|
|
649
766
|
|
|
650
767
|
if parameters["time"] in (cfg.TIME_EVENTS, cfg.TIME_FULL_OBS):
|
|
@@ -670,7 +787,6 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
670
787
|
)
|
|
671
788
|
|
|
672
789
|
else: # arbitrary 'time interval'
|
|
673
|
-
|
|
674
790
|
cursor.execute(
|
|
675
791
|
(
|
|
676
792
|
"SELECT subject, behavior, type, start, stop, modifiers FROM aggregated_events "
|
|
@@ -684,7 +800,13 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
684
800
|
",".join(["?"] * len(parameters[cfg.SELECTED_SUBJECTS])),
|
|
685
801
|
",".join(["?"] * len(parameters[cfg.SELECTED_BEHAVIORS])),
|
|
686
802
|
),
|
|
687
|
-
[
|
|
803
|
+
[
|
|
804
|
+
obs_id,
|
|
805
|
+
init,
|
|
806
|
+
end,
|
|
807
|
+
float(parameters[cfg.START_TIME]),
|
|
808
|
+
float(parameters[cfg.END_TIME]),
|
|
809
|
+
]
|
|
688
810
|
+ parameters[cfg.SELECTED_SUBJECTS]
|
|
689
811
|
+ parameters[cfg.SELECTED_BEHAVIORS],
|
|
690
812
|
)
|
|
@@ -696,14 +818,11 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
696
818
|
else:
|
|
697
819
|
modifiers_str = ""
|
|
698
820
|
|
|
699
|
-
out += (
|
|
700
|
-
"{idx}\n" "{start} --> {stop}\n" "{col1}{subject}: {behavior}" "{modifiers}" "{col2}\n\n"
|
|
701
|
-
).format(
|
|
821
|
+
out += ("{idx}\n{start} --> {stop}\n{col1}{subject}: {behavior}{modifiers}{col2}\n\n").format(
|
|
702
822
|
idx=idx + 1,
|
|
703
823
|
start=util.seconds2time(row["start"] - init).replace(".", ","),
|
|
704
824
|
stop=util.seconds2time(
|
|
705
|
-
(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION)
|
|
706
|
-
- init
|
|
825
|
+
(row["stop"] if row["type"] == cfg.STATE else row["stop"] + cfg.POINT_EVENT_ST_DURATION) - init
|
|
707
826
|
).replace(".", ","),
|
|
708
827
|
col1=col1,
|
|
709
828
|
col2=col2,
|
|
@@ -711,13 +830,19 @@ def create_subtitles(pj: dict, selected_observations: list, parameters: dict, ex
|
|
|
711
830
|
behavior=row["behavior"],
|
|
712
831
|
modifiers=modifiers_str,
|
|
713
832
|
)
|
|
714
|
-
file_name =
|
|
833
|
+
file_name = Path(export_dir) / Path(Path(media_file).stem).with_suffix(".srt")
|
|
715
834
|
|
|
716
835
|
if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL) and file_name.is_file():
|
|
717
836
|
mem_command = dialog.MessageDialog(
|
|
718
837
|
cfg.programName,
|
|
719
838
|
f"The file {file_name} already exists.",
|
|
720
|
-
[
|
|
839
|
+
[
|
|
840
|
+
cfg.OVERWRITE,
|
|
841
|
+
cfg.OVERWRITE_ALL,
|
|
842
|
+
cfg.SKIP,
|
|
843
|
+
cfg.SKIP_ALL,
|
|
844
|
+
cfg.CANCEL,
|
|
845
|
+
],
|
|
721
846
|
)
|
|
722
847
|
if mem_command == cfg.CANCEL:
|
|
723
848
|
return False, ""
|
|
@@ -750,7 +875,13 @@ def export_observations_list(pj: dict, selected_observations: list, file_name: s
|
|
|
750
875
|
"""
|
|
751
876
|
|
|
752
877
|
data = tablib.Dataset()
|
|
753
|
-
data.headers = [
|
|
878
|
+
data.headers = [
|
|
879
|
+
"Observation id",
|
|
880
|
+
"Date",
|
|
881
|
+
"Description",
|
|
882
|
+
"Subjects",
|
|
883
|
+
"Media files/Live observation",
|
|
884
|
+
]
|
|
754
885
|
|
|
755
886
|
indep_var_header = []
|
|
756
887
|
if cfg.INDEPENDENT_VARIABLES in pj:
|
|
@@ -759,10 +890,7 @@ def export_observations_list(pj: dict, selected_observations: list, file_name: s
|
|
|
759
890
|
data.headers.extend(indep_var_header)
|
|
760
891
|
|
|
761
892
|
for obs_id in selected_observations:
|
|
762
|
-
|
|
763
|
-
subjects_list = sorted(
|
|
764
|
-
list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]]))
|
|
765
|
-
)
|
|
893
|
+
subjects_list = sorted(list(set([x[cfg.EVENT_SUBJECT_FIELD_IDX] for x in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]])))
|
|
766
894
|
if "" in subjects_list:
|
|
767
895
|
subjects_list = [cfg.NO_FOCAL_SUBJECT] + subjects_list
|
|
768
896
|
subjects_list.remove("")
|
|
@@ -797,13 +925,13 @@ def export_observations_list(pj: dict, selected_observations: list, file_name: s
|
|
|
797
925
|
+ indep_var
|
|
798
926
|
)
|
|
799
927
|
|
|
800
|
-
if output_format in
|
|
928
|
+
if output_format in (cfg.TSV_EXT, cfg.CSV_EXT, cfg.HTML_EXT):
|
|
801
929
|
try:
|
|
802
930
|
with open(file_name, "wb") as f:
|
|
803
931
|
f.write(str.encode(data.export(output_format)))
|
|
804
932
|
except Exception:
|
|
805
933
|
return False
|
|
806
|
-
if output_format in [
|
|
934
|
+
if output_format in [cfg.ODS_EXT, cfg.XLS_EXT, cfg.XLSX_EXT]:
|
|
807
935
|
try:
|
|
808
936
|
with open(file_name, "wb") as f:
|
|
809
937
|
f.write(data.export(output_format))
|
|
@@ -830,13 +958,9 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
|
|
|
830
958
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
831
959
|
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
832
960
|
try:
|
|
833
|
-
|
|
961
|
+
Path(img_dir).relative_to(Path(project_file_name).parent)
|
|
834
962
|
except ValueError:
|
|
835
|
-
if (
|
|
836
|
-
pl.Path(img_dir).is_absolute()
|
|
837
|
-
or not (pl.Path(project_file_name).parent / pl.Path(img_dir)).is_dir()
|
|
838
|
-
):
|
|
839
|
-
|
|
963
|
+
if Path(img_dir).is_absolute() or not (Path(project_file_name).parent / Path(img_dir)).is_dir():
|
|
840
964
|
QMessageBox.critical(
|
|
841
965
|
None,
|
|
842
966
|
cfg.programName,
|
|
@@ -849,35 +973,29 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
|
|
|
849
973
|
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
850
974
|
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
851
975
|
try:
|
|
852
|
-
|
|
976
|
+
Path(media_file).relative_to(Path(project_file_name).parent)
|
|
853
977
|
except ValueError:
|
|
854
|
-
|
|
855
|
-
if (
|
|
856
|
-
pl.Path(media_file).is_absolute()
|
|
857
|
-
or not (pl.Path(project_file_name).parent / pl.Path(media_file)).is_file()
|
|
858
|
-
):
|
|
859
|
-
|
|
978
|
+
if Path(media_file).is_absolute() or not (Path(project_file_name).parent / Path(media_file)).is_file():
|
|
860
979
|
QMessageBox.critical(
|
|
861
980
|
None,
|
|
862
981
|
cfg.programName,
|
|
863
|
-
|
|
982
|
+
(
|
|
983
|
+
f"Observation <b>{obs_id}</b>:"
|
|
984
|
+
f"<br>the path of <b>{media_file}</b> is not relative to <b>{project_file_name}</b>"
|
|
985
|
+
),
|
|
864
986
|
)
|
|
865
987
|
return False
|
|
866
988
|
|
|
867
989
|
# set media path and image dir relative to project dir
|
|
868
990
|
flag_changed = False
|
|
869
991
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
870
|
-
|
|
871
992
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
872
993
|
new_dir_list = []
|
|
873
994
|
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
874
995
|
try:
|
|
875
|
-
new_dir_list.append(str(
|
|
996
|
+
new_dir_list.append(str(Path(img_dir).relative_to(Path(project_file_name).parent)))
|
|
876
997
|
except ValueError:
|
|
877
|
-
if (
|
|
878
|
-
not pl.Path(img_dir).is_absolute()
|
|
879
|
-
and (pl.Path(project_file_name).parent / pl.Path(img_dir)).is_dir()
|
|
880
|
-
):
|
|
998
|
+
if not Path(img_dir).is_absolute() and (Path(project_file_name).parent / Path(img_dir)).is_dir():
|
|
881
999
|
new_dir_list.append(img_dir)
|
|
882
1000
|
|
|
883
1001
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] != new_dir_list:
|
|
@@ -889,26 +1007,28 @@ def set_media_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
|
|
|
889
1007
|
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
890
1008
|
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
891
1009
|
try:
|
|
892
|
-
p = str(
|
|
1010
|
+
p = str(Path(media_file).relative_to(Path(project_file_name).parent))
|
|
893
1011
|
except ValueError:
|
|
894
|
-
if (
|
|
895
|
-
not pl.Path(media_file).is_absolute()
|
|
896
|
-
and (pl.Path(project_file_name).parent / pl.Path(media_file)).is_file()
|
|
897
|
-
):
|
|
1012
|
+
if not Path(media_file).is_absolute() and (Path(project_file_name).parent / Path(media_file)).is_file():
|
|
898
1013
|
p = media_file
|
|
899
1014
|
if p != media_file:
|
|
900
1015
|
flag_changed = True
|
|
901
1016
|
pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
|
|
902
1017
|
if cfg.MEDIA_INFO in pj[cfg.OBSERVATIONS][obs_id]:
|
|
903
|
-
for info in [
|
|
1018
|
+
for info in [
|
|
1019
|
+
cfg.LENGTH,
|
|
1020
|
+
cfg.HAS_AUDIO,
|
|
1021
|
+
cfg.HAS_VIDEO,
|
|
1022
|
+
cfg.FPS,
|
|
1023
|
+
]:
|
|
904
1024
|
if (
|
|
905
1025
|
info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
|
|
906
1026
|
and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
|
|
907
1027
|
):
|
|
908
1028
|
# add new file path
|
|
909
|
-
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][
|
|
910
|
-
|
|
911
|
-
][
|
|
1029
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
|
|
1030
|
+
cfg.MEDIA_INFO
|
|
1031
|
+
][info][media_file]
|
|
912
1032
|
# remove old path
|
|
913
1033
|
del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
|
|
914
1034
|
return flag_changed
|
|
@@ -930,35 +1050,32 @@ def set_data_paths_relative_to_project_dir(pj: dict, project_file_name: str) ->
|
|
|
930
1050
|
for _, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
|
|
931
1051
|
if cfg.FILE_PATH in v:
|
|
932
1052
|
try:
|
|
933
|
-
|
|
1053
|
+
Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent)
|
|
934
1054
|
except ValueError:
|
|
935
1055
|
# check if file is in project dir
|
|
936
|
-
if (
|
|
937
|
-
pl.Path(v[cfg.FILE_PATH]).is_absolute()
|
|
938
|
-
or not (pl.Path(project_file_name).parent / pl.Path(v[cfg.FILE_PATH])).is_file()
|
|
939
|
-
):
|
|
1056
|
+
if Path(v[cfg.FILE_PATH]).is_absolute() or not (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
|
|
940
1057
|
QMessageBox.critical(
|
|
941
1058
|
None,
|
|
942
1059
|
cfg.programName,
|
|
943
|
-
|
|
1060
|
+
(
|
|
1061
|
+
f"Observation <b>{obs_id}</b>:"
|
|
1062
|
+
f"<br>the path of <b>{v[cfg.FILE_PATH]}</b> "
|
|
1063
|
+
f"is not relative to <b>{project_file_name}</b>."
|
|
1064
|
+
),
|
|
944
1065
|
)
|
|
945
1066
|
return False
|
|
946
1067
|
|
|
947
1068
|
flag_changed = False
|
|
948
1069
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
949
|
-
|
|
950
1070
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
|
|
951
1071
|
continue
|
|
952
1072
|
for idx, v in pj[cfg.OBSERVATIONS][obs_id].get(cfg.PLOT_DATA, {}).items():
|
|
953
1073
|
if cfg.FILE_PATH in v:
|
|
954
1074
|
try:
|
|
955
|
-
p = str(
|
|
1075
|
+
p = str(Path(v[cfg.FILE_PATH]).relative_to(Path(project_file_name).parent))
|
|
956
1076
|
except ValueError:
|
|
957
1077
|
# check if file is in project dir
|
|
958
|
-
if (
|
|
959
|
-
not pl.Path(v[cfg.FILE_PATH]).is_absolute()
|
|
960
|
-
and (pl.Path(project_file_name).parent / pl.Path(v[cfg.FILE_PATH])).is_file()
|
|
961
|
-
):
|
|
1078
|
+
if not Path(v[cfg.FILE_PATH]).is_absolute() and (Path(project_file_name).parent / Path(v[cfg.FILE_PATH])).is_file():
|
|
962
1079
|
p = v[cfg.FILE_PATH]
|
|
963
1080
|
|
|
964
1081
|
if p != v[cfg.FILE_PATH]:
|
|
@@ -980,13 +1097,12 @@ def remove_data_files_path(pj: dict) -> None:
|
|
|
980
1097
|
"""
|
|
981
1098
|
|
|
982
1099
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
983
|
-
|
|
984
1100
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] != cfg.MEDIA:
|
|
985
1101
|
continue
|
|
986
1102
|
if cfg.PLOT_DATA in pj[cfg.OBSERVATIONS][obs_id]:
|
|
987
1103
|
for idx in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA]:
|
|
988
1104
|
if "file_path" in pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]:
|
|
989
|
-
p = str(
|
|
1105
|
+
p = str(Path(pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]).name)
|
|
990
1106
|
if p != pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"]:
|
|
991
1107
|
pj[cfg.OBSERVATIONS][obs_id][cfg.PLOT_DATA][idx]["file_path"] = p
|
|
992
1108
|
|
|
@@ -1006,17 +1122,16 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
|
|
|
1006
1122
|
file_not_found = []
|
|
1007
1123
|
# check if media and images dir
|
|
1008
1124
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1009
|
-
|
|
1010
1125
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
1011
1126
|
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
1012
|
-
if full_path(
|
|
1127
|
+
if full_path(Path(img_dir).name, project_file_name) == "":
|
|
1013
1128
|
file_not_found.append(img_dir)
|
|
1014
1129
|
|
|
1015
1130
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
1016
1131
|
for n_player in cfg.ALL_PLAYERS:
|
|
1017
1132
|
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
1018
1133
|
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
1019
|
-
if full_path(
|
|
1134
|
+
if full_path(Path(media_file).name, project_file_name) == "":
|
|
1020
1135
|
file_not_found.append(media_file)
|
|
1021
1136
|
|
|
1022
1137
|
file_not_found = set(file_not_found)
|
|
@@ -1037,33 +1152,37 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
|
|
|
1037
1152
|
|
|
1038
1153
|
flag_changed = False
|
|
1039
1154
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1040
|
-
|
|
1041
1155
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.IMAGES:
|
|
1042
1156
|
new_img_dir_list = []
|
|
1043
1157
|
for img_dir in pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST]:
|
|
1044
|
-
if img_dir !=
|
|
1158
|
+
if img_dir != Path(img_dir).name:
|
|
1045
1159
|
flag_changed = True
|
|
1046
|
-
new_img_dir_list.append(str(
|
|
1160
|
+
new_img_dir_list.append(str(Path(img_dir).name))
|
|
1047
1161
|
pj[cfg.OBSERVATIONS][obs_id][cfg.DIRECTORIES_LIST] = new_img_dir_list
|
|
1048
1162
|
|
|
1049
1163
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
1050
1164
|
for n_player in cfg.ALL_PLAYERS:
|
|
1051
1165
|
if n_player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
1052
1166
|
for idx, media_file in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player]):
|
|
1053
|
-
p =
|
|
1167
|
+
p = Path(media_file).name
|
|
1054
1168
|
if p != media_file:
|
|
1055
1169
|
flag_changed = True
|
|
1056
1170
|
pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][n_player][idx] = p
|
|
1057
1171
|
if cfg.MEDIA_INFO in pj[cfg.OBSERVATIONS][obs_id]:
|
|
1058
|
-
for info in [
|
|
1172
|
+
for info in [
|
|
1173
|
+
cfg.LENGTH,
|
|
1174
|
+
cfg.HAS_AUDIO,
|
|
1175
|
+
cfg.HAS_VIDEO,
|
|
1176
|
+
cfg.FPS,
|
|
1177
|
+
]:
|
|
1059
1178
|
if (
|
|
1060
1179
|
info in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO]
|
|
1061
1180
|
and media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]
|
|
1062
1181
|
):
|
|
1063
1182
|
# add new file path
|
|
1064
|
-
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][
|
|
1065
|
-
|
|
1066
|
-
][
|
|
1183
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][p] = pj[cfg.OBSERVATIONS][obs_id][
|
|
1184
|
+
cfg.MEDIA_INFO
|
|
1185
|
+
][info][media_file]
|
|
1067
1186
|
# remove old path
|
|
1068
1187
|
del pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info][media_file]
|
|
1069
1188
|
|
|
@@ -1073,7 +1192,7 @@ def remove_media_files_path(pj: dict, project_file_name: str) -> bool:
|
|
|
1073
1192
|
def full_path(path: str, project_file_name: str) -> str:
|
|
1074
1193
|
"""
|
|
1075
1194
|
returns the media/data full path or the images directory full path
|
|
1076
|
-
add path of BORIS project if media/data with relative path
|
|
1195
|
+
add path of BORIS project if media/data/pictures dir with relative path
|
|
1077
1196
|
|
|
1078
1197
|
Args:
|
|
1079
1198
|
path (str): file path or images directory path
|
|
@@ -1083,12 +1202,12 @@ def full_path(path: str, project_file_name: str) -> str:
|
|
|
1083
1202
|
str: full path
|
|
1084
1203
|
"""
|
|
1085
1204
|
|
|
1086
|
-
source_path =
|
|
1205
|
+
source_path = Path(path)
|
|
1087
1206
|
if source_path.exists():
|
|
1088
1207
|
return str(source_path)
|
|
1089
1208
|
else:
|
|
1090
1209
|
# check relative path (to project path)
|
|
1091
|
-
project_path =
|
|
1210
|
+
project_path = Path(project_file_name)
|
|
1092
1211
|
if (project_path.parent / source_path).exists():
|
|
1093
1212
|
return str(project_path.parent / source_path)
|
|
1094
1213
|
else:
|
|
@@ -1107,15 +1226,22 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
|
|
|
1107
1226
|
"""
|
|
1108
1227
|
if not observation[cfg.EVENTS]:
|
|
1109
1228
|
return (dec("0.0"), dec("0.0"))
|
|
1229
|
+
|
|
1110
1230
|
if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
1231
|
+
event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
|
|
1232
|
+
|
|
1111
1233
|
return (
|
|
1112
|
-
min(
|
|
1113
|
-
max(
|
|
1234
|
+
min(event_timestamp),
|
|
1235
|
+
max(event_timestamp),
|
|
1114
1236
|
)
|
|
1115
1237
|
if observation[cfg.TYPE] == cfg.IMAGES:
|
|
1116
1238
|
events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
|
|
1117
|
-
|
|
1118
|
-
|
|
1239
|
+
# test if indexes contain NA
|
|
1240
|
+
try:
|
|
1241
|
+
dec(min(events))
|
|
1242
|
+
return (dec(min(events)), dec(max(events)))
|
|
1243
|
+
except Exception:
|
|
1244
|
+
return (dec("NaN"), dec("NaN"))
|
|
1119
1245
|
|
|
1120
1246
|
|
|
1121
1247
|
def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
|
|
@@ -1133,7 +1259,6 @@ def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple
|
|
|
1133
1259
|
|
|
1134
1260
|
events_flagged: list = []
|
|
1135
1261
|
for idx, event in enumerate(events):
|
|
1136
|
-
|
|
1137
1262
|
_, subject, code, modifier = event[: cfg.EVENT_MODIFIER_FIELD_IDX + 1]
|
|
1138
1263
|
|
|
1139
1264
|
# check if code is state
|
|
@@ -1191,7 +1316,7 @@ def extract_observed_subjects(pj: dict, selected_observations: list) -> list:
|
|
|
1191
1316
|
return list(set(observed_subjects))
|
|
1192
1317
|
|
|
1193
1318
|
|
|
1194
|
-
def open_project_json(
|
|
1319
|
+
def open_project_json(project_file_name: str) -> tuple:
|
|
1195
1320
|
"""
|
|
1196
1321
|
open BORIS project file in json format or GZ compressed json format
|
|
1197
1322
|
|
|
@@ -1205,31 +1330,56 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1205
1330
|
str: message
|
|
1206
1331
|
"""
|
|
1207
1332
|
|
|
1208
|
-
logging.debug(f"
|
|
1333
|
+
logging.debug(f"open_project_json function: {project_file_name}")
|
|
1209
1334
|
|
|
1210
|
-
projectChanged = False
|
|
1211
|
-
msg = ""
|
|
1335
|
+
projectChanged: bool = False
|
|
1336
|
+
msg: str = ""
|
|
1212
1337
|
|
|
1213
|
-
if not
|
|
1214
|
-
return
|
|
1338
|
+
if not Path(project_file_name).is_file():
|
|
1339
|
+
return (
|
|
1340
|
+
project_file_name,
|
|
1341
|
+
projectChanged,
|
|
1342
|
+
{"error": f"File {project_file_name} not found"},
|
|
1343
|
+
msg,
|
|
1344
|
+
)
|
|
1215
1345
|
|
|
1216
1346
|
try:
|
|
1217
|
-
if
|
|
1218
|
-
file_in = gzip.open(
|
|
1347
|
+
if project_file_name.endswith(".boris.gz"):
|
|
1348
|
+
file_in = gzip.open(project_file_name, mode="rt", encoding="utf-8")
|
|
1219
1349
|
else:
|
|
1220
|
-
file_in = open(
|
|
1350
|
+
file_in = open(project_file_name, "r")
|
|
1221
1351
|
file_content = file_in.read()
|
|
1222
1352
|
except PermissionError:
|
|
1223
|
-
return
|
|
1353
|
+
return (
|
|
1354
|
+
project_file_name,
|
|
1355
|
+
projectChanged,
|
|
1356
|
+
{"error": f"File {project_file_name}: Permission denied"},
|
|
1357
|
+
msg,
|
|
1358
|
+
)
|
|
1224
1359
|
except Exception:
|
|
1225
|
-
return
|
|
1360
|
+
return (
|
|
1361
|
+
project_file_name,
|
|
1362
|
+
projectChanged,
|
|
1363
|
+
{"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
|
|
1364
|
+
msg,
|
|
1365
|
+
)
|
|
1226
1366
|
|
|
1227
1367
|
try:
|
|
1228
1368
|
pj = json.loads(file_content)
|
|
1229
1369
|
except json.decoder.JSONDecodeError:
|
|
1230
|
-
return
|
|
1370
|
+
return (
|
|
1371
|
+
project_file_name,
|
|
1372
|
+
projectChanged,
|
|
1373
|
+
{"error": "This project file seems corrupted"},
|
|
1374
|
+
msg,
|
|
1375
|
+
)
|
|
1231
1376
|
except Exception:
|
|
1232
|
-
return
|
|
1377
|
+
return (
|
|
1378
|
+
project_file_name,
|
|
1379
|
+
projectChanged,
|
|
1380
|
+
{"error": f"Error on file {project_file_name}: {sys.exc_info()[1]}"},
|
|
1381
|
+
msg,
|
|
1382
|
+
)
|
|
1233
1383
|
|
|
1234
1384
|
# transform time to decimal
|
|
1235
1385
|
pj = util.convert_time_to_decimal(pj)
|
|
@@ -1247,11 +1397,9 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1247
1397
|
projectChanged = True
|
|
1248
1398
|
|
|
1249
1399
|
# check if project file version is newer than current BORIS project file version
|
|
1250
|
-
if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(
|
|
1251
|
-
version.__version__
|
|
1252
|
-
):
|
|
1400
|
+
if cfg.PROJECT_VERSION in pj and util.versiontuple(pj[cfg.PROJECT_VERSION]) > util.versiontuple(version.__version__):
|
|
1253
1401
|
return (
|
|
1254
|
-
|
|
1402
|
+
project_file_name,
|
|
1255
1403
|
projectChanged,
|
|
1256
1404
|
{
|
|
1257
1405
|
"error": (
|
|
@@ -1264,13 +1412,11 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1264
1412
|
|
|
1265
1413
|
# check if old version v. 0 *.obs
|
|
1266
1414
|
if cfg.PROJECT_VERSION not in pj:
|
|
1267
|
-
|
|
1268
1415
|
# convert VIDEO, AUDIO -> MEDIA
|
|
1269
1416
|
pj[cfg.PROJECT_VERSION] = cfg.project_format_version
|
|
1270
1417
|
projectChanged = True
|
|
1271
1418
|
|
|
1272
1419
|
for obs in [x for x in pj[cfg.OBSERVATIONS]]:
|
|
1273
|
-
|
|
1274
1420
|
# remove 'replace audio' key
|
|
1275
1421
|
if "replace audio" in pj[cfg.OBSERVATIONS][obs]:
|
|
1276
1422
|
del pj[cfg.OBSERVATIONS][obs]["replace audio"]
|
|
@@ -1279,6 +1425,7 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1279
1425
|
pj[cfg.OBSERVATIONS][obs][cfg.TYPE] = cfg.MEDIA
|
|
1280
1426
|
|
|
1281
1427
|
# convert old media list in new one
|
|
1428
|
+
d1: dict = {}
|
|
1282
1429
|
if len(pj[cfg.OBSERVATIONS][obs][cfg.FILE]):
|
|
1283
1430
|
d1 = {cfg.PLAYER1: [pj[cfg.OBSERVATIONS][obs][cfg.FILE][0]]}
|
|
1284
1431
|
|
|
@@ -1296,7 +1443,7 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1296
1443
|
f"The project file was converted to the new format (v. {cfg.project_format_version}) in use with your version of BORIS.<br>"
|
|
1297
1444
|
"Choose a new file name for saving it."
|
|
1298
1445
|
)
|
|
1299
|
-
|
|
1446
|
+
project_file_name = ""
|
|
1300
1447
|
|
|
1301
1448
|
# update modifiers to JSON format
|
|
1302
1449
|
|
|
@@ -1320,16 +1467,26 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1320
1467
|
pj[cfg.ETHOGRAM][idx]["modifiers"] = {}
|
|
1321
1468
|
|
|
1322
1469
|
if not project_lowerthan4:
|
|
1323
|
-
msg = "The project version was updated from {} to {}".format(
|
|
1324
|
-
pj[cfg.PROJECT_VERSION], cfg.project_format_version
|
|
1325
|
-
)
|
|
1470
|
+
msg = "The project version was updated from {} to {}".format(pj[cfg.PROJECT_VERSION], cfg.project_format_version)
|
|
1326
1471
|
pj[cfg.PROJECT_VERSION] = cfg.project_format_version
|
|
1327
1472
|
projectChanged = True
|
|
1328
1473
|
|
|
1474
|
+
# check if behavioral categories are stored as a list
|
|
1475
|
+
if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
|
|
1476
|
+
if isinstance(pj[cfg.BEHAVIORAL_CATEGORIES_CONF], list):
|
|
1477
|
+
# convert to dict
|
|
1478
|
+
pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {str(idx): {"name": bc} for idx, bc in enumerate(pj[cfg.BEHAVIORAL_CATEGORIES_CONF])}
|
|
1479
|
+
logging.info("Behavioral categories was converted from a list to a dictionary")
|
|
1480
|
+
projectChanged = True
|
|
1481
|
+
else:
|
|
1482
|
+
pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict()
|
|
1483
|
+
projectChanged = True
|
|
1484
|
+
|
|
1485
|
+
|
|
1329
1486
|
# add category key if not found
|
|
1330
1487
|
for idx in pj[cfg.ETHOGRAM]:
|
|
1331
|
-
if
|
|
1332
|
-
pj[cfg.ETHOGRAM][idx][
|
|
1488
|
+
if cfg.BEHAVIOR_CATEGORY not in pj[cfg.ETHOGRAM][idx]:
|
|
1489
|
+
pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] = ""
|
|
1333
1490
|
|
|
1334
1491
|
# if one file is present in player #1 -> set "media_info" key with value of media_file_info
|
|
1335
1492
|
for obs in pj[cfg.OBSERVATIONS]:
|
|
@@ -1340,16 +1497,21 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1340
1497
|
cfg.HAS_VIDEO: {},
|
|
1341
1498
|
cfg.HAS_AUDIO: {},
|
|
1342
1499
|
}
|
|
1343
|
-
for player in
|
|
1500
|
+
for player in (cfg.PLAYER1, cfg.PLAYER2):
|
|
1344
1501
|
# fix bug Anne Maijer 2017-07-17
|
|
1345
1502
|
if pj[cfg.OBSERVATIONS][obs][cfg.FILE] == []:
|
|
1346
1503
|
pj[cfg.OBSERVATIONS][obs][cfg.FILE] = {"1": [], "2": []}
|
|
1347
1504
|
|
|
1348
1505
|
for media_file_path in pj[cfg.OBSERVATIONS][obs]["file"][player]:
|
|
1349
1506
|
# FIX: ffmpeg path
|
|
1350
|
-
ret,
|
|
1507
|
+
ret, ffmpeg_bin = util.check_ffmpeg_path()
|
|
1351
1508
|
if not ret:
|
|
1352
|
-
return
|
|
1509
|
+
return (
|
|
1510
|
+
project_file_name,
|
|
1511
|
+
projectChanged,
|
|
1512
|
+
{"error": "FFmpeg path not found"},
|
|
1513
|
+
"",
|
|
1514
|
+
)
|
|
1353
1515
|
else:
|
|
1354
1516
|
ffmpeg_bin = msg
|
|
1355
1517
|
|
|
@@ -1360,7 +1522,7 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1360
1522
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS][media_file_path] = float(r["fps"])
|
|
1361
1523
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_VIDEO][media_file_path] = r["has_video"]
|
|
1362
1524
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.HAS_AUDIO][media_file_path] = r["has_audio"]
|
|
1363
|
-
|
|
1525
|
+
projectChanged = True
|
|
1364
1526
|
else: # file path not found
|
|
1365
1527
|
if (
|
|
1366
1528
|
cfg.MEDIA_FILE_INFO in pj[cfg.OBSERVATIONS][obs]
|
|
@@ -1372,10 +1534,7 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1372
1534
|
# duration
|
|
1373
1535
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO] = {
|
|
1374
1536
|
cfg.LENGTH: {
|
|
1375
|
-
media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key][
|
|
1376
|
-
"video_length"
|
|
1377
|
-
]
|
|
1378
|
-
/ 1000
|
|
1537
|
+
media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000
|
|
1379
1538
|
}
|
|
1380
1539
|
}
|
|
1381
1540
|
projectChanged = True
|
|
@@ -1383,13 +1542,8 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1383
1542
|
# FPS
|
|
1384
1543
|
if "nframe" in pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]:
|
|
1385
1544
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {
|
|
1386
|
-
media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key][
|
|
1387
|
-
|
|
1388
|
-
]
|
|
1389
|
-
/ (
|
|
1390
|
-
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"]
|
|
1391
|
-
/ 1000
|
|
1392
|
-
)
|
|
1545
|
+
media_file_path: pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["nframe"]
|
|
1546
|
+
/ (pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_FILE_INFO][media_md5_key]["video_length"] / 1000)
|
|
1393
1547
|
}
|
|
1394
1548
|
else:
|
|
1395
1549
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.FPS] = {media_file_path: 0}
|
|
@@ -1405,9 +1559,7 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1405
1559
|
for player in pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
|
|
1406
1560
|
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET][player] = 0.0
|
|
1407
1561
|
if pj[cfg.OBSERVATIONS][obs]["time offset second player"]:
|
|
1408
|
-
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(
|
|
1409
|
-
pj[cfg.OBSERVATIONS][obs]["time offset second player"]
|
|
1410
|
-
)
|
|
1562
|
+
pj[cfg.OBSERVATIONS][obs][cfg.MEDIA_INFO][cfg.OFFSET]["2"] = float(pj[cfg.OBSERVATIONS][obs]["time offset second player"])
|
|
1411
1563
|
|
|
1412
1564
|
del pj[cfg.OBSERVATIONS][obs]["time offset second player"]
|
|
1413
1565
|
project_lowerthan7 = True
|
|
@@ -1421,36 +1573,41 @@ def open_project_json(projectFileName: str) -> tuple:
|
|
|
1421
1573
|
projectChanged = True
|
|
1422
1574
|
|
|
1423
1575
|
if project_lowerthan7:
|
|
1424
|
-
|
|
1425
1576
|
msg = f"The project was updated to the current project version ({cfg.project_format_version})."
|
|
1426
1577
|
|
|
1427
1578
|
try:
|
|
1428
|
-
old_project_file_name =
|
|
1429
|
-
copyfile(
|
|
1579
|
+
old_project_file_name = project_file_name.replace(".boris", f".v{pj['project_format_version']}.boris")
|
|
1580
|
+
copyfile(project_file_name, old_project_file_name)
|
|
1430
1581
|
msg += f"\n\nThe old file project was saved as {old_project_file_name}"
|
|
1431
1582
|
except Exception:
|
|
1432
|
-
|
|
1583
|
+
QMessageBox.critical(cfg.programName, f"Error saving old project to {old_project_file_name}")
|
|
1433
1584
|
|
|
1434
1585
|
pj[cfg.PROJECT_VERSION] = cfg.project_format_version
|
|
1435
1586
|
|
|
1436
|
-
|
|
1587
|
+
# sort events by time asc
|
|
1588
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1589
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
|
|
1590
|
+
# sort events list using the first 3 items (time, subject, behavior)
|
|
1591
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
1592
|
+
|
|
1593
|
+
return project_file_name, projectChanged, pj, msg
|
|
1437
1594
|
|
|
1438
1595
|
|
|
1439
|
-
def event_type(code: str, ethogram: dict) -> str:
|
|
1596
|
+
def event_type(code: str, ethogram: dict) -> str | None:
|
|
1440
1597
|
"""
|
|
1441
|
-
returns type of event for code
|
|
1598
|
+
returns type of event for behavior code
|
|
1442
1599
|
|
|
1443
1600
|
Args:
|
|
1444
1601
|
ethogram (dict); ethogram of project
|
|
1445
1602
|
code (str): behavior code
|
|
1446
1603
|
|
|
1447
1604
|
Returns:
|
|
1448
|
-
str:
|
|
1605
|
+
str: behavior type
|
|
1449
1606
|
"""
|
|
1450
1607
|
|
|
1451
1608
|
for idx in ethogram:
|
|
1452
1609
|
if ethogram[idx][cfg.BEHAVIOR_CODE] == code:
|
|
1453
|
-
return ethogram[idx][cfg.TYPE]
|
|
1610
|
+
return ethogram[idx][cfg.TYPE]
|
|
1454
1611
|
return None
|
|
1455
1612
|
|
|
1456
1613
|
|
|
@@ -1459,7 +1616,6 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
|
|
|
1459
1616
|
fix unpaired state events in observation
|
|
1460
1617
|
|
|
1461
1618
|
Args:
|
|
1462
|
-
obsId (str): observation id
|
|
1463
1619
|
ethogram (dict): ethogram dictionary
|
|
1464
1620
|
observation (dict): observation dictionary
|
|
1465
1621
|
fix_at_time (Decimal): time to fix the unpaired events
|
|
@@ -1468,31 +1624,27 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
|
|
|
1468
1624
|
list: list of events with state events fixed
|
|
1469
1625
|
"""
|
|
1470
1626
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
subjects = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
|
|
1475
|
-
ethogram_behaviors = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
1627
|
+
closing_events_to_add: list = []
|
|
1628
|
+
subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in observation[cfg.EVENTS]]
|
|
1629
|
+
ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
1476
1630
|
|
|
1477
1631
|
for subject in sorted(set(subjects)):
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
1481
|
-
for event in observation[cfg.EVENTS]
|
|
1482
|
-
if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1632
|
+
behaviors: list = [
|
|
1633
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1483
1634
|
]
|
|
1484
1635
|
|
|
1485
1636
|
for behavior in sorted(set(behaviors)):
|
|
1486
|
-
if (behavior in ethogram_behaviors) and (
|
|
1487
|
-
|
|
1637
|
+
if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
|
|
1488
1638
|
lst, memTime = [], {}
|
|
1489
1639
|
for event in [
|
|
1490
1640
|
event
|
|
1491
1641
|
for event in observation[cfg.EVENTS]
|
|
1492
1642
|
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1493
1643
|
]:
|
|
1494
|
-
|
|
1495
|
-
|
|
1644
|
+
behav_modif = [
|
|
1645
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
1646
|
+
event[cfg.EVENT_MODIFIER_FIELD_IDX],
|
|
1647
|
+
]
|
|
1496
1648
|
|
|
1497
1649
|
if behav_modif in lst:
|
|
1498
1650
|
lst.remove(behav_modif)
|
|
@@ -1502,11 +1654,78 @@ def fix_unpaired_state_events(ethogram: dict, observation: dict, fix_at_time: de
|
|
|
1502
1654
|
memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
|
|
1503
1655
|
|
|
1504
1656
|
for event in lst:
|
|
1657
|
+
last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
|
|
1658
|
+
|
|
1659
|
+
closing_events_to_add.append(
|
|
1660
|
+
[
|
|
1661
|
+
last_event_time + dec("0.001"),
|
|
1662
|
+
subject,
|
|
1663
|
+
behavior,
|
|
1664
|
+
event[1], # modifiers
|
|
1665
|
+
"Event automatically added by the fix unpaired state events function",
|
|
1666
|
+
cfg.NA, # frame index
|
|
1667
|
+
]
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1670
|
+
return closing_events_to_add
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -> list:
|
|
1674
|
+
"""
|
|
1675
|
+
fix unpaired state events in events list
|
|
1676
|
+
|
|
1677
|
+
Args:
|
|
1678
|
+
ethogram (dict): ethogram dictionary
|
|
1679
|
+
events (list): list of events
|
|
1680
|
+
fix_at_time (Decimal): time to fix the unpaired events
|
|
1681
|
+
|
|
1682
|
+
Returns:
|
|
1683
|
+
list: list of events with state events fixed
|
|
1684
|
+
"""
|
|
1685
|
+
|
|
1686
|
+
logging.debug("fix_unpaired_state_events2 function")
|
|
1687
|
+
|
|
1688
|
+
closing_events_to_add: list = []
|
|
1689
|
+
subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
|
|
1690
|
+
ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|
|
1691
|
+
|
|
1692
|
+
for subject in sorted(set(subjects)):
|
|
1693
|
+
behaviors: list = [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in events if event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject]
|
|
1694
|
+
|
|
1695
|
+
for behavior in sorted(set(behaviors)):
|
|
1696
|
+
if (behavior in ethogram_behaviors) and (event_type(behavior, ethogram) in cfg.STATE_EVENT_TYPES):
|
|
1697
|
+
lst: list = []
|
|
1698
|
+
memTime: dict = {}
|
|
1699
|
+
for event in [
|
|
1700
|
+
event
|
|
1701
|
+
for event in events
|
|
1702
|
+
if event[cfg.EVENT_BEHAVIOR_FIELD_IDX] == behavior and event[cfg.EVENT_SUBJECT_FIELD_IDX] == subject
|
|
1703
|
+
]:
|
|
1704
|
+
behav_modif = [
|
|
1705
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
1706
|
+
event[cfg.EVENT_MODIFIER_FIELD_IDX],
|
|
1707
|
+
]
|
|
1505
1708
|
|
|
1709
|
+
if behav_modif in lst:
|
|
1710
|
+
lst.remove(behav_modif)
|
|
1711
|
+
del memTime[str(behav_modif)]
|
|
1712
|
+
else:
|
|
1713
|
+
lst.append(behav_modif)
|
|
1714
|
+
memTime[str(behav_modif)] = event[cfg.EVENT_TIME_FIELD_IDX]
|
|
1715
|
+
|
|
1716
|
+
for event in lst:
|
|
1506
1717
|
last_event_time = max([fix_at_time] + [x[0] for x in closing_events_to_add])
|
|
1507
1718
|
|
|
1508
1719
|
closing_events_to_add.append(
|
|
1509
|
-
[
|
|
1720
|
+
[
|
|
1721
|
+
# last_event_time + dec("0.001"),
|
|
1722
|
+
last_event_time,
|
|
1723
|
+
subject,
|
|
1724
|
+
behavior,
|
|
1725
|
+
event[1], # modifiers
|
|
1726
|
+
"Event automatically added by the fix unpaired state events function",
|
|
1727
|
+
cfg.NA, # frame index
|
|
1728
|
+
]
|
|
1510
1729
|
)
|
|
1511
1730
|
|
|
1512
1731
|
return closing_events_to_add
|
|
@@ -1533,8 +1752,11 @@ def explore_project(self) -> None:
|
|
|
1533
1752
|
manage double-click on tablewidget of explore project results
|
|
1534
1753
|
"""
|
|
1535
1754
|
observation_operations.load_observation(self, obs_id, cfg.VIEW)
|
|
1536
|
-
|
|
1537
|
-
self.
|
|
1755
|
+
|
|
1756
|
+
self.tv_events.selectRow(event_idx - 1)
|
|
1757
|
+
index = self.tv_events.model().index(event_idx - 1, 0)
|
|
1758
|
+
self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
|
|
1759
|
+
# self.twEvents.scrollToItem(self.twEvents.item(event_idx - 1, 0))
|
|
1538
1760
|
|
|
1539
1761
|
elements_list = ("Subject", "Behavior", "Modifier", "Comment")
|
|
1540
1762
|
elements = []
|
|
@@ -1543,7 +1765,9 @@ def explore_project(self) -> None:
|
|
|
1543
1765
|
elements.append(("cb", "Case sensitive", False))
|
|
1544
1766
|
|
|
1545
1767
|
explore_dlg = dialog.Input_dialog(
|
|
1546
|
-
label_caption="Search in all observations",
|
|
1768
|
+
label_caption="Search in all observations",
|
|
1769
|
+
elements_list=elements,
|
|
1770
|
+
title="Explore project",
|
|
1547
1771
|
)
|
|
1548
1772
|
explore_dlg.pbOK.setText("Find")
|
|
1549
1773
|
if not explore_dlg.exec_():
|
|
@@ -1567,10 +1791,7 @@ def explore_project(self) -> None:
|
|
|
1567
1791
|
if any(
|
|
1568
1792
|
(
|
|
1569
1793
|
(explore_dlg.elements["Case sensitive"].isChecked() and text in event[idx]),
|
|
1570
|
-
(
|
|
1571
|
-
not explore_dlg.elements["Case sensitive"].isChecked()
|
|
1572
|
-
and text.upper() in event[idx].upper()
|
|
1573
|
-
),
|
|
1794
|
+
(not explore_dlg.elements["Case sensitive"].isChecked() and text.upper() in event[idx].upper()),
|
|
1574
1795
|
)
|
|
1575
1796
|
):
|
|
1576
1797
|
nb_results += 1
|
|
@@ -1604,3 +1825,215 @@ def explore_project(self) -> None:
|
|
|
1604
1825
|
|
|
1605
1826
|
else:
|
|
1606
1827
|
QMessageBox.information(self, cfg.programName, "No events found")
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def project2dataframe(pj: dict, observations_list: list = []) -> Tuple[str, pd.DataFrame]:
|
|
1831
|
+
"""
|
|
1832
|
+
returns a pandas dataframe containing observations data
|
|
1833
|
+
"""
|
|
1834
|
+
# print(pj.keys())
|
|
1835
|
+
|
|
1836
|
+
# print(pj["independent_variables"])
|
|
1837
|
+
|
|
1838
|
+
# indep_var = [pj["independent_variables"][idx]["label"] for idx in pj["independent_variables"]]
|
|
1839
|
+
|
|
1840
|
+
indep_variables = dict(
|
|
1841
|
+
[(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"], pj[cfg.INDEPENDENT_VARIABLES][idx]["type"]) for idx in pj[cfg.INDEPENDENT_VARIABLES]]
|
|
1842
|
+
)
|
|
1843
|
+
|
|
1844
|
+
# print()
|
|
1845
|
+
# print(f"{indep_variables=}")
|
|
1846
|
+
|
|
1847
|
+
# n_max_set_modifiers = max([len(pj["behaviors_conf"][behavior_id]["modifiers"]) for behavior_id in pj["behaviors_conf"]])
|
|
1848
|
+
|
|
1849
|
+
# behavioral_categories
|
|
1850
|
+
behavioral_category = dict(
|
|
1851
|
+
[(pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE], pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]) for x in pj[cfg.ETHOGRAM]]
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
# print(f"{pj["behaviors_conf"]=}")
|
|
1855
|
+
|
|
1856
|
+
# check all modifiers
|
|
1857
|
+
all_modifier_sets: list = []
|
|
1858
|
+
for behavior_id in pj[cfg.ETHOGRAM]:
|
|
1859
|
+
modifier_names: list = []
|
|
1860
|
+
set_count = 0
|
|
1861
|
+
if pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS] == "":
|
|
1862
|
+
continue
|
|
1863
|
+
for modifier in pj[cfg.ETHOGRAM][behavior_id][cfg.MODIFIERS].values():
|
|
1864
|
+
if modifier["name"]:
|
|
1865
|
+
modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], modifier["name"]))
|
|
1866
|
+
else:
|
|
1867
|
+
set_count += 1
|
|
1868
|
+
modifier_names.append((pj[cfg.ETHOGRAM][behavior_id][cfg.BEHAVIOR_CODE], f"set #{set_count}"))
|
|
1869
|
+
|
|
1870
|
+
# print(modifier_names)
|
|
1871
|
+
if modifier_names:
|
|
1872
|
+
all_modifier_sets.extend(modifier_names)
|
|
1873
|
+
|
|
1874
|
+
# print()
|
|
1875
|
+
# print(f"{all_modifier_sets=}")
|
|
1876
|
+
|
|
1877
|
+
# create df
|
|
1878
|
+
|
|
1879
|
+
data = {
|
|
1880
|
+
"Observation id": [],
|
|
1881
|
+
"Observation date": [],
|
|
1882
|
+
"Description": [],
|
|
1883
|
+
"Observation type": [],
|
|
1884
|
+
"Observation interval start": [],
|
|
1885
|
+
"Observation interval stop": [],
|
|
1886
|
+
# "Source": [],
|
|
1887
|
+
# "Time offset (s)": [],
|
|
1888
|
+
# "Coding duration": [],
|
|
1889
|
+
# "Media duration (s)": [],
|
|
1890
|
+
# "FPS (frame/s)": [],
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
for indep_var in indep_variables:
|
|
1894
|
+
data[f"independent variable '{indep_var}'"] = []
|
|
1895
|
+
|
|
1896
|
+
data = data | {
|
|
1897
|
+
"Subject": [],
|
|
1898
|
+
"Observation duration by subject by observation": [],
|
|
1899
|
+
"Behavior": [],
|
|
1900
|
+
"Behavioral category": [],
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
for modifier_set in all_modifier_sets:
|
|
1904
|
+
data[modifier_set] = []
|
|
1905
|
+
|
|
1906
|
+
data = data | {
|
|
1907
|
+
"Behavior type": [],
|
|
1908
|
+
"Start (s)": [],
|
|
1909
|
+
"Stop (s)": [],
|
|
1910
|
+
"Duration (s)": [],
|
|
1911
|
+
# "Media file name": [],
|
|
1912
|
+
# "Image index start": [],
|
|
1913
|
+
# "Image index stop": [],
|
|
1914
|
+
# "Image file path start": [],
|
|
1915
|
+
# "Image file path stop": [],
|
|
1916
|
+
"Comment start": [],
|
|
1917
|
+
"Comment stop": [],
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
#
|
|
1921
|
+
|
|
1922
|
+
type_ = {
|
|
1923
|
+
"Observation id": "string",
|
|
1924
|
+
"Observation date": "string",
|
|
1925
|
+
"Description": "string",
|
|
1926
|
+
"Observation type": "string",
|
|
1927
|
+
"Observation interval start": "float64",
|
|
1928
|
+
"Observation interval stop": "float64",
|
|
1929
|
+
# "Source": "string",
|
|
1930
|
+
# "Time offset (s)": "string",
|
|
1931
|
+
# "Coding duration": "float64",
|
|
1932
|
+
# "Media duration (s)": "string",
|
|
1933
|
+
# "FPS (frame/s)": "float64",
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
# TODO: set correct type in base of the var type
|
|
1937
|
+
for indep_var in indep_variables:
|
|
1938
|
+
type_[f"independent variable '{indep_var}'"] = "float64" if indep_variables[indep_var] == cfg.NUMERIC else "string"
|
|
1939
|
+
|
|
1940
|
+
type_ = type_ | {
|
|
1941
|
+
"Subject": "string",
|
|
1942
|
+
"Observation duration by subject by observation": "float64",
|
|
1943
|
+
"Behavior": "string",
|
|
1944
|
+
"Behavioral category": "string",
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
for modifer_set in all_modifier_sets:
|
|
1948
|
+
type_[modifer_set] = "string"
|
|
1949
|
+
|
|
1950
|
+
type_ = type_ | {
|
|
1951
|
+
"Behavior type": "string",
|
|
1952
|
+
"Start (s)": "float64",
|
|
1953
|
+
"Stop (s)": "float64",
|
|
1954
|
+
"Duration (s)": "float64",
|
|
1955
|
+
# "Media file name": "string",
|
|
1956
|
+
# "Image index start": "float64",
|
|
1957
|
+
# "Image index stop": "float64",
|
|
1958
|
+
# "Image file path start": "string",
|
|
1959
|
+
# "Image file path stop": "string",
|
|
1960
|
+
"Comment start": "string",
|
|
1961
|
+
"Comment stop": "string",
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
state_behaviors = util.state_behavior_codes(pj[cfg.ETHOGRAM])
|
|
1965
|
+
|
|
1966
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
1967
|
+
if observations_list and obs_id not in observations_list:
|
|
1968
|
+
continue
|
|
1969
|
+
# print(obs_id)
|
|
1970
|
+
stop_event_idx = set()
|
|
1971
|
+
for idx_event, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
1972
|
+
if idx_event in stop_event_idx:
|
|
1973
|
+
continue
|
|
1974
|
+
data["Observation id"].append(obs_id)
|
|
1975
|
+
data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
|
|
1976
|
+
data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
|
|
1977
|
+
data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
|
|
1978
|
+
|
|
1979
|
+
data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
|
|
1980
|
+
data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
|
|
1981
|
+
|
|
1982
|
+
# data["Source"].append("")
|
|
1983
|
+
# data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
|
|
1984
|
+
# data["Coding duration"].append("")
|
|
1985
|
+
# data["Media duration (s)"].append("")
|
|
1986
|
+
# data["FPS (frame/s)"].append("")
|
|
1987
|
+
|
|
1988
|
+
for indep_var in indep_variables:
|
|
1989
|
+
data[f"independent variable '{indep_var}'"].append(
|
|
1990
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES].get(indep_var, None)
|
|
1991
|
+
)
|
|
1992
|
+
|
|
1993
|
+
data["Subject"].append(event[cfg.EVENT_SUBJECT_FIELD_IDX] if event[cfg.EVENT_SUBJECT_FIELD_IDX] != "" else cfg.NO_FOCAL_SUBJECT)
|
|
1994
|
+
data["Observation duration by subject by observation"].append(-1)
|
|
1995
|
+
data["Behavior"].append(event[2])
|
|
1996
|
+
data["Behavioral category"].append(behavioral_category[event[2]])
|
|
1997
|
+
|
|
1998
|
+
count_set = 0
|
|
1999
|
+
for modifier_set in all_modifier_sets:
|
|
2000
|
+
if event[2] == modifier_set[0]:
|
|
2001
|
+
try:
|
|
2002
|
+
data[modifier_set].append(event[3].split("|")[count_set])
|
|
2003
|
+
except Exception:
|
|
2004
|
+
return f"Modifier error for {event[2]} in observation {obs_id}", pd.DataFrame()
|
|
2005
|
+
count_set += 1
|
|
2006
|
+
else:
|
|
2007
|
+
data[modifier_set].append(np.nan)
|
|
2008
|
+
|
|
2009
|
+
data["Behavior type"].append(cfg.STATE_EVENT if event[2] in state_behaviors else cfg.POINT_EVENT)
|
|
2010
|
+
data["Start (s)"].append(float(event[0]))
|
|
2011
|
+
if event[2] in state_behaviors:
|
|
2012
|
+
# search stop
|
|
2013
|
+
# print(f"==> {idx_event=} {event[1:4]=}")
|
|
2014
|
+
for idx_event2, event2 in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx_event + 1 :], start=idx_event + 1):
|
|
2015
|
+
# print(f"{idx_event2=} {event2[1:4]=}")
|
|
2016
|
+
if event2[1:4] == event[1:4]:
|
|
2017
|
+
# print("found")
|
|
2018
|
+
stop_event_idx.add(idx_event2)
|
|
2019
|
+
data["Stop (s)"].append(float(event2[0]))
|
|
2020
|
+
data["Duration (s)"].append(float(event2[0] - event[0]))
|
|
2021
|
+
data["Comment start"].append(event[4])
|
|
2022
|
+
data["Comment stop"].append(event2[4])
|
|
2023
|
+
break
|
|
2024
|
+
else:
|
|
2025
|
+
return f"Some events are not paired in {obs_id}", pd.DataFrame()
|
|
2026
|
+
|
|
2027
|
+
else: # point
|
|
2028
|
+
data["Stop (s)"].append(float(event[0]))
|
|
2029
|
+
data["Duration (s)"].append(np.nan)
|
|
2030
|
+
data["Comment start"].append(event[4])
|
|
2031
|
+
data["Comment stop"].append(event[4])
|
|
2032
|
+
|
|
2033
|
+
# Set the display option to show all rows and columns
|
|
2034
|
+
pd.set_option("display.max_rows", None)
|
|
2035
|
+
pd.set_option("display.max_columns", None)
|
|
2036
|
+
|
|
2037
|
+
pd.DataFrame(data).info()
|
|
2038
|
+
|
|
2039
|
+
return "", pd.DataFrame(data)
|