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.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/latency.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
Module for analyzing the latency of behaviors after another behavior(s) (marker)
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from . import config as cfg
|
|
27
|
+
from . import select_subj_behav
|
|
28
|
+
from . import dialog
|
|
29
|
+
from . import select_observations
|
|
30
|
+
from . import project_functions, observation_operations
|
|
31
|
+
|
|
32
|
+
from PySide6.QtWidgets import QMessageBox
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_latency(self):
|
|
36
|
+
"""
|
|
37
|
+
get latency (time after marker/stimulus)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
QMessageBox.warning(
|
|
41
|
+
None,
|
|
42
|
+
cfg.programName,
|
|
43
|
+
(
|
|
44
|
+
"This function is experimental. Please test it and report any bug at <br>"
|
|
45
|
+
'<a href="https://github.com/olivierfriard/BORIS/issues">'
|
|
46
|
+
"https://github.com/olivierfriard/BORIS/issues</a><br>"
|
|
47
|
+
"Thank you for your collaboration!"
|
|
48
|
+
),
|
|
49
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
50
|
+
QMessageBox.NoButton,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
SUBJECT, BEHAVIOR, MODIFIERS = 0, 1, 2
|
|
54
|
+
|
|
55
|
+
_, selected_observations = select_observations.select_observations2(
|
|
56
|
+
self, cfg.SELECT1, windows_title="Select one observation for latency analysis"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not selected_observations:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# check if coded behaviors are defined in ethogram
|
|
63
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# check if state events are paired
|
|
67
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
68
|
+
if not_ok or not selected_observations:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
72
|
+
# exit with message if events do not have timestamp
|
|
73
|
+
if start_coding.is_nan():
|
|
74
|
+
QMessageBox.critical(
|
|
75
|
+
None,
|
|
76
|
+
cfg.programName,
|
|
77
|
+
("This function is not available for observations with events that do not have timestamp"),
|
|
78
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
79
|
+
QMessageBox.NoButton,
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
|
|
84
|
+
self,
|
|
85
|
+
selected_observations,
|
|
86
|
+
show_exclude_non_coded_behaviors=False,
|
|
87
|
+
window_title="Select the marker behaviors (stimulus)",
|
|
88
|
+
n_observations=len(selected_observations),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if parameters == {}:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
95
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
marker_behaviors = parameters[cfg.SELECTED_BEHAVIORS]
|
|
99
|
+
marker_subjects = parameters[cfg.SELECTED_SUBJECTS]
|
|
100
|
+
include_marker_modifiers = parameters[cfg.INCLUDE_MODIFIERS]
|
|
101
|
+
|
|
102
|
+
print(f"{marker_behaviors=} {marker_subjects=} {include_marker_modifiers=}")
|
|
103
|
+
|
|
104
|
+
parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
|
|
105
|
+
self, selected_observations, show_exclude_non_coded_behaviors=False, window_title="Select the latency behaviors"
|
|
106
|
+
)
|
|
107
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
108
|
+
return
|
|
109
|
+
latency_behaviors = parameters[cfg.SELECTED_BEHAVIORS]
|
|
110
|
+
latency_subjects = parameters[cfg.SELECTED_SUBJECTS]
|
|
111
|
+
include_latency_modifiers = parameters[cfg.INCLUDE_MODIFIERS]
|
|
112
|
+
|
|
113
|
+
print(f"{latency_behaviors=} {latency_subjects=} {include_latency_modifiers=}")
|
|
114
|
+
|
|
115
|
+
results: dict = {}
|
|
116
|
+
for obs_id in selected_observations:
|
|
117
|
+
print(f"{obs_id=}")
|
|
118
|
+
|
|
119
|
+
events_with_status = project_functions.events_start_stop(
|
|
120
|
+
self.pj[cfg.ETHOGRAM],
|
|
121
|
+
self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS],
|
|
122
|
+
self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
print(f"{events_with_status=}")
|
|
126
|
+
|
|
127
|
+
for idx, event in enumerate(events_with_status):
|
|
128
|
+
print(f"{event=}")
|
|
129
|
+
|
|
130
|
+
print(f"{event[cfg.EVENT_STATUS_FIELD_IDX]=}")
|
|
131
|
+
|
|
132
|
+
print(f"{event[cfg.EVENT_BEHAVIOR_FIELD_IDX]=}")
|
|
133
|
+
|
|
134
|
+
print(f"{event[cfg.EVENT_SUBJECT_FIELD_IDX]=}")
|
|
135
|
+
|
|
136
|
+
if all(
|
|
137
|
+
(
|
|
138
|
+
event[cfg.EVENT_STATUS_FIELD_IDX] in (cfg.START, cfg.POINT),
|
|
139
|
+
event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in marker_behaviors,
|
|
140
|
+
any(
|
|
141
|
+
(
|
|
142
|
+
event[cfg.EVENT_SUBJECT_FIELD_IDX] in marker_subjects,
|
|
143
|
+
all((event[cfg.EVENT_SUBJECT_FIELD_IDX] == "", cfg.NO_FOCAL_SUBJECT in marker_subjects)),
|
|
144
|
+
)
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
):
|
|
148
|
+
if include_marker_modifiers:
|
|
149
|
+
marker = event[cfg.EVENT_TIME_FIELD_IDX : cfg.EVENT_MODIFIER_FIELD_IDX + 1]
|
|
150
|
+
else:
|
|
151
|
+
marker = event[cfg.EVENT_TIME_FIELD_IDX : cfg.EVENT_BEHAVIOR_FIELD_IDX + 1]
|
|
152
|
+
|
|
153
|
+
print(f"{marker=}")
|
|
154
|
+
|
|
155
|
+
if marker not in results:
|
|
156
|
+
results[marker] = {}
|
|
157
|
+
|
|
158
|
+
for event2 in events_with_status[idx + 1 :]:
|
|
159
|
+
if all(
|
|
160
|
+
(
|
|
161
|
+
event2[cfg.EVENT_STATUS_FIELD_IDX] in (cfg.START, cfg.POINT),
|
|
162
|
+
event2[cfg.EVENT_BEHAVIOR_FIELD_IDX] in latency_behaviors,
|
|
163
|
+
any(
|
|
164
|
+
(
|
|
165
|
+
event2[cfg.EVENT_SUBJECT_FIELD_IDX] in latency_subjects,
|
|
166
|
+
all(
|
|
167
|
+
(
|
|
168
|
+
event2[cfg.EVENT_SUBJECT_FIELD_IDX] == "",
|
|
169
|
+
cfg.NO_FOCAL_SUBJECT in latency_subjects,
|
|
170
|
+
)
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
):
|
|
176
|
+
print(event, event2)
|
|
177
|
+
if include_latency_modifiers:
|
|
178
|
+
latency = event2[cfg.EVENT_SUBJECT_FIELD_IDX : cfg.EVENT_MODIFIER_FIELD_IDX + 1]
|
|
179
|
+
else:
|
|
180
|
+
latency = event2[cfg.EVENT_SUBJECT_FIELD_IDX : cfg.EVENT_BEHAVIOR_FIELD_IDX + 1]
|
|
181
|
+
|
|
182
|
+
# print(f"{marker=}")
|
|
183
|
+
print(f"{latency=}")
|
|
184
|
+
if latency not in results[marker]:
|
|
185
|
+
results[marker][latency] = []
|
|
186
|
+
results[marker][latency].append(event2[cfg.EVENT_TIME_FIELD_IDX] - event[cfg.EVENT_TIME_FIELD_IDX])
|
|
187
|
+
|
|
188
|
+
print(f"{results[marker][latency]=}")
|
|
189
|
+
|
|
190
|
+
# check if new marker
|
|
191
|
+
if all(
|
|
192
|
+
(
|
|
193
|
+
event2[cfg.EVENT_STATUS_FIELD_IDX] in (cfg.START, cfg.POINT),
|
|
194
|
+
event2[cfg.EVENT_BEHAVIOR_FIELD_IDX] in marker_behaviors,
|
|
195
|
+
any(
|
|
196
|
+
(
|
|
197
|
+
event2[cfg.EVENT_SUBJECT_FIELD_IDX] in marker_subjects,
|
|
198
|
+
all(
|
|
199
|
+
(
|
|
200
|
+
event2[cfg.EVENT_SUBJECT_FIELD_IDX] == "",
|
|
201
|
+
cfg.NO_FOCAL_SUBJECT in marker_subjects,
|
|
202
|
+
)
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
):
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
# print()
|
|
213
|
+
# import pprint
|
|
214
|
+
# pprint.pprint(results)
|
|
215
|
+
|
|
216
|
+
out = ""
|
|
217
|
+
|
|
218
|
+
for marker in sorted(results.keys()):
|
|
219
|
+
subject = cfg.NO_FOCAL_SUBJECT if marker[cfg.EVENT_SUBJECT_FIELD_IDX] == "" else marker[1]
|
|
220
|
+
if include_marker_modifiers:
|
|
221
|
+
out += f"Marker: <b>{marker[cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> at {marker[cfg.EVENT_TIME_FIELD_IDX]} s (subject: {subject} - modifiers: {marker[cfg.EVENT_MODIFIER_FIELD_IDX]})<br><br>"
|
|
222
|
+
else:
|
|
223
|
+
out += f"Marker: <b>{marker[cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> at {marker[cfg.EVENT_TIME_FIELD_IDX]} s (subject: {subject})<br><br>"
|
|
224
|
+
for behav in results[marker]:
|
|
225
|
+
subject = cfg.NO_FOCAL_SUBJECT if behav[SUBJECT] == "" else behav[SUBJECT]
|
|
226
|
+
if include_latency_modifiers:
|
|
227
|
+
out += f"\nLatency for behavior: <b>{behav[BEHAVIOR]}</b> (subject: {subject} - modifiers: {behav[MODIFIERS]})<br>"
|
|
228
|
+
else:
|
|
229
|
+
out += f"\nLatency for behavior: <b>{behav[BEHAVIOR]}</b> (subject: {subject})<br>"
|
|
230
|
+
|
|
231
|
+
out += "first occurrence: "
|
|
232
|
+
out += f"{sorted(results[marker][behav])[0]} s<br>"
|
|
233
|
+
out += "all occurrences: "
|
|
234
|
+
|
|
235
|
+
out += ", ".join([f"{x} s" for x in sorted(results[marker][behav])])
|
|
236
|
+
out += "<br><br>"
|
|
237
|
+
|
|
238
|
+
out += "<br><br>"
|
|
239
|
+
|
|
240
|
+
self.results = dialog.Results_dialog()
|
|
241
|
+
self.results.setWindowTitle("Latency")
|
|
242
|
+
self.results.ptText.clear()
|
|
243
|
+
self.results.ptText.appendHtml(out)
|
|
244
|
+
self.results.show()
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
|
|
25
|
+
from PySide6.QtCore import Signal
|
|
26
|
+
|
|
27
|
+
# from PySide6.QtGui import *
|
|
28
|
+
from PySide6.QtWidgets import (
|
|
29
|
+
QApplication,
|
|
30
|
+
QWidget,
|
|
31
|
+
QRadioButton,
|
|
32
|
+
QLabel,
|
|
33
|
+
QHBoxLayout,
|
|
34
|
+
QVBoxLayout,
|
|
35
|
+
QLineEdit,
|
|
36
|
+
QPlainTextEdit,
|
|
37
|
+
QCheckBox,
|
|
38
|
+
QPushButton,
|
|
39
|
+
QFileDialog,
|
|
40
|
+
QMessageBox,
|
|
41
|
+
)
|
|
42
|
+
from . import dialog
|
|
43
|
+
from . import config as cfg
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class wgMeasurement(QWidget):
|
|
47
|
+
""" """
|
|
48
|
+
|
|
49
|
+
closeSignal, clearSignal = Signal(), Signal()
|
|
50
|
+
flagSaved = True
|
|
51
|
+
draw_mem = []
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
super().__init__()
|
|
55
|
+
|
|
56
|
+
self.setWindowTitle("Geometric measurements")
|
|
57
|
+
|
|
58
|
+
vbox = QVBoxLayout(self)
|
|
59
|
+
|
|
60
|
+
self.rbPoint = QRadioButton("Point (left click)")
|
|
61
|
+
vbox.addWidget(self.rbPoint)
|
|
62
|
+
|
|
63
|
+
self.rbDistance = QRadioButton("Distance (start: left click, end: right click)")
|
|
64
|
+
vbox.addWidget(self.rbDistance)
|
|
65
|
+
|
|
66
|
+
self.rbArea = QRadioButton("Area (left click for area vertices, right click to close area)")
|
|
67
|
+
vbox.addWidget(self.rbArea)
|
|
68
|
+
|
|
69
|
+
self.rbAngle = QRadioButton("Angle (vertex: left click, segments: right click)")
|
|
70
|
+
vbox.addWidget(self.rbAngle)
|
|
71
|
+
|
|
72
|
+
self.cbPersistentMeasurements = QCheckBox("Measurements are persistent")
|
|
73
|
+
self.cbPersistentMeasurements.setChecked(True)
|
|
74
|
+
vbox.addWidget(self.cbPersistentMeasurements)
|
|
75
|
+
|
|
76
|
+
vbox.addWidget(QLabel("<b>Scale</b>"))
|
|
77
|
+
|
|
78
|
+
hbox1 = QHBoxLayout()
|
|
79
|
+
|
|
80
|
+
self.lbRef = QLabel("Reference")
|
|
81
|
+
hbox1.addWidget(self.lbRef)
|
|
82
|
+
|
|
83
|
+
self.lbPx = QLabel("Pixels")
|
|
84
|
+
hbox1.addWidget(self.lbPx)
|
|
85
|
+
|
|
86
|
+
vbox.addLayout(hbox1)
|
|
87
|
+
|
|
88
|
+
hbox2 = QHBoxLayout()
|
|
89
|
+
|
|
90
|
+
self.leRef = QLineEdit()
|
|
91
|
+
self.leRef.setText("1")
|
|
92
|
+
hbox2.addWidget(self.leRef)
|
|
93
|
+
|
|
94
|
+
self.lePx = QLineEdit()
|
|
95
|
+
self.lePx.setText("1")
|
|
96
|
+
hbox2.addWidget(self.lePx)
|
|
97
|
+
|
|
98
|
+
vbox.addLayout(hbox2)
|
|
99
|
+
|
|
100
|
+
self.pte = QPlainTextEdit()
|
|
101
|
+
vbox.addWidget(self.pte)
|
|
102
|
+
|
|
103
|
+
self.status_lb = QLabel()
|
|
104
|
+
vbox.addWidget(self.status_lb)
|
|
105
|
+
|
|
106
|
+
hbox3 = QHBoxLayout()
|
|
107
|
+
|
|
108
|
+
self.pbClear = QPushButton("Clear measurements", clicked=self.pbClear_clicked)
|
|
109
|
+
hbox3.addWidget(self.pbClear)
|
|
110
|
+
|
|
111
|
+
self.pbSave = QPushButton("Save results", clicked=self.pbSave_clicked)
|
|
112
|
+
hbox3.addWidget(self.pbSave)
|
|
113
|
+
|
|
114
|
+
self.pbClose = QPushButton(cfg.CLOSE, clicked=self.pbClose_clicked)
|
|
115
|
+
hbox3.addWidget(self.pbClose)
|
|
116
|
+
|
|
117
|
+
vbox.addLayout(hbox3)
|
|
118
|
+
|
|
119
|
+
def pbClear_clicked(self):
|
|
120
|
+
"""
|
|
121
|
+
clear measurements draw and results
|
|
122
|
+
"""
|
|
123
|
+
self.draw_mem = {}
|
|
124
|
+
self.pte.clear()
|
|
125
|
+
self.clearSignal.emit()
|
|
126
|
+
|
|
127
|
+
def pbClose_clicked(self):
|
|
128
|
+
if not self.flagSaved:
|
|
129
|
+
response = dialog.MessageDialog(
|
|
130
|
+
cfg.programName,
|
|
131
|
+
"The current results are not saved. Do you want to save results before closing?",
|
|
132
|
+
[cfg.YES, cfg.NO, cfg.CANCEL],
|
|
133
|
+
)
|
|
134
|
+
if response == cfg.YES:
|
|
135
|
+
self.pbSave_clicked()
|
|
136
|
+
if response == cfg.CANCEL:
|
|
137
|
+
return
|
|
138
|
+
self.closeSignal.emit()
|
|
139
|
+
|
|
140
|
+
def pbSave_clicked(self):
|
|
141
|
+
"""
|
|
142
|
+
save results
|
|
143
|
+
"""
|
|
144
|
+
if self.pte.toPlainText():
|
|
145
|
+
fileName, _ = QFileDialog().getSaveFileName(self, "Save measurement results", "", "Text files (*.txt);;All files (*)")
|
|
146
|
+
if fileName:
|
|
147
|
+
with open(fileName, "w") as f:
|
|
148
|
+
f.write(self.pte.toPlainText())
|
|
149
|
+
self.flagSaved = True
|
|
150
|
+
else:
|
|
151
|
+
QMessageBox.information(self, cfg.programName, "There are no results to save")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
import sys
|
|
156
|
+
|
|
157
|
+
app = QApplication(sys.argv)
|
|
158
|
+
w = wgMeasurement(logging.getLogger().getEffectiveLevel())
|
|
159
|
+
w.show()
|
|
160
|
+
|
|
161
|
+
sys.exit(app.exec_())
|
boris/media_file.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
from PySide6.QtWidgets import QFileDialog
|
|
24
|
+
|
|
25
|
+
from . import config as cfg
|
|
26
|
+
from . import utilities as util
|
|
27
|
+
from . import dialog
|
|
28
|
+
from . import project_functions
|
|
29
|
+
from . import utilities as util
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_info(self) -> None:
|
|
33
|
+
"""
|
|
34
|
+
show info about media file (current media file if an observation is opened)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def media_analysis_str(ffmpeg_bin: str, media_full_path: str) -> str:
|
|
38
|
+
r = util.accurate_media_analysis(ffmpeg_bin, media_full_path)
|
|
39
|
+
|
|
40
|
+
if "error" in r:
|
|
41
|
+
ffmpeg_output = f"File path: {media_full_path}<br><br>{r['error']}<br><br>"
|
|
42
|
+
else:
|
|
43
|
+
ffmpeg_output = f"<br><b>{r['analysis_program']} analysis</b><br>"
|
|
44
|
+
|
|
45
|
+
ffmpeg_output += (
|
|
46
|
+
f"File path: <b>{media_full_path}</b><br><br>"
|
|
47
|
+
f"Duration: {r['duration']} seconds ({util.convertTime(self.timeFormat, r['duration'])})<br>"
|
|
48
|
+
f"FPS: {r['fps']}<br>"
|
|
49
|
+
f"Resolution: {r['resolution']} pixels<br>"
|
|
50
|
+
f"Format long name: {r.get('format_long_name', cfg.NA)}<br>"
|
|
51
|
+
f"Creation time: {r.get('creation_time', cfg.NA)}<br>"
|
|
52
|
+
f"Number of frames: {r['frames_number']}<br>"
|
|
53
|
+
f"Bitrate: {util.smart_size_format(r['bitrate'])} <br>"
|
|
54
|
+
f"Has video: {r['has_video']}<br>"
|
|
55
|
+
f"Has audio: {r['has_audio']}<br>"
|
|
56
|
+
f"File size: {util.smart_size_format(r.get('file size', cfg.NA))}<br>"
|
|
57
|
+
f"Video codec: {r.get('video_codec', cfg.NA)}<br>"
|
|
58
|
+
f"Audio codec: {r.get('audio_codec', cfg.NA)}<br>"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return ffmpeg_output
|
|
62
|
+
|
|
63
|
+
if self.observationId and self.playerType == cfg.MEDIA:
|
|
64
|
+
tot_output: str = ""
|
|
65
|
+
|
|
66
|
+
for i, dw in enumerate(self.dw_player):
|
|
67
|
+
if not (
|
|
68
|
+
str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
|
|
69
|
+
and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][str(i + 1)]
|
|
70
|
+
):
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
mpv_output = (
|
|
74
|
+
"<b>MPV information</b><br>"
|
|
75
|
+
f"Duration: {dw.player.duration} seconds ({util.seconds2time(dw.player.duration)})<br>"
|
|
76
|
+
# "Position: {} %<br>"
|
|
77
|
+
f"FPS: {dw.player.container_fps}<br>"
|
|
78
|
+
# "Rate: {}<br>"
|
|
79
|
+
f"Resolution: {dw.player.width}x{dw.player.height} pixels<br>"
|
|
80
|
+
# "Scale: {}<br>"
|
|
81
|
+
f"Video format: {dw.player.video_format}<br>"
|
|
82
|
+
# "State: {}<br>"
|
|
83
|
+
# "Media Resource Location: {}<br>"
|
|
84
|
+
# "File name: {}<br>"
|
|
85
|
+
# "Track: {}/{}<br>"
|
|
86
|
+
f"Number of media in media list: {dw.player.playlist_count}<br>"
|
|
87
|
+
f"Current time position: {dw.player.time_pos}<br>"
|
|
88
|
+
f"Aspect ratio: {round(dw.player.width / dw.player.height, 3)}<br>"
|
|
89
|
+
# "is seekable? {}<br>"
|
|
90
|
+
# "has_vout? {}<br>"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# FFmpeg/FFprobe analysis
|
|
94
|
+
ffmpeg_output: str = ""
|
|
95
|
+
for file_path in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][str(i + 1)]:
|
|
96
|
+
media_full_path = project_functions.full_path(file_path, self.projectFileName)
|
|
97
|
+
ffmpeg_output += media_analysis_str(self.ffmpeg_bin, media_full_path)
|
|
98
|
+
|
|
99
|
+
ffmpeg_output += f"<br>Total duration: {sum(self.dw_player[i].media_durations) / 1000} ({util.convertTime(self.timeFormat, sum(self.dw_player[i].media_durations) / 1000)})"
|
|
100
|
+
|
|
101
|
+
tot_output += mpv_output + ffmpeg_output + "<br><hr>"
|
|
102
|
+
|
|
103
|
+
else: # no open observation
|
|
104
|
+
file_paths, _ = QFileDialog().getOpenFileNames(self, "Select a media file", "", "Media files (*)")
|
|
105
|
+
if not file_paths:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
tot_output: str = ""
|
|
109
|
+
for file_path in file_paths:
|
|
110
|
+
tot_output += media_analysis_str(self.ffmpeg_bin, file_path)
|
|
111
|
+
|
|
112
|
+
self.results = dialog.Results_dialog()
|
|
113
|
+
self.results.setWindowTitle(f"{cfg.programName} - Media file information")
|
|
114
|
+
self.results.ptText.appendHtml(tot_output)
|
|
115
|
+
self.results.show()
|