boris-behav-obs 9.7.12__py3-none-any.whl → 9.8.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 +4 -3
- boris/add_modifier.py +1 -1
- boris/advanced_event_filtering.py +1 -1
- boris/analysis_plugins/export_to_feral.py +336 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +2 -2
- boris/behav_coding_map_creator.py +1 -1
- boris/behavior_binary_table.py +1 -1
- boris/behaviors_coding_map.py +1 -1
- boris/boris_cli.py +1 -1
- boris/cmd_arguments.py +1 -1
- boris/coding_pad.py +1 -1
- boris/config.py +15 -3
- boris/config_file.py +18 -19
- boris/connections.py +12 -13
- boris/converters.py +1 -1
- boris/converters_ui.py +2 -3
- boris/cooccurence.py +1 -1
- boris/core.py +168 -166
- boris/core_qrc.py +1830 -1967
- boris/core_ui.py +1 -1
- boris/db_functions.py +5 -14
- boris/dialog.py +24 -24
- boris/edit_event.py +1 -1
- boris/event_operations.py +1 -1
- boris/events_cursor.py +1 -1
- boris/events_snapshots.py +133 -78
- boris/exclusion_matrix.py +1 -1
- boris/export_events.py +49 -43
- boris/export_observation.py +1 -1
- boris/external_processes.py +1 -1
- boris/geometric_measurement.py +1 -1
- boris/gui_utilities.py +1 -1
- boris/image_overlay.py +1 -1
- boris/import_observations.py +1 -1
- boris/ipc_mpv.py +1 -1
- boris/irr.py +1 -1
- boris/latency.py +1 -1
- boris/measurement_widget.py +1 -1
- boris/media_file.py +1 -1
- boris/menu_options.py +14 -12
- boris/modifier_coding_map_creator.py +1 -1
- boris/modifiers_coding_map.py +1 -1
- boris/observation.py +13 -14
- boris/observation_operations.py +1 -1
- boris/observations_list.py +1 -1
- boris/otx_parser.py +1 -1
- boris/param_panel.py +1 -1
- boris/player_dock_widget.py +1 -1
- boris/plot_data_module.py +1 -1
- boris/plot_events.py +1 -1
- boris/plot_events_rt.py +1 -1
- boris/plot_spectrogram_rt.py +42 -73
- boris/plot_waveform_rt.py +1 -1
- boris/plugins.py +1 -1
- boris/preferences.py +35 -4
- boris/preferences_ui.py +48 -18
- boris/project.py +1 -1
- boris/project_functions.py +19 -22
- boris/project_import_export.py +1 -1
- boris/select_modifiers.py +1 -1
- boris/select_observations.py +22 -23
- boris/select_subj_behav.py +4 -4
- boris/state_events.py +1 -1
- boris/subjects_pad.py +1 -1
- boris/synthetic_time_budget.py +1 -1
- boris/time_budget_functions.py +1 -1
- boris/time_budget_widget.py +1 -1
- boris/transitions.py +1 -1
- boris/utilities.py +1 -1
- boris/version.py +3 -3
- boris/video_equalizer.py +1 -1
- boris/video_operations.py +1 -1
- boris/view_df.py +28 -4
- boris/write_event.py +1 -1
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/METADATA +2 -2
- boris_behav_obs-9.8.2.dist-info/RECORD +110 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/WHEEL +1 -1
- boris/analysis_plugins/_export_to_feral.py +0 -225
- boris_behav_obs-9.7.12.dist-info/RECORD +0 -110
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.12.dist-info → boris_behav_obs-9.8.2.dist-info}/top_level.txt +0 -0
boris/core_ui.py
CHANGED
|
@@ -912,7 +912,7 @@ class Ui_MainWindow(object):
|
|
|
912
912
|
self.actionEdit_selected_events.setText(QCoreApplication.translate("MainWindow", u"Edit selected event(s)", None))
|
|
913
913
|
self.actionShow_spectrogram.setText(QCoreApplication.translate("MainWindow", u"Show the sound spectrogram", None))
|
|
914
914
|
self.actionExport_events_as_Praat_TextGrid.setText(QCoreApplication.translate("MainWindow", u"as Praat TextGrid", None))
|
|
915
|
-
self.actionExtract_events_from_media_files.setText(QCoreApplication.translate("MainWindow", u"Extract
|
|
915
|
+
self.actionExtract_events_from_media_files.setText(QCoreApplication.translate("MainWindow", u"Extract clips from media files", None))
|
|
916
916
|
self.action_geometric_measurements.setText(QCoreApplication.translate("MainWindow", u"Geometric measurement", None))
|
|
917
917
|
self.actionFrame_forward.setText(QCoreApplication.translate("MainWindow", u"Frame forward", None))
|
|
918
918
|
self.actionFrame_backward.setText(QCoreApplication.translate("MainWindow", u"frame backward", None))
|
boris/db_functions.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
This program is free software; you can redistribute it and/or modify
|
|
@@ -21,12 +21,12 @@ Copyright 2012-2025 Olivier Friard
|
|
|
21
21
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
import sqlite3
|
|
25
24
|
import logging
|
|
25
|
+
import sqlite3
|
|
26
26
|
from typing import Optional, Tuple
|
|
27
|
+
|
|
27
28
|
from . import config as cfg
|
|
28
|
-
from . import project_functions
|
|
29
|
-
from . import event_operations
|
|
29
|
+
from . import event_operations, project_functions
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def load_events_in_db(
|
|
@@ -39,6 +39,7 @@ def load_events_in_db(
|
|
|
39
39
|
"""
|
|
40
40
|
populate a memory sqlite database with events from selected_observations,
|
|
41
41
|
selected_subjects and selected_behaviors
|
|
42
|
+
include modifiers
|
|
42
43
|
|
|
43
44
|
Args:
|
|
44
45
|
pj (dict): project dictionary
|
|
@@ -59,16 +60,6 @@ def load_events_in_db(
|
|
|
59
60
|
if cfg.STATE in pj[cfg.ETHOGRAM][x][cfg.TYPE].upper() and pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] in selected_behaviors
|
|
60
61
|
]
|
|
61
62
|
|
|
62
|
-
# selected behaviors defined as point event
|
|
63
|
-
"""
|
|
64
|
-
point_behaviors_codes = [
|
|
65
|
-
pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE]
|
|
66
|
-
for x in pj[cfg.ETHOGRAM]
|
|
67
|
-
if cfg.POINT in pj[cfg.ETHOGRAM][x][cfg.TYPE].upper()
|
|
68
|
-
and pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] in selected_behaviors
|
|
69
|
-
]
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
63
|
db = sqlite3.connect(":memory:", isolation_level=None)
|
|
73
64
|
|
|
74
65
|
"""
|
boris/dialog.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -21,23 +21,28 @@ This file is part of BORIS.
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
import datetime as dt
|
|
24
|
-
from decimal import Decimal as dec
|
|
25
24
|
import logging
|
|
26
25
|
import math
|
|
27
26
|
import pathlib as pl
|
|
28
27
|
import platform
|
|
29
28
|
import sys
|
|
30
29
|
import traceback
|
|
30
|
+
from decimal import Decimal as dec
|
|
31
31
|
from typing import Union
|
|
32
32
|
|
|
33
|
-
from PySide6.QtCore import
|
|
33
|
+
from PySide6.QtCore import QDateTime, QRect, QSize, Qt, QTime, Signal, qVersion
|
|
34
|
+
from PySide6.QtGui import QFont, QTextCursor
|
|
34
35
|
from PySide6.QtWidgets import (
|
|
35
|
-
QApplication,
|
|
36
36
|
QAbstractItemView,
|
|
37
|
+
QAbstractSpinBox,
|
|
38
|
+
QApplication,
|
|
37
39
|
QCheckBox,
|
|
38
40
|
QComboBox,
|
|
41
|
+
QDateTimeEdit,
|
|
39
42
|
QDialog,
|
|
43
|
+
QDoubleSpinBox,
|
|
40
44
|
QFileDialog,
|
|
45
|
+
QFrame,
|
|
41
46
|
QHBoxLayout,
|
|
42
47
|
QLabel,
|
|
43
48
|
QLineEdit,
|
|
@@ -46,26 +51,21 @@ from PySide6.QtWidgets import (
|
|
|
46
51
|
QMessageBox,
|
|
47
52
|
QPlainTextEdit,
|
|
48
53
|
QPushButton,
|
|
54
|
+
QRadioButton,
|
|
49
55
|
QSizePolicy,
|
|
50
56
|
QSpacerItem,
|
|
51
57
|
QSpinBox,
|
|
52
|
-
|
|
58
|
+
QStackedWidget,
|
|
53
59
|
QTableView,
|
|
54
60
|
QTableWidget,
|
|
61
|
+
QTimeEdit,
|
|
55
62
|
QVBoxLayout,
|
|
56
63
|
QWidget,
|
|
57
|
-
QDateTimeEdit,
|
|
58
|
-
QTimeEdit,
|
|
59
|
-
QAbstractSpinBox,
|
|
60
|
-
QRadioButton,
|
|
61
|
-
QStackedWidget,
|
|
62
|
-
QFrame,
|
|
63
64
|
)
|
|
64
|
-
from PySide6.QtGui import QFont, QTextCursor
|
|
65
65
|
|
|
66
66
|
from . import config as cfg
|
|
67
|
-
from . import version
|
|
68
67
|
from . import utilities as util
|
|
68
|
+
from . import version
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def MessageDialog(title: str, text: str, buttons: tuple) -> str:
|
|
@@ -538,8 +538,8 @@ class Video_overlay_dialog(QDialog):
|
|
|
538
538
|
None,
|
|
539
539
|
cfg.programName,
|
|
540
540
|
"Select a file containing a PNG image",
|
|
541
|
-
QMessageBox.Ok | QMessageBox.Default,
|
|
542
|
-
QMessageBox.NoButton,
|
|
541
|
+
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Default,
|
|
542
|
+
QMessageBox.StandardButton.NoButton,
|
|
543
543
|
)
|
|
544
544
|
return
|
|
545
545
|
|
|
@@ -548,8 +548,8 @@ class Video_overlay_dialog(QDialog):
|
|
|
548
548
|
None,
|
|
549
549
|
cfg.programName,
|
|
550
550
|
"The overlay position must be in x,y format",
|
|
551
|
-
QMessageBox.Ok | QMessageBox.Default,
|
|
552
|
-
QMessageBox.NoButton,
|
|
551
|
+
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Default,
|
|
552
|
+
QMessageBox.StandardButton.NoButton,
|
|
553
553
|
)
|
|
554
554
|
return
|
|
555
555
|
if self.le_overlay_position.text():
|
|
@@ -560,8 +560,8 @@ class Video_overlay_dialog(QDialog):
|
|
|
560
560
|
None,
|
|
561
561
|
cfg.programName,
|
|
562
562
|
"The overlay position must be in x,y format",
|
|
563
|
-
QMessageBox.Ok | QMessageBox.Default,
|
|
564
|
-
QMessageBox.NoButton,
|
|
563
|
+
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Default,
|
|
564
|
+
QMessageBox.StandardButton.NoButton,
|
|
565
565
|
)
|
|
566
566
|
return
|
|
567
567
|
self.accept()
|
|
@@ -590,18 +590,18 @@ class Input_dialog(QDialog):
|
|
|
590
590
|
|
|
591
591
|
self.elements: dict = {}
|
|
592
592
|
for element in elements_list:
|
|
593
|
-
if element[0] ==
|
|
593
|
+
if element[0] == cfg.CHECKBOX:
|
|
594
594
|
self.elements[element[1]] = QCheckBox(element[1])
|
|
595
595
|
self.elements[element[1]].setChecked(element[2])
|
|
596
596
|
hbox.addWidget(self.elements[element[1]])
|
|
597
597
|
|
|
598
|
-
if element[0] ==
|
|
598
|
+
if element[0] == cfg.LINE_EDIT:
|
|
599
599
|
lb = QLabel(element[1])
|
|
600
600
|
hbox.addWidget(lb)
|
|
601
601
|
self.elements[element[1]] = QLineEdit()
|
|
602
602
|
hbox.addWidget(self.elements[element[1]])
|
|
603
603
|
|
|
604
|
-
if element[0] ==
|
|
604
|
+
if element[0] == cfg.SPINBOX:
|
|
605
605
|
# 1 - Label
|
|
606
606
|
# 2 - minimum value
|
|
607
607
|
# 3 - maximum value
|
|
@@ -616,7 +616,7 @@ class Input_dialog(QDialog):
|
|
|
616
616
|
self.elements[element[1]].setValue(element[5])
|
|
617
617
|
hbox.addWidget(self.elements[element[1]])
|
|
618
618
|
|
|
619
|
-
if element[0] ==
|
|
619
|
+
if element[0] == cfg.DOUBLE_SPINBOX:
|
|
620
620
|
# 1 - Label
|
|
621
621
|
# 2 - minimum value
|
|
622
622
|
# 3 - maximum value
|
|
@@ -633,7 +633,7 @@ class Input_dialog(QDialog):
|
|
|
633
633
|
self.elements[element[1]].setDecimals(element[6])
|
|
634
634
|
hbox.addWidget(self.elements[element[1]])
|
|
635
635
|
|
|
636
|
-
if element[0] ==
|
|
636
|
+
if element[0] == cfg.ITEMS_LIST:
|
|
637
637
|
# 1 - Label
|
|
638
638
|
# 2 - Values (tuple of tuple: 0 - value; 1 - "", "selected")
|
|
639
639
|
lb = QLabel(element[1])
|
boris/edit_event.py
CHANGED
boris/event_operations.py
CHANGED
boris/events_cursor.py
CHANGED
boris/events_snapshots.py
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
|
+
This file is part of BORIS.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
8
9
|
it under the terms of the GNU General Public License as published by
|
|
9
|
-
the Free Software Foundation; either version
|
|
10
|
-
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
13
14
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
15
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
16
|
GNU General Public License for more details.
|
|
16
17
|
|
|
17
18
|
You should have received a copy of the GNU General Public License
|
|
18
|
-
along with this program; if not
|
|
19
|
-
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
-
MA 02110-1301, USA.
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
21
20
|
|
|
22
21
|
"""
|
|
23
22
|
|
|
@@ -27,17 +26,18 @@ import pathlib as pl
|
|
|
27
26
|
import subprocess
|
|
28
27
|
from decimal import Decimal as dec
|
|
29
28
|
|
|
30
|
-
from PySide6.
|
|
29
|
+
from PySide6.QtCore import QProcess
|
|
30
|
+
from PySide6.QtWidgets import QApplication, QFileDialog
|
|
31
31
|
|
|
32
32
|
from . import config as cfg
|
|
33
33
|
from . import db_functions, dialog, project_functions, select_observations, select_subj_behav
|
|
34
34
|
from . import utilities as util
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def extract_media_snapshots(self):
|
|
38
38
|
"""
|
|
39
39
|
create snapshots corresponding to coded events
|
|
40
|
-
|
|
40
|
+
Observations must be from media file and media files must have video
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
43
|
_, selected_observations = select_observations.select_observations2(
|
|
@@ -47,7 +47,7 @@ def events_snapshots(self):
|
|
|
47
47
|
return
|
|
48
48
|
|
|
49
49
|
# check if obs are MEDIA
|
|
50
|
-
live_images_obs_list = []
|
|
50
|
+
live_images_obs_list: list = []
|
|
51
51
|
for obs_id in selected_observations:
|
|
52
52
|
if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in [cfg.LIVE, cfg.IMAGES]:
|
|
53
53
|
live_images_obs_list.append(obs_id)
|
|
@@ -79,7 +79,7 @@ def events_snapshots(self):
|
|
|
79
79
|
selected_observations,
|
|
80
80
|
start_coding=dec("NaN"),
|
|
81
81
|
end_coding=dec("NaN"),
|
|
82
|
-
show_include_modifiers=
|
|
82
|
+
show_include_modifiers=True,
|
|
83
83
|
show_exclude_non_coded_behaviors=False,
|
|
84
84
|
n_observations=len(selected_observations),
|
|
85
85
|
)
|
|
@@ -93,8 +93,16 @@ def events_snapshots(self):
|
|
|
93
93
|
ib = dialog.Input_dialog(
|
|
94
94
|
label_caption="Choose parameters",
|
|
95
95
|
elements_list=[
|
|
96
|
-
(
|
|
97
|
-
(
|
|
96
|
+
(cfg.DOUBLE_SPINBOX, "Time interval around the events (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
97
|
+
(
|
|
98
|
+
cfg.ITEMS_LIST,
|
|
99
|
+
"Bitmap format",
|
|
100
|
+
(
|
|
101
|
+
("JPG - small size / low quality", ""),
|
|
102
|
+
("PNG - big size / high quality", ""),
|
|
103
|
+
# ("WEBP - small size / high quality", "")
|
|
104
|
+
),
|
|
105
|
+
),
|
|
98
106
|
],
|
|
99
107
|
title="Extract frames",
|
|
100
108
|
)
|
|
@@ -105,6 +113,8 @@ def events_snapshots(self):
|
|
|
105
113
|
frame_bitmap_format = "jpg"
|
|
106
114
|
elif "PNG" in ib.elements["Bitmap format"].currentText():
|
|
107
115
|
frame_bitmap_format = "png"
|
|
116
|
+
# elif "WEBP" in ib.elements["Bitmap format"].currentText():
|
|
117
|
+
# frame_bitmap_format = "webp"
|
|
108
118
|
else:
|
|
109
119
|
return
|
|
110
120
|
|
|
@@ -113,7 +123,7 @@ def events_snapshots(self):
|
|
|
113
123
|
self,
|
|
114
124
|
"Choose a directory to extract events",
|
|
115
125
|
os.path.expanduser("~"),
|
|
116
|
-
options=QFileDialog.ShowDirsOnly,
|
|
126
|
+
options=QFileDialog.Option.ShowDirsOnly,
|
|
117
127
|
)
|
|
118
128
|
if not export_dir:
|
|
119
129
|
return
|
|
@@ -137,10 +147,13 @@ def events_snapshots(self):
|
|
|
137
147
|
for subject in parameters[cfg.SELECTED_SUBJECTS]:
|
|
138
148
|
for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
139
149
|
cursor.execute(
|
|
140
|
-
"SELECT occurence FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
150
|
+
"SELECT occurence, modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
141
151
|
(obs_id, subject, behavior),
|
|
142
152
|
)
|
|
143
|
-
|
|
153
|
+
|
|
154
|
+
rows = tuple(
|
|
155
|
+
{"occurence": util.float2decimal(r["occurence"]), "modifiers": r[cfg.MODIFIERS]} for r in cursor.fetchall()
|
|
156
|
+
)
|
|
144
157
|
|
|
145
158
|
behavior_state = project_functions.event_type(behavior, self.pj[cfg.ETHOGRAM])
|
|
146
159
|
|
|
@@ -165,11 +178,11 @@ def events_snapshots(self):
|
|
|
165
178
|
"The following media file does not have video.<br>"
|
|
166
179
|
f"{self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]}"
|
|
167
180
|
),
|
|
168
|
-
|
|
181
|
+
(cfg.OK, cfg.ABORT),
|
|
169
182
|
)
|
|
170
183
|
if response == cfg.OK:
|
|
171
184
|
continue
|
|
172
|
-
if response ==
|
|
185
|
+
if response == cfg.ABORT:
|
|
173
186
|
return
|
|
174
187
|
|
|
175
188
|
# check FPS
|
|
@@ -194,11 +207,11 @@ def events_snapshots(self):
|
|
|
194
207
|
"The FPS was not found for the following media file:<br>"
|
|
195
208
|
f"{self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]}"
|
|
196
209
|
),
|
|
197
|
-
|
|
210
|
+
(cfg.OK, cfg.ABORT),
|
|
198
211
|
)
|
|
199
212
|
if response == cfg.OK:
|
|
200
213
|
continue
|
|
201
|
-
if response ==
|
|
214
|
+
if response == cfg.ABORT:
|
|
202
215
|
return
|
|
203
216
|
|
|
204
217
|
global_start = dec("0.000") if row["occurence"] < time_interval else round(row["occurence"] - time_interval, 3)
|
|
@@ -238,11 +251,11 @@ def events_snapshots(self):
|
|
|
238
251
|
"At the moment it no possible to extract frames "
|
|
239
252
|
"for this type of event.<br>"
|
|
240
253
|
),
|
|
241
|
-
|
|
254
|
+
(cfg.OK, cfg.ABORT),
|
|
242
255
|
)
|
|
243
256
|
if response == cfg.OK:
|
|
244
257
|
continue
|
|
245
|
-
if response ==
|
|
258
|
+
if response == cfg.ABORT:
|
|
246
259
|
return
|
|
247
260
|
|
|
248
261
|
# globalStop = round(rows[idx + 1]["occurence"] + time_interval, 3)
|
|
@@ -279,17 +292,23 @@ def events_snapshots(self):
|
|
|
279
292
|
else:
|
|
280
293
|
continue
|
|
281
294
|
|
|
282
|
-
ffmpeg_command = (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
295
|
+
ffmpeg_command = "".join(
|
|
296
|
+
[
|
|
297
|
+
f'"{self.ffmpeg_bin}" ',
|
|
298
|
+
f'-i "{media_path}" ',
|
|
299
|
+
f"-ss {start:.3f} ",
|
|
300
|
+
f"-vframes {vframes} ",
|
|
301
|
+
f'"{export_dir}{os.sep}',
|
|
302
|
+
f"{util.safeFileName(obs_id).replace(' ', '-')}",
|
|
303
|
+
f"_PLAYER{nplayer}",
|
|
304
|
+
f"_{util.safeFileName(subject).replace(' ', '-')}",
|
|
305
|
+
f"_{util.safeFileName(behavior).replace(' ', '-')}",
|
|
306
|
+
f"_{global_start:.3f}_%08d",
|
|
307
|
+
f"_{util.safeFileName(row[cfg.MODIFIERS].replace('|', '+')).replace(' ', '-')}"
|
|
308
|
+
if parameters[cfg.INCLUDE_MODIFIERS] and row[cfg.MODIFIERS]
|
|
309
|
+
else "",
|
|
310
|
+
f'.{frame_bitmap_format}"',
|
|
311
|
+
]
|
|
293
312
|
)
|
|
294
313
|
|
|
295
314
|
logging.debug(f"ffmpeg command: {ffmpeg_command}")
|
|
@@ -300,26 +319,29 @@ def events_snapshots(self):
|
|
|
300
319
|
self.statusbar.showMessage(f"Frames extracted in {export_dir}", 0)
|
|
301
320
|
|
|
302
321
|
|
|
303
|
-
def
|
|
322
|
+
def extract_media_clips(self):
|
|
304
323
|
"""
|
|
305
|
-
extract sub-sequences from media files corresponding to coded events
|
|
306
|
-
|
|
324
|
+
extract with FFmpeg sub-sequences from media files corresponding to coded events
|
|
325
|
+
In case of point event, from -n to +n seconds are extracted (n is asked to user)
|
|
307
326
|
"""
|
|
308
327
|
|
|
328
|
+
# def on_finished(self, exit_code, exit_status):
|
|
329
|
+
# self.statusbar.showMessage("Media sequences extracted", 0)
|
|
330
|
+
|
|
309
331
|
_, selected_observations = select_observations.select_observations2(
|
|
310
332
|
self, cfg.MULTIPLE, windows_title="Select observations for extracting events"
|
|
311
333
|
)
|
|
312
334
|
if not selected_observations:
|
|
313
335
|
return
|
|
314
336
|
|
|
315
|
-
# check if obs are
|
|
316
|
-
live_images_obs_list = []
|
|
337
|
+
# check if obs are from media files
|
|
338
|
+
live_images_obs_list: list = []
|
|
317
339
|
for obs_id in selected_observations:
|
|
318
|
-
if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in
|
|
340
|
+
if self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] in (cfg.LIVE, cfg.IMAGES):
|
|
319
341
|
live_images_obs_list.append(obs_id)
|
|
320
342
|
|
|
321
343
|
if live_images_obs_list:
|
|
322
|
-
out = "The following observations are live observations or observation from pictures and will be removed
|
|
344
|
+
out = "The following observations are live observations or observation from pictures and will be removed<br><br>"
|
|
323
345
|
out += "<br>".join(live_images_obs_list)
|
|
324
346
|
results = dialog.Results_dialog()
|
|
325
347
|
results.setWindowTitle(cfg.programName)
|
|
@@ -345,39 +367,59 @@ def extract_events(self):
|
|
|
345
367
|
selected_observations,
|
|
346
368
|
start_coding=dec("NaN"),
|
|
347
369
|
end_coding=dec("NaN"),
|
|
348
|
-
show_include_modifiers=
|
|
370
|
+
show_include_modifiers=True,
|
|
349
371
|
show_exclude_non_coded_behaviors=False,
|
|
350
372
|
)
|
|
351
373
|
if parameters == {}:
|
|
352
374
|
return
|
|
353
375
|
|
|
354
376
|
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
355
|
-
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
356
377
|
return
|
|
357
378
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
379
|
+
ib = dialog.Input_dialog(
|
|
380
|
+
label_caption="Choose parameters",
|
|
381
|
+
elements_list=[
|
|
382
|
+
(cfg.DOUBLE_SPINBOX, "Time interval around the events (in seconds)", 0.0, 86400, 1, 0, 3),
|
|
383
|
+
(
|
|
384
|
+
cfg.ITEMS_LIST,
|
|
385
|
+
"Tracks to extract",
|
|
386
|
+
(
|
|
387
|
+
("Video and audio", ""),
|
|
388
|
+
("Only video", ""),
|
|
389
|
+
("Only audio", ""),
|
|
390
|
+
),
|
|
391
|
+
),
|
|
392
|
+
],
|
|
393
|
+
title="Extract clips",
|
|
372
394
|
)
|
|
373
|
-
if not
|
|
395
|
+
if not ib.exec_():
|
|
374
396
|
return
|
|
375
397
|
|
|
398
|
+
timeOffset = util.float2decimal(ib.elements["Time interval around the events (in seconds)"].value())
|
|
399
|
+
items_to_extract = ib.elements["Tracks to extract"].currentText()
|
|
400
|
+
|
|
401
|
+
# Ask for time interval around the event
|
|
402
|
+
# while True:
|
|
403
|
+
# text, ok = QInputDialog.getDouble(self, "Time interval around the events", "Time (in seconds):", 0.0, 0.0, 86400, 1)
|
|
404
|
+
# if not ok:
|
|
405
|
+
# return
|
|
406
|
+
# try:
|
|
407
|
+
# timeOffset = util.float2decimal(text)
|
|
408
|
+
# break
|
|
409
|
+
# except Exception:
|
|
410
|
+
# QMessageBox.warning(self, cfg.programName, f"<b>{text}</b> is not recognized as time")
|
|
411
|
+
## ask for video / audio extraction
|
|
412
|
+
# items_to_extract, ok = QInputDialog.getItem(
|
|
413
|
+
# self, "Tracks to extract", "Tracks", ("Video and audio", "Only video", "Only audio"), 0, False
|
|
414
|
+
# )
|
|
415
|
+
# if not ok:
|
|
416
|
+
# return
|
|
417
|
+
|
|
376
418
|
export_dir = QFileDialog.getExistingDirectory(
|
|
377
419
|
self,
|
|
378
420
|
"Choose a directory to extract events",
|
|
379
421
|
os.path.expanduser("~"),
|
|
380
|
-
options=QFileDialog.ShowDirsOnly,
|
|
422
|
+
options=QFileDialog.Option.ShowDirsOnly,
|
|
381
423
|
)
|
|
382
424
|
if not export_dir:
|
|
383
425
|
return
|
|
@@ -393,7 +435,7 @@ def extract_events(self):
|
|
|
393
435
|
self.statusBar().showMessage("Extracting sequences from media files")
|
|
394
436
|
QApplication.processEvents()
|
|
395
437
|
|
|
396
|
-
ffmpeg_extract_command: str = '"{ffmpeg_bin}" -
|
|
438
|
+
ffmpeg_extract_command: str = '"{ffmpeg_bin}" -i "{input_}" -ss {start} -y -t {duration} {codecs} '
|
|
397
439
|
mem_command: str = ""
|
|
398
440
|
for obs_id in selected_observations:
|
|
399
441
|
for nplayer in self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
@@ -407,11 +449,12 @@ def extract_events(self):
|
|
|
407
449
|
for subject in parameters[cfg.SELECTED_SUBJECTS]:
|
|
408
450
|
for behavior in parameters[cfg.SELECTED_BEHAVIORS]:
|
|
409
451
|
cursor.execute(
|
|
410
|
-
"SELECT occurence FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
452
|
+
"SELECT occurence, modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
|
|
411
453
|
(obs_id, subject, behavior),
|
|
412
454
|
)
|
|
413
|
-
rows =
|
|
414
|
-
|
|
455
|
+
rows = tuple(
|
|
456
|
+
{"occurence": util.float2decimal(r["occurence"]), "modifiers": r[cfg.MODIFIERS]} for r in cursor.fetchall()
|
|
457
|
+
)
|
|
415
458
|
behavior_state = project_functions.event_type(behavior, self.pj[cfg.ETHOGRAM])
|
|
416
459
|
if behavior_state in cfg.STATE_EVENT_TYPES and len(rows) % 2: # unpaired events
|
|
417
460
|
continue
|
|
@@ -433,7 +476,7 @@ def extract_events(self):
|
|
|
433
476
|
dialog.MessageDialog(
|
|
434
477
|
cfg.programName,
|
|
435
478
|
f"The media file {self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]} does not have a video stream",
|
|
436
|
-
|
|
479
|
+
("Continue", "Abort"),
|
|
437
480
|
)
|
|
438
481
|
== "Abort"
|
|
439
482
|
):
|
|
@@ -461,9 +504,9 @@ def extract_events(self):
|
|
|
461
504
|
dialog.MessageDialog(
|
|
462
505
|
cfg.programName,
|
|
463
506
|
f"The media file {self.pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][nplayer][mediaFileIdx]} does not have an audio stream",
|
|
464
|
-
|
|
507
|
+
("Continue", cfg.ABORT),
|
|
465
508
|
)
|
|
466
|
-
==
|
|
509
|
+
== cfg.ABORT
|
|
467
510
|
):
|
|
468
511
|
return
|
|
469
512
|
else:
|
|
@@ -509,11 +552,11 @@ def extract_events(self):
|
|
|
509
552
|
"The event extends on 2 successive video. "
|
|
510
553
|
" At the moment it is not possible to extract this type of event.<br>"
|
|
511
554
|
),
|
|
512
|
-
|
|
555
|
+
(cfg.OK, cfg.ABORT),
|
|
513
556
|
)
|
|
514
557
|
if response == cfg.OK:
|
|
515
558
|
continue
|
|
516
|
-
if response ==
|
|
559
|
+
if response == cfg.ABORT:
|
|
517
560
|
return
|
|
518
561
|
|
|
519
562
|
globalStart = dec("0.000") if row["occurence"] < timeOffset else round(row["occurence"] - timeOffset, 3)
|
|
@@ -550,22 +593,27 @@ def extract_events(self):
|
|
|
550
593
|
continue
|
|
551
594
|
|
|
552
595
|
new_file_name = pl.Path(export_dir) / pl.Path(
|
|
553
|
-
(
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
596
|
+
"".join(
|
|
597
|
+
[
|
|
598
|
+
f"{util.safeFileName(obs_id).replace(' ', '-')}_",
|
|
599
|
+
f"PLAYER{nplayer}_",
|
|
600
|
+
f"{util.safeFileName(subject).replace(' ', '-')}_",
|
|
601
|
+
f"{util.safeFileName(behavior)}_",
|
|
602
|
+
f"{globalStart}-{globalStop}",
|
|
603
|
+
f"_{util.safeFileName(row[cfg.MODIFIERS].replace('|', '+')).replace(' ', '-')}"
|
|
604
|
+
if parameters[cfg.INCLUDE_MODIFIERS] and row[cfg.MODIFIERS]
|
|
605
|
+
else "",
|
|
606
|
+
f"{new_extension}",
|
|
607
|
+
]
|
|
560
608
|
)
|
|
561
|
-
)
|
|
609
|
+
)
|
|
562
610
|
|
|
563
611
|
if new_file_name.is_file():
|
|
564
612
|
if mem_command not in (cfg.OVERWRITE_ALL, cfg.SKIP_ALL):
|
|
565
613
|
mem_command = dialog.MessageDialog(
|
|
566
614
|
cfg.programName,
|
|
567
615
|
f"The file <b>{new_file_name}</b> already exists.",
|
|
568
|
-
|
|
616
|
+
(cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL),
|
|
569
617
|
)
|
|
570
618
|
if mem_command == cfg.CANCEL:
|
|
571
619
|
return
|
|
@@ -585,6 +633,13 @@ def extract_events(self):
|
|
|
585
633
|
|
|
586
634
|
logging.debug(f'ffmpeg command: {ffmpeg_command} "{new_file_name}"')
|
|
587
635
|
|
|
636
|
+
# run ffmpeg command non blocking UI
|
|
637
|
+
# self.process = QProcess(self)
|
|
638
|
+
# self.process.readyReadStandardOutput.connect(self.on_stdout)
|
|
639
|
+
# self.process.readyReadStandardError.connect(self.on_stderr)
|
|
640
|
+
# self.process.finished.connect(on_finished)
|
|
641
|
+
# self.process.start(ffmpeg_command, [str(new_file_name)])
|
|
642
|
+
|
|
588
643
|
p = subprocess.Popen(
|
|
589
644
|
f'{ffmpeg_command} "{new_file_name}"',
|
|
590
645
|
stdout=subprocess.PIPE,
|