boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +24 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +235 -131
- boris/advanced_event_filtering.py +23 -29
- 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 +16 -34
- boris/config.py +108 -49
- boris/config_file.py +58 -67
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2106 -1277
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +134 -0
- boris/dialog.py +461 -242
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +405 -281
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +180 -203
- boris/export_observation.py +60 -73
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +427 -218
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +304 -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 +16 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv2.py +127 -36
- boris/observation.py +493 -210
- boris/observation_operations.py +1010 -391
- boris/observation_ui.py +573 -363
- boris/observations_list.py +51 -58
- boris/otx_parser.py +74 -68
- boris/param_panel.py +45 -59
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +91 -56
- boris/plot_data_module.py +18 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +80 -56
- boris/plot_waveform_rt.py +23 -48
- 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 +304 -123
- boris/preferences_ui.py +684 -227
- boris/project.py +293 -270
- boris/project_functions.py +618 -537
- boris/project_import_export.py +204 -213
- boris/project_ui.py +673 -441
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +62 -90
- boris/select_observations.py +19 -197
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +51 -33
- boris/subjects_pad.py +6 -8
- boris/synthetic_time_budget.py +25 -17
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +71 -86
- boris/transitions.py +41 -41
- boris/utilities.py +562 -222
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +78 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +240 -136
- boris_behav_obs-9.7.2.dist-info/METADATA +140 -0
- boris_behav_obs-9.7.2.dist-info/RECORD +109 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.2.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 -37
- boris/core.ui +0 -1571
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -982
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1074
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.16.6.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
- boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.2.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.dist-info}/top_level.txt +0 -0
boris/write_event.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
|
|
|
7
7
|
This program is free software; you can redistribute it and/or modify
|
|
@@ -25,6 +25,7 @@ import bisect
|
|
|
25
25
|
import logging
|
|
26
26
|
from decimal import Decimal as dec
|
|
27
27
|
import re
|
|
28
|
+
import pathlib as pl
|
|
28
29
|
|
|
29
30
|
from . import config as cfg
|
|
30
31
|
from . import dialog
|
|
@@ -32,10 +33,6 @@ from . import utilities as util
|
|
|
32
33
|
from . import select_modifiers
|
|
33
34
|
from . import event_operations
|
|
34
35
|
|
|
35
|
-
from PyQt5.QtWidgets import (
|
|
36
|
-
QAbstractItemView,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
36
|
|
|
40
37
|
def write_event(self, event: dict, mem_time: dec) -> int:
|
|
41
38
|
"""
|
|
@@ -54,37 +51,35 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
54
51
|
logging.debug(f"write event - event: {event} memtime: {mem_time}")
|
|
55
52
|
|
|
56
53
|
if event is None:
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
self.pj[cfg.OBSERVATIONS][self.observationId]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
return
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
|
|
57
|
+
# live observation finished (end of time interval reached)
|
|
58
|
+
if not self.liveObservationStarted and mem_time.is_nan():
|
|
59
|
+
_ = dialog.MessageDialog(
|
|
60
|
+
cfg.programName,
|
|
61
|
+
(
|
|
62
|
+
"The live observation is finished.<br>"
|
|
63
|
+
"The observation interval is "
|
|
64
|
+
f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
|
|
65
|
+
f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
|
|
66
|
+
),
|
|
67
|
+
(cfg.OK,),
|
|
68
|
+
)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
if mem_time < self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]:
|
|
72
|
+
_ = dialog.MessageDialog(
|
|
73
|
+
cfg.programName,
|
|
74
|
+
(
|
|
75
|
+
"The live observation has not began.<br>"
|
|
76
|
+
"The observation interval is "
|
|
77
|
+
f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
|
|
78
|
+
f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
|
|
79
|
+
),
|
|
80
|
+
(cfg.OK,),
|
|
81
|
+
)
|
|
82
|
+
return 1
|
|
88
83
|
|
|
89
84
|
editing_event = "row" in event
|
|
90
85
|
|
|
@@ -94,6 +89,33 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
94
89
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
95
90
|
mem_time += dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET]).quantize(dec(".001"))
|
|
96
91
|
|
|
92
|
+
# add media creation time
|
|
93
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
|
|
94
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
|
|
95
|
+
media_file_name = self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"]
|
|
96
|
+
|
|
97
|
+
logging.debug(f"{media_file_name=}")
|
|
98
|
+
|
|
99
|
+
media_file_name_posix = pl.Path(media_file_name).as_posix()
|
|
100
|
+
|
|
101
|
+
logging.debug(f"{media_file_name_posix=}")
|
|
102
|
+
|
|
103
|
+
# add media creation date/time
|
|
104
|
+
|
|
105
|
+
mem_time += dec(
|
|
106
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_file_name_posix]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# check if time > 2**31 - 1 (2147483647)
|
|
110
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.VIEWER_MEDIA, cfg.LIVE, cfg.VIEWER_LIVE):
|
|
111
|
+
if (mem_time < -2147483647) or (mem_time > 2147483647):
|
|
112
|
+
_ = dialog.MessageDialog(
|
|
113
|
+
cfg.programName,
|
|
114
|
+
(f"The timestamp must be between -2147483647 and 2147483647.<br>The current timestamp is {mem_time}"),
|
|
115
|
+
(cfg.OK,),
|
|
116
|
+
)
|
|
117
|
+
return 1
|
|
118
|
+
|
|
97
119
|
# remove key code from modifiers
|
|
98
120
|
subject = event.get(cfg.SUBJECT, self.currentSubject)
|
|
99
121
|
comment = event.get(cfg.COMMENT, "")
|
|
@@ -101,6 +123,9 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
101
123
|
if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
|
|
102
124
|
image_idx = event.get(cfg.IMAGE_INDEX, "")
|
|
103
125
|
image_path = event.get(cfg.IMAGE_PATH, "")
|
|
126
|
+
# check if pictures dir is relative
|
|
127
|
+
if str(pl.Path(image_path).parent) not in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
|
|
128
|
+
image_path = str(pl.Path(image_path).relative_to(pl.Path(self.projectFileName).parent))
|
|
104
129
|
|
|
105
130
|
if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA):
|
|
106
131
|
frame_idx = event.get(cfg.FRAME_INDEX, cfg.NA)
|
|
@@ -115,9 +140,7 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
115
140
|
subject,
|
|
116
141
|
event[cfg.BEHAVIOR_CODE],
|
|
117
142
|
):
|
|
118
|
-
_ = dialog.MessageDialog(
|
|
119
|
-
cfg.programName, "The same event already exists (same time, behavior code and subject).", (cfg.OK,)
|
|
120
|
-
)
|
|
143
|
+
_ = dialog.MessageDialog(cfg.programName, "The same event already exists (same time, behavior code and subject).", (cfg.OK,))
|
|
121
144
|
return 1
|
|
122
145
|
|
|
123
146
|
# modifying event and time was changed
|
|
@@ -154,9 +177,7 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
154
177
|
if (
|
|
155
178
|
editing_event
|
|
156
179
|
and image_idx
|
|
157
|
-
!= self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][
|
|
158
|
-
cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]
|
|
159
|
-
]
|
|
180
|
+
!= self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]]
|
|
160
181
|
):
|
|
161
182
|
if self.checkSameEvent(
|
|
162
183
|
self.observationId,
|
|
@@ -171,77 +192,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
171
192
|
)
|
|
172
193
|
return 1
|
|
173
194
|
|
|
174
|
-
if "from map" not in event: # modifiers only for behaviors without coding map
|
|
175
|
-
# check if event has modifiers
|
|
176
|
-
modifier_str = ""
|
|
177
|
-
|
|
178
|
-
if event[cfg.MODIFIERS]:
|
|
179
|
-
selected_modifiers, modifiers_external_data = {}, {}
|
|
180
|
-
# check if modifiers are from external data
|
|
181
|
-
for idx in event[cfg.MODIFIERS]:
|
|
182
|
-
if event[cfg.MODIFIERS][idx]["type"] == cfg.EXTERNAL_DATA_MODIFIER:
|
|
183
|
-
if "row" not in event: # no edit
|
|
184
|
-
for idx2 in self.plot_data:
|
|
185
|
-
if self.plot_data[idx2].y_label.upper() == event[cfg.MODIFIERS][idx]["name"].upper():
|
|
186
|
-
modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
|
|
187
|
-
modifiers_external_data[idx]["selected"] = self.plot_data[idx2].lb_value.text()
|
|
188
|
-
else: # edit
|
|
189
|
-
original_modifiers_list = event.get("original_modifiers", "").split("|")
|
|
190
|
-
modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
|
|
191
|
-
modifiers_external_data[idx]["selected"] = original_modifiers_list[int(idx)]
|
|
192
|
-
|
|
193
|
-
# check if modifiers are in single, multiple or numeric
|
|
194
|
-
if [x for x in event[cfg.MODIFIERS] if event[cfg.MODIFIERS][x]["type"] != cfg.EXTERNAL_DATA_MODIFIER]:
|
|
195
|
-
# pause media
|
|
196
|
-
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in [cfg.MEDIA]:
|
|
197
|
-
if self.playerType == cfg.MEDIA:
|
|
198
|
-
if self.dw_player[0].player.pause:
|
|
199
|
-
memState = "paused"
|
|
200
|
-
elif self.dw_player[0].player.time_pos is not None:
|
|
201
|
-
memState = "playing"
|
|
202
|
-
else:
|
|
203
|
-
memState = "stopped"
|
|
204
|
-
if memState == "playing":
|
|
205
|
-
self.pause_video()
|
|
206
|
-
|
|
207
|
-
# check if editing (original_modifiers key)
|
|
208
|
-
currentModifiers = event.get("original_modifiers", "")
|
|
209
|
-
|
|
210
|
-
modifiers_selector = select_modifiers.ModifiersList(
|
|
211
|
-
event["code"], eval(str(event[cfg.MODIFIERS])), currentModifiers
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
r = modifiers_selector.exec_()
|
|
215
|
-
if r:
|
|
216
|
-
selected_modifiers = modifiers_selector.get_modifiers()
|
|
217
|
-
|
|
218
|
-
# restart media
|
|
219
|
-
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
220
|
-
if self.playerType == cfg.MEDIA:
|
|
221
|
-
if memState == "playing":
|
|
222
|
-
self.play_video()
|
|
223
|
-
if not r: # cancel button pressed
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
all_modifiers = {**selected_modifiers, **modifiers_external_data}
|
|
227
|
-
|
|
228
|
-
modifier_str = ""
|
|
229
|
-
for idx in util.sorted_keys(all_modifiers):
|
|
230
|
-
if modifier_str:
|
|
231
|
-
modifier_str += "|"
|
|
232
|
-
if all_modifiers[idx]["type"] in [cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION]:
|
|
233
|
-
modifier_str += ",".join(all_modifiers[idx].get("selected", ""))
|
|
234
|
-
if all_modifiers[idx]["type"] in [cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER]:
|
|
235
|
-
modifier_str += all_modifiers[idx].get("selected", "NA")
|
|
236
|
-
|
|
237
|
-
else:
|
|
238
|
-
modifier_str = event["from map"]
|
|
239
|
-
|
|
240
|
-
modifier_str = re.sub(" \(.*\)", "", modifier_str)
|
|
241
|
-
|
|
242
|
-
# update current state
|
|
243
|
-
# TODO: verify event["subject"] / self.currentSubject
|
|
244
|
-
|
|
245
195
|
# extract State events
|
|
246
196
|
state_behaviors_codes = util.state_behavior_codes(self.pj[cfg.ETHOGRAM])
|
|
247
197
|
|
|
@@ -253,7 +203,7 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
253
203
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
254
204
|
position = dec(image_idx) # decimal to pass to util.get_current_states_modifiers_by_subject
|
|
255
205
|
|
|
256
|
-
current_states = util.get_current_states_modifiers_by_subject(
|
|
206
|
+
current_states: dict = util.get_current_states_modifiers_by_subject(
|
|
257
207
|
state_behaviors_codes,
|
|
258
208
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
259
209
|
dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}),
|
|
@@ -261,19 +211,117 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
261
211
|
include_modifiers=False,
|
|
262
212
|
)
|
|
263
213
|
|
|
214
|
+
# check if ask modifiers at stop is enabled
|
|
215
|
+
flag_ask_at_stop = False
|
|
216
|
+
if event["type"] == cfg.STATE_EVENT:
|
|
217
|
+
for idx in event[cfg.MODIFIERS]:
|
|
218
|
+
if event[cfg.MODIFIERS][idx].get("ask at stop", False):
|
|
219
|
+
flag_ask_at_stop = True
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
flag_ask_modifier = False
|
|
223
|
+
if flag_ask_at_stop:
|
|
224
|
+
# TODO: check if new event is a STOP one
|
|
225
|
+
|
|
226
|
+
idx_subject: str = ""
|
|
227
|
+
for idx in current_states:
|
|
228
|
+
if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
|
|
229
|
+
idx_subject = idx
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
if event[cfg.BEHAVIOR_CODE] in current_states[idx_subject]:
|
|
233
|
+
flag_ask_modifier = True
|
|
234
|
+
else:
|
|
235
|
+
flag_ask_modifier = True
|
|
236
|
+
|
|
237
|
+
if flag_ask_modifier:
|
|
238
|
+
if "from map" in event: # modifiers only for behaviors without coding map
|
|
239
|
+
modifier_str = event["from map"]
|
|
240
|
+
else:
|
|
241
|
+
# check if event has modifiers
|
|
242
|
+
modifier_str: str = ""
|
|
243
|
+
|
|
244
|
+
if event[cfg.MODIFIERS]:
|
|
245
|
+
selected_modifiers: dict = {}
|
|
246
|
+
modifiers_external_data: dict = {}
|
|
247
|
+
# check if modifiers are from external data
|
|
248
|
+
for idx in event[cfg.MODIFIERS]:
|
|
249
|
+
if event[cfg.MODIFIERS][idx]["type"] == cfg.EXTERNAL_DATA_MODIFIER:
|
|
250
|
+
if editing_event:
|
|
251
|
+
original_modifiers_list = event.get("original_modifiers", "").split("|")
|
|
252
|
+
modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
|
|
253
|
+
modifiers_external_data[idx]["selected"] = original_modifiers_list[int(idx)]
|
|
254
|
+
|
|
255
|
+
else: # no edit
|
|
256
|
+
for idx2 in self.plot_data:
|
|
257
|
+
if self.plot_data[idx2].y_label.upper() == event[cfg.MODIFIERS][idx]["name"].upper():
|
|
258
|
+
modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
|
|
259
|
+
modifiers_external_data[idx]["selected"] = self.plot_data[idx2].lb_value.text()
|
|
260
|
+
|
|
261
|
+
# check if modifiers are in single, multiple or numeric
|
|
262
|
+
if [x for x in event[cfg.MODIFIERS] if event[cfg.MODIFIERS][x]["type"] != cfg.EXTERNAL_DATA_MODIFIER]:
|
|
263
|
+
# pause media
|
|
264
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
|
|
265
|
+
if self.playerType == cfg.MEDIA:
|
|
266
|
+
if self.dw_player[0].player.pause:
|
|
267
|
+
memState = cfg.PAUSED
|
|
268
|
+
elif self.dw_player[0].player.time_pos is not None:
|
|
269
|
+
memState = cfg.PLAYING
|
|
270
|
+
else:
|
|
271
|
+
memState = cfg.STOPPED
|
|
272
|
+
if memState == cfg.PLAYING:
|
|
273
|
+
self.pause_video()
|
|
274
|
+
|
|
275
|
+
# check if editing (original_modifiers key)
|
|
276
|
+
currentModifiers = event.get("original_modifiers", "")
|
|
277
|
+
|
|
278
|
+
modifiers_selector = select_modifiers.ModifiersList(
|
|
279
|
+
event[cfg.BEHAVIOR_CODE], eval(str(event[cfg.MODIFIERS])), currentModifiers
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
r = modifiers_selector.exec_()
|
|
283
|
+
if r:
|
|
284
|
+
selected_modifiers = modifiers_selector.get_modifiers()
|
|
285
|
+
|
|
286
|
+
# restart media
|
|
287
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
288
|
+
if self.playerType == cfg.MEDIA:
|
|
289
|
+
if memState == cfg.PLAYING:
|
|
290
|
+
self.play_video()
|
|
291
|
+
if not r: # cancel button pressed
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
all_modifiers = {**selected_modifiers, **modifiers_external_data}
|
|
295
|
+
|
|
296
|
+
modifier_str: str = ""
|
|
297
|
+
for idx in util.sorted_keys(all_modifiers):
|
|
298
|
+
if modifier_str:
|
|
299
|
+
modifier_str += "|"
|
|
300
|
+
if all_modifiers[idx]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
|
|
301
|
+
modifier_str += ",".join(all_modifiers[idx].get("selected", ""))
|
|
302
|
+
if all_modifiers[idx]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
|
|
303
|
+
modifier_str += all_modifiers[idx].get("selected", "NA")
|
|
304
|
+
|
|
305
|
+
modifier_str = re.sub(r" \(.*\)", "", modifier_str)
|
|
306
|
+
else: # do not ask modifier
|
|
307
|
+
modifier_str = ""
|
|
308
|
+
|
|
309
|
+
# update current state
|
|
310
|
+
# TODO: verify event["subject"] / self.currentSubject
|
|
311
|
+
|
|
312
|
+
# print(f"{current_states=}")
|
|
313
|
+
|
|
264
314
|
# logging.debug(f"self.currentSubject {self.currentSubject}")
|
|
265
315
|
# logging.debug(f"current_states {current_states}")
|
|
266
316
|
|
|
267
317
|
# fill the undo list
|
|
268
|
-
event_operations.fill_events_undo_list(
|
|
269
|
-
self, "Undo last event edition" if editing_event else "Undo last event insertion"
|
|
270
|
-
)
|
|
318
|
+
event_operations.fill_events_undo_list(self, "Undo last event edition" if editing_event else "Undo last event insertion")
|
|
271
319
|
|
|
272
320
|
logging.debug("save list of events for undo operation")
|
|
273
321
|
|
|
274
322
|
if not editing_event:
|
|
275
323
|
if self.currentSubject:
|
|
276
|
-
csj: list = []
|
|
324
|
+
csj: list = [] # list of current state for the current subject
|
|
277
325
|
for idx in current_states:
|
|
278
326
|
if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
|
|
279
327
|
csj = current_states[idx]
|
|
@@ -287,6 +335,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
287
335
|
|
|
288
336
|
logging.debug(f"csj {csj}")
|
|
289
337
|
|
|
338
|
+
# print(f"{csj=}")
|
|
339
|
+
|
|
290
340
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
|
|
291
341
|
check_index = cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
|
|
292
342
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
@@ -302,6 +352,21 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
302
352
|
if ev[cfg.EVENT_BEHAVIOR_FIELD_IDX] == cs:
|
|
303
353
|
cm[cs] = ev[cfg.EVENT_MODIFIER_FIELD_IDX]
|
|
304
354
|
|
|
355
|
+
if flag_ask_at_stop:
|
|
356
|
+
# set modifier to START behavior
|
|
357
|
+
if event[cfg.BEHAVIOR_CODE] in csj:
|
|
358
|
+
mem_idx = -1
|
|
359
|
+
for idx, e in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
360
|
+
# print(f"{e=}")
|
|
361
|
+
if e[0] >= mem_time:
|
|
362
|
+
break
|
|
363
|
+
# same behavior, same subject and modifier(s) not set
|
|
364
|
+
if e[2] == event[cfg.BEHAVIOR_CODE] and e[1] == self.currentSubject and e[3] == "":
|
|
365
|
+
mem_idx = idx
|
|
366
|
+
if mem_idx != -1:
|
|
367
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][mem_idx][3] = modifier_str
|
|
368
|
+
csj.remove(event[cfg.BEHAVIOR_CODE])
|
|
369
|
+
|
|
305
370
|
for cs in csj:
|
|
306
371
|
# close state if same state without modifier
|
|
307
372
|
if (
|
|
@@ -312,25 +377,74 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
312
377
|
modifier_str = cm[cs]
|
|
313
378
|
continue
|
|
314
379
|
|
|
315
|
-
if (event[
|
|
380
|
+
if (event[cfg.EXCLUDED] and cs in event[cfg.EXCLUDED].split(",")) or (
|
|
316
381
|
event[cfg.BEHAVIOR_CODE] == cs and cm[cs] != modifier_str
|
|
317
382
|
):
|
|
383
|
+
# check if behavior to stop is a 'ask modifier at stop'
|
|
384
|
+
behavior_to_stop = [
|
|
385
|
+
self.pj[cfg.ETHOGRAM][x]
|
|
386
|
+
for x in self.pj[cfg.ETHOGRAM]
|
|
387
|
+
if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == cs
|
|
388
|
+
and self.pj[cfg.ETHOGRAM][x]["type"] in (cfg.STATE_EVENT, cfg.STATE_EVENT_WITH_CODING_MAP)
|
|
389
|
+
]
|
|
390
|
+
if behavior_to_stop:
|
|
391
|
+
behavior_to_stop = behavior_to_stop[0]
|
|
392
|
+
|
|
393
|
+
flag_behavior_ask_at_stop = False
|
|
394
|
+
for idx in behavior_to_stop[cfg.MODIFIERS]:
|
|
395
|
+
if behavior_to_stop[cfg.MODIFIERS][idx].get("ask at stop", False):
|
|
396
|
+
flag_behavior_ask_at_stop = True
|
|
397
|
+
break
|
|
398
|
+
if flag_behavior_ask_at_stop:
|
|
399
|
+
modifiers_selector = select_modifiers.ModifiersList(
|
|
400
|
+
behavior_to_stop[cfg.BEHAVIOR_CODE],
|
|
401
|
+
eval(str(behavior_to_stop[cfg.MODIFIERS])),
|
|
402
|
+
currentModifier="",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
r = modifiers_selector.exec_()
|
|
406
|
+
if r:
|
|
407
|
+
selected_modifiers = modifiers_selector.get_modifiers()
|
|
408
|
+
|
|
409
|
+
behavior_to_stop_modifier_str: str = ""
|
|
410
|
+
for idx in util.sorted_keys(selected_modifiers):
|
|
411
|
+
if behavior_to_stop_modifier_str:
|
|
412
|
+
behavior_to_stop_modifier_str += "|"
|
|
413
|
+
if selected_modifiers[idx]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
|
|
414
|
+
behavior_to_stop_modifier_str += ",".join(selected_modifiers[idx].get("selected", ""))
|
|
415
|
+
if selected_modifiers[idx]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
|
|
416
|
+
behavior_to_stop_modifier_str += selected_modifiers[idx].get("selected", "NA")
|
|
417
|
+
|
|
418
|
+
# set the start modifier
|
|
419
|
+
mem_idx = -1
|
|
420
|
+
for idx, e in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
421
|
+
if e[0] >= mem_time - dec("0.001"):
|
|
422
|
+
break
|
|
423
|
+
# same behavior, same subject and modifier(s) not set
|
|
424
|
+
if e[2] == behavior_to_stop[cfg.BEHAVIOR_CODE] and e[1] == self.currentSubject and e[3] == "":
|
|
425
|
+
mem_idx = idx
|
|
426
|
+
if mem_idx != -1:
|
|
427
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][mem_idx][3] = behavior_to_stop_modifier_str
|
|
428
|
+
|
|
429
|
+
else:
|
|
430
|
+
behavior_to_stop_modifier_str = cm[cs]
|
|
431
|
+
|
|
318
432
|
# add excluded state event to observations (= STOP them)
|
|
319
433
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE):
|
|
320
434
|
bisect.insort(
|
|
321
435
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
322
|
-
[mem_time - dec("0.001"), self.currentSubject, cs,
|
|
436
|
+
[mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, ""],
|
|
323
437
|
)
|
|
324
438
|
|
|
325
439
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
|
|
326
440
|
bisect.insort(
|
|
327
441
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
328
|
-
[mem_time - dec("0.001"), self.currentSubject, cs,
|
|
442
|
+
[mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, "", cfg.NA],
|
|
329
443
|
)
|
|
330
444
|
|
|
331
445
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
|
|
332
446
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(
|
|
333
|
-
[mem_time, self.currentSubject, cs,
|
|
447
|
+
[mem_time, self.currentSubject, cs, behavior_to_stop_modifier_str, "", image_idx, image_path]
|
|
334
448
|
)
|
|
335
449
|
|
|
336
450
|
# order by image index ASC
|
|
@@ -349,8 +463,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
349
463
|
comment,
|
|
350
464
|
frame_idx,
|
|
351
465
|
]
|
|
352
|
-
# order
|
|
353
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
|
|
466
|
+
# order events list using time, subject, behavior
|
|
467
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
354
468
|
|
|
355
469
|
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
|
|
356
470
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
|
|
@@ -360,8 +474,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
360
474
|
modifier_str,
|
|
361
475
|
comment,
|
|
362
476
|
]
|
|
363
|
-
# order
|
|
364
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
|
|
477
|
+
# order events list using time, subject, behavior
|
|
478
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
365
479
|
|
|
366
480
|
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
367
481
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
|
|
@@ -402,16 +516,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
|
|
|
402
516
|
# reload all events in tw
|
|
403
517
|
self.load_tw_events(self.observationId)
|
|
404
518
|
|
|
405
|
-
if self.playerType in (cfg.MEDIA, cfg.LIVE):
|
|
406
|
-
position_in_events = [
|
|
407
|
-
i for i, t in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]) if t[0] == mem_time
|
|
408
|
-
][0]
|
|
409
|
-
|
|
410
|
-
if position_in_events == len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]) - 1:
|
|
411
|
-
self.twEvents.scrollToBottom()
|
|
412
|
-
else:
|
|
413
|
-
self.twEvents.scrollToItem(self.twEvents.item(position_in_events, 0), QAbstractItemView.EnsureVisible)
|
|
414
|
-
|
|
415
519
|
self.project_changed()
|
|
416
520
|
|
|
417
521
|
self.get_events_current_row()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boris-behav-obs
|
|
3
|
+
Version: 9.7.2
|
|
4
|
+
Summary: BORIS - Behavioral Observation Research Interactive Software
|
|
5
|
+
Author-email: Olivier Friard <olivier.friard@unito.it>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, http://www.boris.unito.it
|
|
8
|
+
Project-URL: Documentation, https://boris.readthedocs.io/en/latest/
|
|
9
|
+
Project-URL: Change_log, https://github.com/olivierfriard/BORIS/wiki/BORIS-change-log-v.8
|
|
10
|
+
Project-URL: Source_code, https://github.com/olivierfriard/BORIS
|
|
11
|
+
Project-URL: Issues, https://github.com/olivierfriard/BORIS/issues
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.TXT
|
|
21
|
+
Requires-Dist: exifread==3.5.1
|
|
22
|
+
Requires-Dist: numpy==2.3.2
|
|
23
|
+
Requires-Dist: matplotlib==3.10.5
|
|
24
|
+
Requires-Dist: pandas==2.3.2
|
|
25
|
+
Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]==3.8.0
|
|
26
|
+
Requires-Dist: pyreadr==0.5.3
|
|
27
|
+
Requires-Dist: pyside6==6.10
|
|
28
|
+
Requires-Dist: hachoir==3.3.0
|
|
29
|
+
Requires-Dist: scipy==1.16.1
|
|
30
|
+
Requires-Dist: scikit-learn==1.7.1
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
35
|
+
Provides-Extra: r
|
|
36
|
+
Requires-Dist: rpy2>=3.6.1; extra == "r"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
BORIS (Behavioral Observation Research Interactive Software)
|
|
40
|
+
===============================================================
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
BORIS is an easy-to-use event logging software for video/audio coding or live observations.
|
|
46
|
+
|
|
47
|
+
BORIS is a free and open-source software available for GNU/Linux and Windows.
|
|
48
|
+
You can not longer run BORIS natively on MacOS (since v.8). Some alternatives to run the last version of BORIS are available, see [BORIS on MacOS](https://www.boris.unito.it/download_mac).
|
|
49
|
+
|
|
50
|
+
It provides also some analysis tools like time budget and some plotting functions.
|
|
51
|
+
|
|
52
|
+
<!-- The BO-RIS paper has more than [ citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
The BORIS paper has more than 2407 citations in peer-reviewed scientific publications.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
See the official [BORIS web site](https://www.boris.unito.it).
|
|
61
|
+
|
|
62
|
+
<a href="https://www.boris.unito.it" target="_blank"><img alt="Website" src="https://img.shields.io/website?url=https%3A%2F%2Fwww.boris.unito.it"></a>
|
|
63
|
+
<a href="https://www.boris.unito.it/user_guide/" target="_blank"><img alt="User guide" src="https://img.shields.io/badge/Documentation-orange"></a>
|
|
64
|
+
[](https://www.python.org)
|
|
65
|
+

|
|
66
|
+

|
|
67
|
+
[](https://pypi.org/project/boris-behav-obs/)
|
|
68
|
+
|
|
69
|
+
[](https://pepy.tech/project/boris-behav-obs)
|
|
70
|
+

|
|
71
|
+

|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
[](https://github.com/olivierfriard/BORIS/stargazers)
|
|
78
|
+
|
|
79
|
+
# Documentation
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
The [user guide](https://www.boris.unito.it/user_guide/) provides a good starting point for learning how to use BORIS.
|
|
84
|
+
|
|
85
|
+
Some [video tutorials](https://www.boris.unito.it/video_tutorials/) are available.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Bug reports and feature requests
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
To search for bugs, report them or request a feature, please use the [GitHub issues tracker](https://github.com/olivierfriard/BORIS/issues)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Citing BORIS
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
Please acknowledge and cite the use of this software and its authors when
|
|
104
|
+
results are used in publications or published elsewhere. You can use the
|
|
105
|
+
following BibTex entry
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
@article {MEE3:MEE312584,
|
|
109
|
+
author = {Friard, Olivier and Gamba, Marco},
|
|
110
|
+
title = {BORIS: a free, versatile open-source event-logging software for video/audio coding and live observations},
|
|
111
|
+
journal = {Methods in Ecology and Evolution},
|
|
112
|
+
issn = {2041-210X},
|
|
113
|
+
url = {http://dx.doi.org/10.1111/2041-210X.12584},
|
|
114
|
+
doi = {10.1111/2041-210X.12584},
|
|
115
|
+
pages = {1324--1330},
|
|
116
|
+
year = {2016},
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
You can also send us a nice postcard! See the [user testimonials](https://www.boris.unito.it/postcards).
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Licence
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
This program is distributed in the hope that it will be useful,
|
|
133
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
134
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
135
|
+
GNU General Public License for more details.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Distributed with a [GPL v.3 license](LICENSE.TXT).
|
|
139
|
+
|
|
140
|
+
Copyright (C) 2012-2025 Olivier Friard
|