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/write_event.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This program is free software; you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program; if not, write to the Free Software
|
|
19
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
+
MA 02110-1301, USA.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import bisect
|
|
25
|
+
import logging
|
|
26
|
+
from decimal import Decimal as dec
|
|
27
|
+
import re
|
|
28
|
+
import pathlib as pl
|
|
29
|
+
|
|
30
|
+
from . import config as cfg
|
|
31
|
+
from . import dialog
|
|
32
|
+
from . import utilities as util
|
|
33
|
+
from . import select_modifiers
|
|
34
|
+
from . import event_operations
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def write_event(self, event: dict, mem_time: dec) -> int:
|
|
38
|
+
"""
|
|
39
|
+
add event from pressed key to observation
|
|
40
|
+
offset is added to event time
|
|
41
|
+
ask for modifiers if configured
|
|
42
|
+
load events in tableview
|
|
43
|
+
scroll to active event
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
event (dict): event parameters
|
|
47
|
+
memTime (Decimal): time
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
logging.debug(f"write event - event: {event} memtime: {mem_time}")
|
|
52
|
+
|
|
53
|
+
if event is None:
|
|
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
|
|
83
|
+
|
|
84
|
+
editing_event = "row" in event
|
|
85
|
+
|
|
86
|
+
# add time offset if not from editing
|
|
87
|
+
if not editing_event:
|
|
88
|
+
# add offset
|
|
89
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
90
|
+
mem_time += dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET]).quantize(dec(".001"))
|
|
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
|
+
|
|
119
|
+
# remove key code from modifiers
|
|
120
|
+
subject = event.get(cfg.SUBJECT, self.currentSubject)
|
|
121
|
+
comment = event.get(cfg.COMMENT, "")
|
|
122
|
+
|
|
123
|
+
if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
|
|
124
|
+
image_idx = event.get(cfg.IMAGE_INDEX, "")
|
|
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))
|
|
129
|
+
|
|
130
|
+
if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA):
|
|
131
|
+
frame_idx = event.get(cfg.FRAME_INDEX, cfg.NA)
|
|
132
|
+
|
|
133
|
+
# check if a same event is already in events list (time, subject, code)
|
|
134
|
+
|
|
135
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
136
|
+
# adding event
|
|
137
|
+
if (not editing_event) and self.checkSameEvent(
|
|
138
|
+
self.observationId,
|
|
139
|
+
mem_time,
|
|
140
|
+
subject,
|
|
141
|
+
event[cfg.BEHAVIOR_CODE],
|
|
142
|
+
):
|
|
143
|
+
_ = dialog.MessageDialog(cfg.programName, "The same event already exists (same time, behavior code and subject).", (cfg.OK,))
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
# modifying event and time was changed
|
|
147
|
+
if editing_event and mem_time != self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][0]:
|
|
148
|
+
if self.checkSameEvent(
|
|
149
|
+
self.observationId,
|
|
150
|
+
mem_time,
|
|
151
|
+
subject,
|
|
152
|
+
event[cfg.BEHAVIOR_CODE],
|
|
153
|
+
):
|
|
154
|
+
_ = dialog.MessageDialog(
|
|
155
|
+
cfg.programName,
|
|
156
|
+
"The same event already exists (same time, behavior code and subject).",
|
|
157
|
+
[cfg.OK],
|
|
158
|
+
)
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
162
|
+
# adding event
|
|
163
|
+
if (not editing_event) and self.checkSameEvent(
|
|
164
|
+
self.observationId,
|
|
165
|
+
image_idx,
|
|
166
|
+
subject,
|
|
167
|
+
event[cfg.BEHAVIOR_CODE],
|
|
168
|
+
):
|
|
169
|
+
_ = dialog.MessageDialog(
|
|
170
|
+
cfg.programName,
|
|
171
|
+
"The same event already exists (same image index, behavior code and subject).",
|
|
172
|
+
[cfg.OK],
|
|
173
|
+
)
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
# modifying event and time was changed
|
|
177
|
+
if (
|
|
178
|
+
editing_event
|
|
179
|
+
and image_idx
|
|
180
|
+
!= self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]]
|
|
181
|
+
):
|
|
182
|
+
if self.checkSameEvent(
|
|
183
|
+
self.observationId,
|
|
184
|
+
image_idx,
|
|
185
|
+
subject,
|
|
186
|
+
event[cfg.BEHAVIOR_CODE],
|
|
187
|
+
):
|
|
188
|
+
_ = dialog.MessageDialog(
|
|
189
|
+
cfg.programName,
|
|
190
|
+
"The same event already exists (same image index, behavior code and subject).",
|
|
191
|
+
(cfg.OK,),
|
|
192
|
+
)
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
# extract State events
|
|
196
|
+
state_behaviors_codes = util.state_behavior_codes(self.pj[cfg.ETHOGRAM])
|
|
197
|
+
|
|
198
|
+
# index of current subject
|
|
199
|
+
# subject_idx = self.subject_name_index[self.currentSubject] if self.currentSubject else ""
|
|
200
|
+
|
|
201
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
|
|
202
|
+
position = mem_time
|
|
203
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
204
|
+
position = dec(image_idx) # decimal to pass to util.get_current_states_modifiers_by_subject
|
|
205
|
+
|
|
206
|
+
current_states: dict = util.get_current_states_modifiers_by_subject(
|
|
207
|
+
state_behaviors_codes,
|
|
208
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
209
|
+
dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}),
|
|
210
|
+
position,
|
|
211
|
+
include_modifiers=False,
|
|
212
|
+
)
|
|
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
|
+
|
|
314
|
+
# logging.debug(f"self.currentSubject {self.currentSubject}")
|
|
315
|
+
# logging.debug(f"current_states {current_states}")
|
|
316
|
+
|
|
317
|
+
# fill the undo list
|
|
318
|
+
event_operations.fill_events_undo_list(self, "Undo last event edition" if editing_event else "Undo last event insertion")
|
|
319
|
+
|
|
320
|
+
logging.debug("save list of events for undo operation")
|
|
321
|
+
|
|
322
|
+
if not editing_event:
|
|
323
|
+
if self.currentSubject:
|
|
324
|
+
csj: list = [] # list of current state for the current subject
|
|
325
|
+
for idx in current_states:
|
|
326
|
+
if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
|
|
327
|
+
csj = current_states[idx]
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
else: # no focal subject
|
|
331
|
+
try:
|
|
332
|
+
csj = current_states[""]
|
|
333
|
+
except Exception:
|
|
334
|
+
csj = []
|
|
335
|
+
|
|
336
|
+
logging.debug(f"csj {csj}")
|
|
337
|
+
|
|
338
|
+
# print(f"{csj=}")
|
|
339
|
+
|
|
340
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
|
|
341
|
+
check_index = cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
|
|
342
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
343
|
+
check_index = cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]
|
|
344
|
+
|
|
345
|
+
cm: dict = {} # modifiers for current behaviors
|
|
346
|
+
for cs in csj:
|
|
347
|
+
for ev in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]:
|
|
348
|
+
if ev[check_index] > position:
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
if ev[cfg.EVENT_SUBJECT_FIELD_IDX] == self.currentSubject:
|
|
352
|
+
if ev[cfg.EVENT_BEHAVIOR_FIELD_IDX] == cs:
|
|
353
|
+
cm[cs] = ev[cfg.EVENT_MODIFIER_FIELD_IDX]
|
|
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
|
+
|
|
370
|
+
for cs in csj:
|
|
371
|
+
# close state if same state without modifier
|
|
372
|
+
if (
|
|
373
|
+
self.close_the_same_current_event
|
|
374
|
+
and (event[cfg.BEHAVIOR_CODE] == cs)
|
|
375
|
+
and modifier_str.replace("None", "").replace("|", "") == ""
|
|
376
|
+
):
|
|
377
|
+
modifier_str = cm[cs]
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
if (event[cfg.EXCLUDED] and cs in event[cfg.EXCLUDED].split(",")) or (
|
|
381
|
+
event[cfg.BEHAVIOR_CODE] == cs and cm[cs] != modifier_str
|
|
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
|
+
|
|
432
|
+
# add excluded state event to observations (= STOP them)
|
|
433
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE):
|
|
434
|
+
bisect.insort(
|
|
435
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
436
|
+
[mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, ""],
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
|
|
440
|
+
bisect.insort(
|
|
441
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
442
|
+
[mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, "", cfg.NA],
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
|
|
446
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(
|
|
447
|
+
[mem_time, self.currentSubject, cs, behavior_to_stop_modifier_str, "", image_idx, image_path]
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# order by image index ASC
|
|
451
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(
|
|
452
|
+
key=lambda x: x[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]]
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# add event to pj
|
|
456
|
+
if editing_event: # modifying event
|
|
457
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
458
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
|
|
459
|
+
mem_time,
|
|
460
|
+
subject,
|
|
461
|
+
event[cfg.BEHAVIOR_CODE],
|
|
462
|
+
modifier_str,
|
|
463
|
+
comment,
|
|
464
|
+
frame_idx,
|
|
465
|
+
]
|
|
466
|
+
# order events list using time, subject, behavior
|
|
467
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
468
|
+
|
|
469
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
|
|
470
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
|
|
471
|
+
mem_time,
|
|
472
|
+
subject,
|
|
473
|
+
event[cfg.BEHAVIOR_CODE],
|
|
474
|
+
modifier_str,
|
|
475
|
+
comment,
|
|
476
|
+
]
|
|
477
|
+
# order events list using time, subject, behavior
|
|
478
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
479
|
+
|
|
480
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
481
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
|
|
482
|
+
mem_time,
|
|
483
|
+
subject,
|
|
484
|
+
event[cfg.BEHAVIOR_CODE],
|
|
485
|
+
modifier_str,
|
|
486
|
+
comment,
|
|
487
|
+
image_idx,
|
|
488
|
+
image_path,
|
|
489
|
+
]
|
|
490
|
+
# order by image index ASC
|
|
491
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(
|
|
492
|
+
key=lambda x: x[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]]
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
else: # add event
|
|
496
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
497
|
+
bisect.insort(
|
|
498
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
499
|
+
[mem_time, subject, event[cfg.BEHAVIOR_CODE], modifier_str, comment, frame_idx],
|
|
500
|
+
)
|
|
501
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
|
|
502
|
+
bisect.insort(
|
|
503
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
504
|
+
[mem_time, subject, event[cfg.BEHAVIOR_CODE], modifier_str, comment],
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
508
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(
|
|
509
|
+
[mem_time, subject, event[cfg.BEHAVIOR_CODE], modifier_str, comment, image_idx, image_path]
|
|
510
|
+
)
|
|
511
|
+
# order by image index ASC
|
|
512
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(
|
|
513
|
+
key=lambda x: x[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]]
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# reload all events in tw
|
|
517
|
+
self.load_tw_events(self.observationId)
|
|
518
|
+
|
|
519
|
+
self.project_changed()
|
|
520
|
+
|
|
521
|
+
self.get_events_current_row()
|
|
522
|
+
|
|
523
|
+
# index of current subject selected by observer
|
|
524
|
+
subject_idx = self.subject_name_index[self.currentSubject] if self.currentSubject else ""
|
|
525
|
+
|
|
526
|
+
self.currentStates = util.get_current_states_modifiers_by_subject(
|
|
527
|
+
self.state_behaviors_codes,
|
|
528
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
529
|
+
dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}),
|
|
530
|
+
self.getLaps(),
|
|
531
|
+
include_modifiers=True,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
self.lbCurrentStates.setText(f"Observed behaviors: {', '.join(self.currentStates[subject_idx])}")
|
|
535
|
+
# show current states in subjects table
|
|
536
|
+
self.show_current_states_in_subjects_table()
|
|
537
|
+
|
|
538
|
+
return 0
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boris-behav-obs
|
|
3
|
+
Version: 9.7.6
|
|
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, Windows and macOS.
|
|
48
|
+
|
|
49
|
+
It provides also some analysis tools like time budget and some plotting functions.
|
|
50
|
+
|
|
51
|
+
<!-- The BO-RIS paper has more than [ citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
The BORIS paper has more than 2423 citations in peer-reviewed scientific publications.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
See the official [BORIS web site](https://www.boris.unito.it).
|
|
60
|
+
|
|
61
|
+
<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>
|
|
62
|
+
<a href="https://www.boris.unito.it/user_guide/" target="_blank"><img alt="User guide" src="https://img.shields.io/badge/Documentation-orange"></a>
|
|
63
|
+
[](https://www.python.org)
|
|
64
|
+

|
|
65
|
+

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

|
|
70
|
+

|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
|
|
75
|
+

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