boris-behav-obs 9.7.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from decimal import Decimal as dec
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from PySide6.QtCore import Qt
|
|
28
|
+
from PySide6.QtGui import QFont
|
|
29
|
+
from PySide6.QtWidgets import QCheckBox, QListWidgetItem, QMessageBox
|
|
30
|
+
|
|
31
|
+
from . import config as cfg
|
|
32
|
+
from . import gui_utilities, param_panel, project_functions
|
|
33
|
+
from . import utilities as util
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def choose_obs_subj_behav_category(
|
|
37
|
+
self,
|
|
38
|
+
selected_observations: list,
|
|
39
|
+
start_coding: Optional[dec] = dec("NaN"), # Union[..., None]
|
|
40
|
+
end_coding: Optional[dec] = dec("NaN"),
|
|
41
|
+
start_interval: Optional[dec] = dec("NaN"),
|
|
42
|
+
end_interval: Optional[dec] = dec("NaN"),
|
|
43
|
+
maxTime: Optional[dec] = None,
|
|
44
|
+
show_include_modifiers: bool = True,
|
|
45
|
+
show_exclude_non_coded_behaviors: bool = True,
|
|
46
|
+
by_category: bool = False,
|
|
47
|
+
n_observations: int = 1,
|
|
48
|
+
show_time_bin_size: bool = False,
|
|
49
|
+
window_title: str = "Select subjects and behaviors",
|
|
50
|
+
show_exclude_non_coded_modifiers: bool = False,
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
show window for:
|
|
54
|
+
- selection of subjects
|
|
55
|
+
- selection of behaviors (based on selected subjects)
|
|
56
|
+
- selection of time interval
|
|
57
|
+
- inclusion/exclusion of modifiers
|
|
58
|
+
- inclusion/exclusion of behaviors without events (show_exclude_non_coded_behaviors == True)
|
|
59
|
+
- selection of time bin size (show_time_bin_size == True)
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
selected_observations (list): List of selected observations
|
|
63
|
+
...
|
|
64
|
+
show_exclude_non_coded_modifiers (bool): display the Exclude non coded modifiers checkbox
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
dict: {"selected subjects": selectedSubjects,
|
|
68
|
+
"selected behaviors": selectedBehaviors,
|
|
69
|
+
"include modifiers": True/False,
|
|
70
|
+
"exclude behaviors": True/False,
|
|
71
|
+
"time": TIME_FULL_OBS / TIME_EVENTS / TIME_ARBITRARY_INTERVAL / TIME_OBS_INTERVAL
|
|
72
|
+
"start time": startTime,
|
|
73
|
+
"end time": endTime
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
paramPanelWindow = param_panel.Param_panel()
|
|
79
|
+
paramPanelWindow.resize(400, 400)
|
|
80
|
+
paramPanelWindow.setWindowTitle(window_title)
|
|
81
|
+
paramPanelWindow.selectedObservations = selected_observations
|
|
82
|
+
paramPanelWindow.pj = self.pj
|
|
83
|
+
paramPanelWindow.extract_observed_behaviors = self.extract_observed_behaviors
|
|
84
|
+
|
|
85
|
+
paramPanelWindow.cbIncludeModifiers.setVisible(show_include_modifiers)
|
|
86
|
+
paramPanelWindow.cb_exclude_non_coded_modifiers.setVisible(show_exclude_non_coded_modifiers)
|
|
87
|
+
paramPanelWindow.cbExcludeBehaviors.setVisible(show_exclude_non_coded_behaviors)
|
|
88
|
+
# show_time_bin_size:
|
|
89
|
+
paramPanelWindow.frm_time_bin_size.setVisible(show_time_bin_size)
|
|
90
|
+
|
|
91
|
+
if by_category:
|
|
92
|
+
paramPanelWindow.cbIncludeModifiers.setVisible(False)
|
|
93
|
+
paramPanelWindow.cbExcludeBehaviors.setVisible(False)
|
|
94
|
+
paramPanelWindow.cb_exclude_non_coded_modifiers.setVisible(False)
|
|
95
|
+
|
|
96
|
+
# set state of cb_exclude_non_coded_modifiers
|
|
97
|
+
paramPanelWindow.cb_exclude_non_coded_modifiers.setEnabled(paramPanelWindow.cbIncludeModifiers.isChecked())
|
|
98
|
+
|
|
99
|
+
paramPanelWindow.media_duration = maxTime
|
|
100
|
+
paramPanelWindow.start_coding = start_coding
|
|
101
|
+
paramPanelWindow.end_coding = end_coding
|
|
102
|
+
paramPanelWindow.start_interval = start_interval
|
|
103
|
+
paramPanelWindow.end_interval = end_interval
|
|
104
|
+
|
|
105
|
+
if self.timeFormat == cfg.S:
|
|
106
|
+
paramPanelWindow.start_time.rb_seconds.setChecked(True)
|
|
107
|
+
paramPanelWindow.end_time.rb_seconds.setChecked(True)
|
|
108
|
+
if self.timeFormat == cfg.HHMMSS:
|
|
109
|
+
paramPanelWindow.start_time.rb_time.setChecked(True)
|
|
110
|
+
paramPanelWindow.end_time.rb_time.setChecked(True)
|
|
111
|
+
|
|
112
|
+
if n_observations > 1:
|
|
113
|
+
paramPanelWindow.frm_time_interval.setVisible(False)
|
|
114
|
+
else:
|
|
115
|
+
if (start_coding is None) or (start_coding.is_nan()):
|
|
116
|
+
paramPanelWindow.rb_observed_events.setEnabled(False)
|
|
117
|
+
paramPanelWindow.frm_time_interval.setVisible(False)
|
|
118
|
+
paramPanelWindow.rb_user_defined.setVisible(False)
|
|
119
|
+
paramPanelWindow.rb_obs_interval.setVisible(False)
|
|
120
|
+
paramPanelWindow.rb_media_duration.setVisible(False)
|
|
121
|
+
else:
|
|
122
|
+
paramPanelWindow.frm_time_interval.setEnabled(False)
|
|
123
|
+
paramPanelWindow.start_time.set_time(start_coding)
|
|
124
|
+
paramPanelWindow.end_time.set_time(end_coding)
|
|
125
|
+
|
|
126
|
+
# check observation time interval
|
|
127
|
+
if start_interval is None or start_interval.is_nan() or end_interval is None or end_interval.is_nan():
|
|
128
|
+
paramPanelWindow.rb_obs_interval.setEnabled(False)
|
|
129
|
+
|
|
130
|
+
if selected_observations:
|
|
131
|
+
observedSubjects = project_functions.extract_observed_subjects(self.pj, selected_observations)
|
|
132
|
+
else:
|
|
133
|
+
# load all subjects and "No focal subject"
|
|
134
|
+
observedSubjects = [self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]] + [""]
|
|
135
|
+
selectedSubjects = []
|
|
136
|
+
|
|
137
|
+
# add 'No focal subject'
|
|
138
|
+
if "" in observedSubjects:
|
|
139
|
+
selectedSubjects.append(cfg.NO_FOCAL_SUBJECT)
|
|
140
|
+
paramPanelWindow.item = QListWidgetItem(paramPanelWindow.lwSubjects)
|
|
141
|
+
paramPanelWindow.ch = QCheckBox()
|
|
142
|
+
paramPanelWindow.ch.setText(cfg.NO_FOCAL_SUBJECT)
|
|
143
|
+
paramPanelWindow.ch.stateChanged.connect(paramPanelWindow.cb_changed)
|
|
144
|
+
paramPanelWindow.ch.setChecked(True)
|
|
145
|
+
paramPanelWindow.lwSubjects.setItemWidget(paramPanelWindow.item, paramPanelWindow.ch)
|
|
146
|
+
|
|
147
|
+
all_subjects = [self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in util.sorted_keys(self.pj[cfg.SUBJECTS])]
|
|
148
|
+
|
|
149
|
+
for subject in all_subjects:
|
|
150
|
+
paramPanelWindow.item = QListWidgetItem(paramPanelWindow.lwSubjects)
|
|
151
|
+
paramPanelWindow.ch = QCheckBox()
|
|
152
|
+
paramPanelWindow.ch.setText(subject)
|
|
153
|
+
paramPanelWindow.ch.stateChanged.connect(paramPanelWindow.cb_changed)
|
|
154
|
+
if subject in observedSubjects:
|
|
155
|
+
selectedSubjects.append(subject)
|
|
156
|
+
paramPanelWindow.ch.setChecked(True)
|
|
157
|
+
|
|
158
|
+
paramPanelWindow.lwSubjects.setItemWidget(paramPanelWindow.item, paramPanelWindow.ch)
|
|
159
|
+
|
|
160
|
+
logging.debug(f"selected subjects: {selectedSubjects}")
|
|
161
|
+
|
|
162
|
+
if selected_observations:
|
|
163
|
+
observedBehaviors = self.extract_observed_behaviors(selected_observations, selectedSubjects) # not sorted
|
|
164
|
+
else:
|
|
165
|
+
# load all behaviors
|
|
166
|
+
observedBehaviors = [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]]
|
|
167
|
+
|
|
168
|
+
logging.debug(f"observed behaviors: {observedBehaviors}")
|
|
169
|
+
|
|
170
|
+
if cfg.BEHAVIORAL_CATEGORIES in self.pj:
|
|
171
|
+
categories = self.pj[cfg.BEHAVIORAL_CATEGORIES][:]
|
|
172
|
+
# check if behavior not included in a category
|
|
173
|
+
try:
|
|
174
|
+
if "" in [
|
|
175
|
+
self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY]
|
|
176
|
+
for idx in self.pj[cfg.ETHOGRAM]
|
|
177
|
+
if cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
|
|
178
|
+
]:
|
|
179
|
+
categories += [""]
|
|
180
|
+
except Exception:
|
|
181
|
+
categories = ["###no category###"]
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
categories = ["###no category###"]
|
|
185
|
+
|
|
186
|
+
for category in categories:
|
|
187
|
+
if category != "###no category###":
|
|
188
|
+
if category == "":
|
|
189
|
+
paramPanelWindow.item = QListWidgetItem("No category")
|
|
190
|
+
paramPanelWindow.item.setData(34, "No category")
|
|
191
|
+
else:
|
|
192
|
+
paramPanelWindow.item = QListWidgetItem(category)
|
|
193
|
+
paramPanelWindow.item.setData(34, category)
|
|
194
|
+
|
|
195
|
+
font = QFont()
|
|
196
|
+
font.setBold(True)
|
|
197
|
+
paramPanelWindow.item.setFont(font)
|
|
198
|
+
paramPanelWindow.item.setData(33, "category")
|
|
199
|
+
paramPanelWindow.item.setData(35, False)
|
|
200
|
+
|
|
201
|
+
paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
|
|
202
|
+
|
|
203
|
+
for behavior in [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]:
|
|
204
|
+
if (categories == ["###no category###"]) or (
|
|
205
|
+
behavior
|
|
206
|
+
in [
|
|
207
|
+
self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE]
|
|
208
|
+
for x in self.pj[cfg.ETHOGRAM]
|
|
209
|
+
if cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][x] and self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY] == category
|
|
210
|
+
]
|
|
211
|
+
):
|
|
212
|
+
paramPanelWindow.item = QListWidgetItem(behavior)
|
|
213
|
+
if behavior in observedBehaviors:
|
|
214
|
+
paramPanelWindow.item.setCheckState(Qt.Checked)
|
|
215
|
+
else:
|
|
216
|
+
paramPanelWindow.item.setCheckState(Qt.Unchecked)
|
|
217
|
+
|
|
218
|
+
if category != "###no category###":
|
|
219
|
+
paramPanelWindow.item.setData(33, "behavior")
|
|
220
|
+
if category == "":
|
|
221
|
+
paramPanelWindow.item.setData(34, "No category")
|
|
222
|
+
else:
|
|
223
|
+
paramPanelWindow.item.setData(34, category)
|
|
224
|
+
|
|
225
|
+
paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
|
|
226
|
+
|
|
227
|
+
gui_utilities.restore_geometry(paramPanelWindow, "param panel", (600, 500))
|
|
228
|
+
|
|
229
|
+
if not paramPanelWindow.exec_():
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
gui_utilities.save_geometry(paramPanelWindow, "param panel")
|
|
233
|
+
|
|
234
|
+
selectedSubjects = paramPanelWindow.selectedSubjects
|
|
235
|
+
selectedBehaviors = paramPanelWindow.selectedBehaviors
|
|
236
|
+
|
|
237
|
+
logging.debug(f"selected subjects: {selectedSubjects}")
|
|
238
|
+
logging.debug(f"selected behaviors: {selectedBehaviors}")
|
|
239
|
+
|
|
240
|
+
if paramPanelWindow.rb_user_defined.isChecked():
|
|
241
|
+
startTime = paramPanelWindow.start_time.get_time()
|
|
242
|
+
endTime = paramPanelWindow.end_time.get_time()
|
|
243
|
+
|
|
244
|
+
if startTime > endTime:
|
|
245
|
+
QMessageBox.warning(
|
|
246
|
+
None,
|
|
247
|
+
cfg.programName,
|
|
248
|
+
"The start time is after the end time",
|
|
249
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
250
|
+
QMessageBox.NoButton,
|
|
251
|
+
)
|
|
252
|
+
return {cfg.SELECTED_SUBJECTS: [], cfg.SELECTED_BEHAVIORS: []}
|
|
253
|
+
|
|
254
|
+
elif paramPanelWindow.rb_obs_interval.isChecked() and not ((start_interval is None) or start_interval.is_nan()):
|
|
255
|
+
startTime = paramPanelWindow.start_time.get_time()
|
|
256
|
+
endTime = paramPanelWindow.end_time.get_time()
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
startTime = None
|
|
260
|
+
endTime = None
|
|
261
|
+
|
|
262
|
+
# if startTime is None:
|
|
263
|
+
# startTime = dec("NaN")
|
|
264
|
+
# if endTime is None:
|
|
265
|
+
# endTime = dec("NaN")
|
|
266
|
+
|
|
267
|
+
if paramPanelWindow.rb_media_duration.isChecked():
|
|
268
|
+
time_param = cfg.TIME_FULL_OBS
|
|
269
|
+
if paramPanelWindow.rb_observed_events.isChecked():
|
|
270
|
+
time_param = cfg.TIME_EVENTS
|
|
271
|
+
if paramPanelWindow.rb_obs_interval.isChecked():
|
|
272
|
+
time_param = cfg.TIME_OBS_INTERVAL
|
|
273
|
+
if paramPanelWindow.rb_user_defined.isChecked():
|
|
274
|
+
time_param = cfg.TIME_ARBITRARY_INTERVAL
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
cfg.SELECTED_SUBJECTS: selectedSubjects,
|
|
278
|
+
cfg.SELECTED_BEHAVIORS: selectedBehaviors,
|
|
279
|
+
cfg.INCLUDE_MODIFIERS: paramPanelWindow.cbIncludeModifiers.isChecked(),
|
|
280
|
+
cfg.EXCLUDE_BEHAVIORS: paramPanelWindow.cbExcludeBehaviors.isChecked(),
|
|
281
|
+
cfg.EXCLUDE_NON_CODED_MODIFIERS: paramPanelWindow.cb_exclude_non_coded_modifiers.isChecked(),
|
|
282
|
+
"time": time_param,
|
|
283
|
+
cfg.START_TIME: startTime,
|
|
284
|
+
cfg.END_TIME: endTime,
|
|
285
|
+
cfg.TIME_BIN_SIZE: paramPanelWindow.sb_time_bin_size.value(),
|
|
286
|
+
}
|
boris/state_events.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
|
|
21
|
+
Module containing functions for state events
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import time
|
|
26
|
+
from decimal import Decimal as dec
|
|
27
|
+
|
|
28
|
+
from PySide6.QtWidgets import QMessageBox, QAbstractItemView
|
|
29
|
+
|
|
30
|
+
from . import config as cfg
|
|
31
|
+
from . import dialog, project_functions, select_observations
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_state_events(self, mode: str = "all") -> None:
|
|
35
|
+
"""
|
|
36
|
+
check state events for each subject
|
|
37
|
+
use check_state_events_obs function in project_functions.py
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
mode (str): current: check current observation / all: ask user to select observations
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
tot_out = ""
|
|
44
|
+
if mode == "current":
|
|
45
|
+
if self.observationId:
|
|
46
|
+
_, msg = project_functions.check_state_events_obs(
|
|
47
|
+
self.observationId,
|
|
48
|
+
self.pj[cfg.ETHOGRAM],
|
|
49
|
+
self.pj[cfg.OBSERVATIONS][self.observationId],
|
|
50
|
+
self.timeFormat,
|
|
51
|
+
)
|
|
52
|
+
tot_out = f"Observation: <strong>{self.observationId}</strong><br>{msg}<br><br>"
|
|
53
|
+
|
|
54
|
+
if mode == "all":
|
|
55
|
+
if not self.pj[cfg.OBSERVATIONS]:
|
|
56
|
+
QMessageBox.warning(
|
|
57
|
+
self,
|
|
58
|
+
cfg.programName,
|
|
59
|
+
"The project does not contain any observation",
|
|
60
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
61
|
+
QMessageBox.NoButton,
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# ask user observations to analyze
|
|
66
|
+
_, selectedObservations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
|
|
67
|
+
if not selectedObservations:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
for obsId in sorted(selectedObservations):
|
|
71
|
+
r, msg = project_functions.check_state_events_obs(
|
|
72
|
+
obsId, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obsId], self.timeFormat
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
tot_out += f"<strong>{obsId}</strong><br>{msg}<br>"
|
|
76
|
+
|
|
77
|
+
results = dialog.Results_dialog()
|
|
78
|
+
results.setWindowTitle("Check state events")
|
|
79
|
+
results.ptText.clear()
|
|
80
|
+
results.ptText.setReadOnly(True)
|
|
81
|
+
results.ptText.appendHtml(tot_out)
|
|
82
|
+
results.exec_()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def fix_unpaired_events(self, silent_mode: bool = False):
|
|
86
|
+
"""
|
|
87
|
+
fix unpaired state events
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if self.observationId:
|
|
91
|
+
r, msg = project_functions.check_state_events_obs(
|
|
92
|
+
self.observationId, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][self.observationId]
|
|
93
|
+
)
|
|
94
|
+
if not silent_mode and "not PAIRED" not in msg:
|
|
95
|
+
QMessageBox.information(
|
|
96
|
+
None,
|
|
97
|
+
cfg.programName,
|
|
98
|
+
"All state events are already paired",
|
|
99
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
100
|
+
QMessageBox.NoButton,
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
w = dialog.Ask_time(0)
|
|
105
|
+
w.setWindowTitle("Fix UNPAIRED state events")
|
|
106
|
+
w.label.setText("Fix UNPAIRED events at time:")
|
|
107
|
+
|
|
108
|
+
if not w.exec_():
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
fix_at_time = w.time_widget.get_time()
|
|
112
|
+
if fix_at_time.is_nan():
|
|
113
|
+
QMessageBox.warning(
|
|
114
|
+
self,
|
|
115
|
+
cfg.programName,
|
|
116
|
+
("Select a time format"),
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
events_to_add = project_functions.fix_unpaired_state_events(
|
|
121
|
+
self.pj[cfg.ETHOGRAM],
|
|
122
|
+
self.pj[cfg.OBSERVATIONS][self.observationId],
|
|
123
|
+
fix_at_time - dec("0.001"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if events_to_add:
|
|
127
|
+
# determine the new frame index
|
|
128
|
+
if (self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA) and self.playerType == cfg.MEDIA:
|
|
129
|
+
mem_time = self.getLaps()
|
|
130
|
+
for event in events_to_add:
|
|
131
|
+
if not self.seek_mediaplayer(event[0]):
|
|
132
|
+
time.sleep(0.1)
|
|
133
|
+
frame_idx = self.get_frame_index()
|
|
134
|
+
event[cfg.PJ_OBS_FIELDS[cfg.MEDIA][cfg.FRAME_INDEX]] = frame_idx
|
|
135
|
+
self.seek_mediaplayer(mem_time)
|
|
136
|
+
|
|
137
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
|
|
138
|
+
self.project_changed()
|
|
139
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
|
|
140
|
+
self.load_tw_events(self.observationId)
|
|
141
|
+
|
|
142
|
+
index = self.tv_events.model().index(
|
|
143
|
+
[
|
|
144
|
+
event_idx
|
|
145
|
+
for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
146
|
+
if event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]] == fix_at_time
|
|
147
|
+
][0],
|
|
148
|
+
0,
|
|
149
|
+
)
|
|
150
|
+
self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
|
|
151
|
+
|
|
152
|
+
# selected observations
|
|
153
|
+
else:
|
|
154
|
+
_, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
|
|
155
|
+
if not selected_observations:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# check if state events are paired
|
|
159
|
+
out: str = ""
|
|
160
|
+
for obs_id in selected_observations:
|
|
161
|
+
r, msg = project_functions.check_state_events_obs(obs_id, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obs_id])
|
|
162
|
+
if "NOT PAIRED" in msg.upper():
|
|
163
|
+
# determine max time of events
|
|
164
|
+
fix_at_time = max(x[0] for x in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS])
|
|
165
|
+
# list of events to add to fix unpaired events
|
|
166
|
+
events_to_add = project_functions.fix_unpaired_state_events(
|
|
167
|
+
self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obs_id], fix_at_time
|
|
168
|
+
)
|
|
169
|
+
if events_to_add:
|
|
170
|
+
events_backup = self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][:]
|
|
171
|
+
self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].extend(events_to_add)
|
|
172
|
+
|
|
173
|
+
# check if modified obs if fixed
|
|
174
|
+
r, msg = project_functions.check_state_events_obs(obs_id, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obs_id])
|
|
175
|
+
if "NOT PAIRED" in msg.upper():
|
|
176
|
+
out += f"The observation <b>{obs_id}</b> can not be automatically fixed.<br><br>"
|
|
177
|
+
self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS] = events_backup
|
|
178
|
+
else:
|
|
179
|
+
out += f"<b>{obs_id}</b><br>"
|
|
180
|
+
self.project_changed()
|
|
181
|
+
if out:
|
|
182
|
+
out = "The following observations were modified to fix the unpaired state events:<br><br>" + out
|
|
183
|
+
self.results = dialog.Results_dialog()
|
|
184
|
+
self.results.setWindowTitle(cfg.programName + " - Fixed observations")
|
|
185
|
+
self.results.ptText.setReadOnly(True)
|
|
186
|
+
self.results.ptText.appendHtml(out)
|
|
187
|
+
self.results.pbSave.setVisible(False)
|
|
188
|
+
self.results.pbCancel.setVisible(True)
|
|
189
|
+
self.results.exec_()
|
|
190
|
+
else:
|
|
191
|
+
QMessageBox.information(
|
|
192
|
+
None,
|
|
193
|
+
cfg.programName,
|
|
194
|
+
"All state events are already paired",
|
|
195
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
196
|
+
QMessageBox.NoButton,
|
|
197
|
+
)
|
boris/subjects_pad.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from PySide6.QtCore import Signal, QRect, QEvent, Qt
|
|
23
|
+
from PySide6.QtWidgets import QGridLayout, QPushButton, QHBoxLayout, QWidget
|
|
24
|
+
|
|
25
|
+
from . import config as cfg
|
|
26
|
+
from . import utilities as util
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SubjectsPad(QWidget):
|
|
30
|
+
clickSignal = Signal(str)
|
|
31
|
+
sendEventSignal = Signal(QEvent)
|
|
32
|
+
close_signal = Signal(QRect)
|
|
33
|
+
|
|
34
|
+
def __init__(self, pj, filtered_subjects, parent=None):
|
|
35
|
+
super(SubjectsPad, self).__init__(parent)
|
|
36
|
+
self.pj = pj
|
|
37
|
+
self.filtered_subjects = filtered_subjects
|
|
38
|
+
|
|
39
|
+
self.setWindowTitle("Subjects pad")
|
|
40
|
+
self.grid = QGridLayout(self)
|
|
41
|
+
self.installEventFilter(self)
|
|
42
|
+
self.compose()
|
|
43
|
+
|
|
44
|
+
def compose(self):
|
|
45
|
+
for i in reversed(range(self.grid.count())):
|
|
46
|
+
self.grid.itemAt(i).widget().setParent(None)
|
|
47
|
+
|
|
48
|
+
subjects_list = [
|
|
49
|
+
["", self.pj[cfg.SUBJECTS][x]["name"]]
|
|
50
|
+
for x in util.sorted_keys(self.pj[cfg.SUBJECTS])
|
|
51
|
+
if self.pj[cfg.SUBJECTS][x]["name"] in self.filtered_subjects
|
|
52
|
+
]
|
|
53
|
+
dim = int(len(subjects_list) ** 0.5 + 0.999)
|
|
54
|
+
|
|
55
|
+
c = 0
|
|
56
|
+
for i in range(1, dim + 1):
|
|
57
|
+
for j in range(1, dim + 1):
|
|
58
|
+
if c >= len(subjects_list):
|
|
59
|
+
break
|
|
60
|
+
self.addWidget(subjects_list[c][1], i, j)
|
|
61
|
+
c += 1
|
|
62
|
+
|
|
63
|
+
def addWidget(self, subject, i, j):
|
|
64
|
+
self.grid.addWidget(Button(), i, j)
|
|
65
|
+
index = self.grid.count() - 1
|
|
66
|
+
widget = self.grid.itemAt(index).widget()
|
|
67
|
+
|
|
68
|
+
if widget is not None:
|
|
69
|
+
widget.pushButton.setText(subject)
|
|
70
|
+
color = "cyan"
|
|
71
|
+
widget.pushButton.setStyleSheet(
|
|
72
|
+
(
|
|
73
|
+
"background-color: {}; border-radius: 0px; min-width: 50px; max-width: 200px;"
|
|
74
|
+
" min-height:50px; max-height:200px; font-weight: bold;"
|
|
75
|
+
).format(color)
|
|
76
|
+
)
|
|
77
|
+
widget.pushButton.clicked.connect(lambda: self.click(subject))
|
|
78
|
+
|
|
79
|
+
def click(self, subject):
|
|
80
|
+
self.clickSignal.emit(subject)
|
|
81
|
+
|
|
82
|
+
def eventFilter(self, receiver, event):
|
|
83
|
+
"""
|
|
84
|
+
send event (if keypress) to main window
|
|
85
|
+
"""
|
|
86
|
+
if event.type() == QEvent.KeyPress:
|
|
87
|
+
self.sendEventSignal.emit(event)
|
|
88
|
+
return True
|
|
89
|
+
else:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
def closeEvent(self, event):
|
|
93
|
+
"""
|
|
94
|
+
send event for widget geometry memory
|
|
95
|
+
"""
|
|
96
|
+
self.close_signal.emit(self.geometry())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Button(QWidget):
|
|
100
|
+
def __init__(self, parent=None):
|
|
101
|
+
super(Button, self).__init__(parent)
|
|
102
|
+
self.pushButton = QPushButton()
|
|
103
|
+
self.pushButton.setFocusPolicy(Qt.NoFocus)
|
|
104
|
+
layout = QHBoxLayout()
|
|
105
|
+
layout.addWidget(self.pushButton)
|
|
106
|
+
self.setLayout(layout)
|