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/utilities.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -19,33 +19,243 @@ Copyright 2012-2023 Olivier Friard
|
|
|
19
19
|
MA 02110-1301, USA.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
from
|
|
22
|
+
from decimal import Decimal as dec
|
|
23
|
+
from decimal import getcontext, ROUND_DOWN
|
|
24
|
+
from hachoir.metadata import extractMetadata
|
|
25
|
+
from hachoir.parser import createParser
|
|
26
|
+
from shutil import copyfile, which
|
|
27
|
+
from typing import Union, Tuple
|
|
23
28
|
import csv
|
|
24
29
|
import datetime
|
|
25
|
-
import
|
|
30
|
+
import datetime as dt
|
|
31
|
+
import exifread
|
|
26
32
|
import json
|
|
27
33
|
import logging
|
|
28
34
|
import math
|
|
35
|
+
import numpy as np
|
|
29
36
|
import os
|
|
30
|
-
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from PIL.ImageQt import Image
|
|
39
|
+
import platform
|
|
31
40
|
import re
|
|
32
|
-
import
|
|
41
|
+
import shutil
|
|
33
42
|
import subprocess
|
|
34
43
|
import sys
|
|
35
44
|
import urllib.parse
|
|
45
|
+
import urllib.request
|
|
36
46
|
import wave
|
|
37
|
-
|
|
38
|
-
from decimal import getcontext, ROUND_DOWN
|
|
39
|
-
from shutil import copyfile
|
|
40
|
-
from typing import Union, Optional, List, Tuple, Dict
|
|
41
|
-
|
|
42
|
-
import numpy as np
|
|
43
|
-
from PyQt5.QtGui import qRgb
|
|
44
|
-
from PyQt5.QtGui import QPixmap, QImage
|
|
47
|
+
import socket
|
|
45
48
|
|
|
46
|
-
from
|
|
49
|
+
from PySide6 import __version__ as pyside6_version
|
|
50
|
+
from PySide6.QtGui import QPixmap, QImage
|
|
51
|
+
from PySide6.QtCore import qVersion
|
|
47
52
|
|
|
48
53
|
from . import config as cfg
|
|
54
|
+
from . import version
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
if (sys.platform.startswith("win") or sys.platform.startswith("linux")) and ("-i" not in sys.argv) and ("--ipc" not in sys.argv):
|
|
59
|
+
try:
|
|
60
|
+
from . import mpv2 as mpv
|
|
61
|
+
except Exception:
|
|
62
|
+
logger.warning("MPV library not found")
|
|
63
|
+
|
|
64
|
+
if sys.platform.startswith("win"):
|
|
65
|
+
import ctypes
|
|
66
|
+
|
|
67
|
+
logger.info("The MPV library was not found!\nIt will be downloaded from the BORIS GitHub repository")
|
|
68
|
+
# ctypes.windll.user32.MessageBoxW(0, "The MPV library was not found!\nIt will be downloaded.", "BORIS", 0)
|
|
69
|
+
|
|
70
|
+
# test if following function works on windows
|
|
71
|
+
MessageBoxTimeoutW = ctypes.windll.user32.MessageBoxTimeoutW
|
|
72
|
+
MessageBoxTimeoutW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint]
|
|
73
|
+
ctypes.windll.user32.MessageBoxTimeoutW(
|
|
74
|
+
None, "The MPV library was not found.\nIt will be downloaded from the BORIS GitHub repository.", "MPV library", 0, 0, 10000
|
|
75
|
+
) # time out
|
|
76
|
+
|
|
77
|
+
# download libmpv2.dll from https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/
|
|
78
|
+
|
|
79
|
+
url: str = "https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/"
|
|
80
|
+
|
|
81
|
+
external_files_dir = ""
|
|
82
|
+
# search where to download libmpv-2.dll
|
|
83
|
+
|
|
84
|
+
external_files_dir = Path(__file__).parent / "misc"
|
|
85
|
+
if not external_files_dir.is_dir():
|
|
86
|
+
logger.info(f"Creating {external_files_dir} directory")
|
|
87
|
+
external_files_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
logger.info(f"MPV library directory: {external_files_dir}")
|
|
90
|
+
|
|
91
|
+
local_filename = external_files_dir / "libmpv-2.dll"
|
|
92
|
+
logger.info("Downloading libmpv-2.dll...")
|
|
93
|
+
try:
|
|
94
|
+
urllib.request.urlretrieve(url + "libmpv-2.dll", local_filename)
|
|
95
|
+
logger.info(f"File downloaded as {local_filename}")
|
|
96
|
+
except Exception:
|
|
97
|
+
logger.critical("The MPV library can not be downloaded! Check your connection.")
|
|
98
|
+
ctypes.windll.user32.MessageBoxW(0, "The MPV library can not be downloaded!\nCheck your connection.", "BORIS", 0)
|
|
99
|
+
sys.exit(5)
|
|
100
|
+
# reload package
|
|
101
|
+
try:
|
|
102
|
+
from . import mpv2 as mpv
|
|
103
|
+
except Exception:
|
|
104
|
+
logger.critical("MPV library not found after dowloading")
|
|
105
|
+
sys.exit(5)
|
|
106
|
+
|
|
107
|
+
elif sys.platform.startswith("linux"):
|
|
108
|
+
text = (
|
|
109
|
+
"The MPV library was not found!\nInstall it\n\n"
|
|
110
|
+
"With Debian/Ubuntu/Mint:\nsudo apt install libmpv2\n\n"
|
|
111
|
+
"With Fedora:\nsudo dnf install mpv-libs\n\n"
|
|
112
|
+
"With OpenSUSE:\nsudo zypper install mpv\n\n"
|
|
113
|
+
"Arch Linux / Manjaro:\nsudo pacman -S mpv\n\n"
|
|
114
|
+
)
|
|
115
|
+
if shutil.which("zenity") is not None:
|
|
116
|
+
subprocess.run(["zenity", "--error", f"--text={text}"])
|
|
117
|
+
elif shutil.which("kdialog"):
|
|
118
|
+
subprocess.run(["kdialog", "--msgbox", text])
|
|
119
|
+
elif shutil.which("gxmessage"):
|
|
120
|
+
subprocess.run(["gxmessage", text])
|
|
121
|
+
elif shutil.which("xmessage"):
|
|
122
|
+
subprocess.run(["xmessage", text])
|
|
123
|
+
|
|
124
|
+
sys.exit(5)
|
|
125
|
+
else:
|
|
126
|
+
sys.exit(5)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_mpv_ipc(socket_path: str = cfg.MPV_SOCKET) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
test if socket available
|
|
132
|
+
"""
|
|
133
|
+
if not os.path.exists(socket_path):
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
138
|
+
client.connect(socket_path)
|
|
139
|
+
client.close()
|
|
140
|
+
return True
|
|
141
|
+
except Exception:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def extract_exif_DateTimeOriginal(file_path: str) -> int:
|
|
146
|
+
"""
|
|
147
|
+
extract the EXIF DateTimeOriginal tag
|
|
148
|
+
return epoch time
|
|
149
|
+
if the tag is not available return -1
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
file_path (str): path of the media file
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
int: timestamp
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
with open(file_path, "rb") as f_in:
|
|
160
|
+
tags = exifread.process_file(f_in, details=False, stop_tag="EXIF DateTimeOriginal")
|
|
161
|
+
if "EXIF DateTimeOriginal" in tags:
|
|
162
|
+
date_time_original = (
|
|
163
|
+
f"{tags['EXIF DateTimeOriginal'].values[:4]}-"
|
|
164
|
+
f"{tags['EXIF DateTimeOriginal'].values[5:7]}-"
|
|
165
|
+
f"{tags['EXIF DateTimeOriginal'].values[8:10]} "
|
|
166
|
+
f"{tags['EXIF DateTimeOriginal'].values.split(' ')[-1]}"
|
|
167
|
+
)
|
|
168
|
+
return int(datetime.datetime.strptime(date_time_original, "%Y-%m-%d %H:%M:%S").timestamp())
|
|
169
|
+
else:
|
|
170
|
+
try:
|
|
171
|
+
# read from file name (YYYY-MM-DD_HHMMSS)
|
|
172
|
+
return int(datetime.datetime.strptime(Path(file_path).stem, "%Y-%m-%d_%H%M%S").timestamp())
|
|
173
|
+
except Exception:
|
|
174
|
+
# read from file name (YYYY-MM-DD_HH:MM:SS)
|
|
175
|
+
return int(datetime.datetime.strptime(Path(file_path).stem, "%Y-%m-%d_%H:%M:%S").timestamp())
|
|
176
|
+
|
|
177
|
+
except Exception:
|
|
178
|
+
return -1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def extract_video_creation_date(file_path: str) -> int | None:
|
|
182
|
+
"""
|
|
183
|
+
returns the timestamp of the media creation date time with Hachoir
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
logger.debug(f"extract_video_creation_date for {file_path}")
|
|
187
|
+
|
|
188
|
+
if not Path(file_path).is_file():
|
|
189
|
+
logger.debug(f"{file_path} not found")
|
|
190
|
+
return None
|
|
191
|
+
try:
|
|
192
|
+
parser = createParser(file_path)
|
|
193
|
+
metadata = extractMetadata(parser)
|
|
194
|
+
except Exception:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
if metadata.has("creation_date"):
|
|
198
|
+
if metadata.get("creation_date") == datetime.datetime(1904, 1, 1, 0, 0):
|
|
199
|
+
return None
|
|
200
|
+
return metadata.get("creation_date").timestamp()
|
|
201
|
+
else:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def extract_date_time_from_file_name(file_path: str) -> int | None:
|
|
206
|
+
"""
|
|
207
|
+
extract YYYY-MM-DD_HHMMSS or YYYY-MM-DD_HH:MM:SS from file name
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
patterns = (r"\d{4}-\d{2}-\d{2}_\d{6}", r"\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}")
|
|
211
|
+
for pattern in patterns:
|
|
212
|
+
matches = re.findall(pattern, file_path)
|
|
213
|
+
|
|
214
|
+
if matches:
|
|
215
|
+
if pattern == r"\d{4}-\d{2}-\d{2}_\d{6}":
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"extract_date_time_from_file_name timestamp from {file_path}: {int(datetime.datetime.strptime(matches[0], '%Y-%m-%d_%H%M%S').timestamp())}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return int(datetime.datetime.strptime(matches[0], "%Y-%m-%d_%H%M%S").timestamp())
|
|
221
|
+
|
|
222
|
+
if pattern == r"\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}":
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"extract_date_time_from_file_name timestamp from {file_path}: {int(datetime.datetime.strptime(matches[0], '%Y-%m-%d_%H:%M:%S').timestamp())}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return int(datetime.datetime.strptime(matches[0], "%Y-%m-%d_%H:%M:%S").timestamp())
|
|
228
|
+
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def mpv_lib_version() -> Tuple[str, str, str]:
|
|
233
|
+
"""
|
|
234
|
+
Version of MPV library
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
str: MPV library version
|
|
238
|
+
"""
|
|
239
|
+
if ("-i" in sys.argv) or ("--ipc" in sys.argv) or sys.platform.startswith("darwin"):
|
|
240
|
+
return "MPV IPC mode", "", ""
|
|
241
|
+
|
|
242
|
+
mpv_lib_file = None
|
|
243
|
+
if sys.platform.startswith("linux"):
|
|
244
|
+
mpv_lib_file = mpv.sofile
|
|
245
|
+
if sys.platform.startswith("win"):
|
|
246
|
+
mpv_lib_file = mpv.dll
|
|
247
|
+
|
|
248
|
+
return (".".join([str(x) for x in mpv._mpv_client_api_version()]), mpv_lib_file, mpv.MPV_VERSION)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def python_mpv_script_version() -> str:
|
|
252
|
+
"""
|
|
253
|
+
version of python-mpv script
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
return mpv.__version__
|
|
257
|
+
except Exception:
|
|
258
|
+
return "Not found"
|
|
49
259
|
|
|
50
260
|
|
|
51
261
|
def error_info(exc_info: tuple) -> tuple:
|
|
@@ -156,28 +366,7 @@ def return_file_header_footer(file_name: str, file_row_number: int = 0, row_numb
|
|
|
156
366
|
return header, footer
|
|
157
367
|
|
|
158
368
|
|
|
159
|
-
def
|
|
160
|
-
"""
|
|
161
|
-
Translate bytes to string.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
b (bytes): byte to convert
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
str: converted byte
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
if isinstance(b, bytes):
|
|
171
|
-
fileSystemEncoding = sys.getfilesystemencoding()
|
|
172
|
-
# hack for PyInstaller
|
|
173
|
-
if fileSystemEncoding is None:
|
|
174
|
-
fileSystemEncoding = "UTF-8"
|
|
175
|
-
return b.decode(fileSystemEncoding)
|
|
176
|
-
else:
|
|
177
|
-
return b
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def convertTime(time_format: str, sec: float) -> str:
|
|
369
|
+
def convertTime(time_format: str, sec: Union[float, dec]) -> Union[str, None]:
|
|
181
370
|
"""
|
|
182
371
|
convert time in base at the current format (S or HHMMSS)
|
|
183
372
|
|
|
@@ -200,6 +389,20 @@ def convertTime(time_format: str, sec: float) -> str:
|
|
|
200
389
|
return None
|
|
201
390
|
|
|
202
391
|
|
|
392
|
+
def smart_time_format(sec: Union[float, dec], time_format: str = cfg.S, cutoff: dec = cfg.SMART_TIME_CUTOFF_DEFAULT) -> str:
|
|
393
|
+
"""
|
|
394
|
+
Smart time format
|
|
395
|
+
returns time in seconds if <= cutoff else in HH:MM:SS.ZZZ format
|
|
396
|
+
"""
|
|
397
|
+
# cutoff = 0 follows the time format selectd by user
|
|
398
|
+
if cutoff == 0:
|
|
399
|
+
return convertTime(time_format, sec)
|
|
400
|
+
if sec <= cutoff:
|
|
401
|
+
return f"{sec:.3f}"
|
|
402
|
+
else:
|
|
403
|
+
return seconds2time(sec)
|
|
404
|
+
|
|
405
|
+
|
|
203
406
|
def convert_time_to_decimal(pj: dict) -> dict:
|
|
204
407
|
"""
|
|
205
408
|
convert time of project from float to decimal
|
|
@@ -212,7 +415,10 @@ def convert_time_to_decimal(pj: dict) -> dict:
|
|
|
212
415
|
"""
|
|
213
416
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
214
417
|
if cfg.TIME_OFFSET in pj[cfg.OBSERVATIONS][obs_id]:
|
|
215
|
-
pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
|
|
418
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] is not None:
|
|
419
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec(str(pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]))
|
|
420
|
+
else:
|
|
421
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec("0.000")
|
|
216
422
|
for idx, _ in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
217
423
|
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX] = dec(
|
|
218
424
|
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX]
|
|
@@ -221,18 +427,11 @@ def convert_time_to_decimal(pj: dict) -> dict:
|
|
|
221
427
|
return pj
|
|
222
428
|
|
|
223
429
|
|
|
224
|
-
def
|
|
430
|
+
def count_media_file(media_files: dict) -> int:
|
|
225
431
|
"""
|
|
226
|
-
|
|
432
|
+
count number of media file for observation
|
|
227
433
|
"""
|
|
228
|
-
|
|
229
|
-
try:
|
|
230
|
-
with open(file_name, "rb") as f:
|
|
231
|
-
for chunk in iter(lambda: f.read(4096), b""):
|
|
232
|
-
hash_md5.update(chunk)
|
|
233
|
-
return hash_md5.hexdigest()
|
|
234
|
-
except FileNotFoundError:
|
|
235
|
-
return ""
|
|
434
|
+
return sum([len(media_files[idx]) for idx in media_files])
|
|
236
435
|
|
|
237
436
|
|
|
238
437
|
def txt2np_array(
|
|
@@ -269,24 +468,40 @@ def txt2np_array(
|
|
|
269
468
|
np_converters: dict = {}
|
|
270
469
|
for column_idx in column_converter:
|
|
271
470
|
if column_converter[column_idx] in converters:
|
|
272
|
-
|
|
273
471
|
conv_name = column_converter[column_idx]
|
|
274
472
|
|
|
275
473
|
function = f"""def {conv_name}(INPUT):\n"""
|
|
276
|
-
function += """ INPUT = INPUT.decode("utf-8") if isinstance(INPUT, bytes) else INPUT"""
|
|
474
|
+
function += """ INPUT = INPUT.decode("utf-8") if isinstance(INPUT, bytes) else INPUT\n\n"""
|
|
277
475
|
for line in converters[conv_name]["code"].split("\n"):
|
|
278
476
|
function += f" {line}\n"
|
|
279
477
|
function += """ return OUTPUT"""
|
|
280
478
|
|
|
479
|
+
print("=============")
|
|
480
|
+
print(function)
|
|
481
|
+
print("=============")
|
|
482
|
+
|
|
483
|
+
import types
|
|
484
|
+
|
|
485
|
+
mod = types.ModuleType("converter_module")
|
|
486
|
+
exec(function, mod.__dict__)
|
|
487
|
+
|
|
488
|
+
"""
|
|
281
489
|
try:
|
|
282
490
|
exec(function)
|
|
283
491
|
except Exception:
|
|
284
492
|
return False, f"error in converter: {sys.exc_info()[1]}", np.array([])
|
|
285
493
|
|
|
286
|
-
|
|
494
|
+
print(f"{converters=}")
|
|
495
|
+
print(f"{column_converter=}")
|
|
496
|
+
print(locals())
|
|
497
|
+
print(f"{conv_name=}")
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
# np_converters[column_idx - 1] = locals()['conv_name']
|
|
501
|
+
np_converters[column_idx - 1] = getattr(mod, conv_name)
|
|
287
502
|
|
|
288
503
|
else:
|
|
289
|
-
return False, f"converter {
|
|
504
|
+
return False, f"converter {column_converter[column_idx]} not found", np.array([])
|
|
290
505
|
|
|
291
506
|
# snif txt file
|
|
292
507
|
try:
|
|
@@ -308,9 +523,7 @@ def txt2np_array(
|
|
|
308
523
|
return False, f"{sys.exc_info()[1]}", np.array([])
|
|
309
524
|
|
|
310
525
|
try:
|
|
311
|
-
data = np.loadtxt(
|
|
312
|
-
file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters
|
|
313
|
-
)
|
|
526
|
+
data = np.loadtxt(file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters)
|
|
314
527
|
|
|
315
528
|
except Exception:
|
|
316
529
|
return False, f"{sys.exc_info()[1]}", np.array([])
|
|
@@ -322,7 +535,7 @@ def txt2np_array(
|
|
|
322
535
|
return True, "", data
|
|
323
536
|
|
|
324
537
|
|
|
325
|
-
def versiontuple(version_str: str):
|
|
538
|
+
def versiontuple(version_str: str) -> tuple:
|
|
326
539
|
"""
|
|
327
540
|
Convert version from str to tuple of str
|
|
328
541
|
|
|
@@ -338,9 +551,34 @@ def versiontuple(version_str: str):
|
|
|
338
551
|
return tuple(filled)
|
|
339
552
|
|
|
340
553
|
|
|
554
|
+
def behavior_user_color(ethogram: dict, behavior_code: str) -> Union[str, None]:
|
|
555
|
+
"""
|
|
556
|
+
returns the color of behavior if defined else None
|
|
557
|
+
"""
|
|
558
|
+
for x in ethogram:
|
|
559
|
+
if ethogram[x][cfg.BEHAVIOR_CODE] == behavior_code:
|
|
560
|
+
if ethogram[x].get(cfg.COLOR, None) == "":
|
|
561
|
+
return None
|
|
562
|
+
else:
|
|
563
|
+
return ethogram[x].get(cfg.COLOR, None)
|
|
564
|
+
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def behav_category_user_color(behavioral_categories: dict, name: str) -> Union[str, None]:
|
|
569
|
+
"""
|
|
570
|
+
returns the color of the behavioral category if defined else None
|
|
571
|
+
"""
|
|
572
|
+
for key in behavioral_categories:
|
|
573
|
+
if behavioral_categories[key]["name"] == name:
|
|
574
|
+
return behavioral_categories[key].get(cfg.COLOR, None)
|
|
575
|
+
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
|
|
341
579
|
def state_behavior_codes(ethogram: dict) -> list:
|
|
342
580
|
"""
|
|
343
|
-
behavior codes defined as STATE event
|
|
581
|
+
returns a list of behavior codes defined as STATE event
|
|
344
582
|
|
|
345
583
|
Args:
|
|
346
584
|
ethogram (dict): ethogram dictionary
|
|
@@ -349,12 +587,12 @@ def state_behavior_codes(ethogram: dict) -> list:
|
|
|
349
587
|
list: list of behavior codes defined as STATE event
|
|
350
588
|
|
|
351
589
|
"""
|
|
352
|
-
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if
|
|
590
|
+
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if ethogram[x][cfg.TYPE] in cfg.STATE_EVENT_TYPES]
|
|
353
591
|
|
|
354
592
|
|
|
355
593
|
def point_behavior_codes(ethogram: dict) -> list:
|
|
356
594
|
"""
|
|
357
|
-
behavior codes defined as POINT event
|
|
595
|
+
returns a list of behavior codes defined as POINT event
|
|
358
596
|
|
|
359
597
|
Args:
|
|
360
598
|
ethogram (dict): ethogram dictionary
|
|
@@ -363,7 +601,7 @@ def point_behavior_codes(ethogram: dict) -> list:
|
|
|
363
601
|
list: list of behavior codes defined as POINT event
|
|
364
602
|
|
|
365
603
|
"""
|
|
366
|
-
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if
|
|
604
|
+
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if ethogram[x][cfg.TYPE] in (cfg.POINT_EVENT, cfg.POINT_EVENT_WITH_CODING_MAP)]
|
|
367
605
|
|
|
368
606
|
|
|
369
607
|
def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict:
|
|
@@ -388,7 +626,6 @@ def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict
|
|
|
388
626
|
intervals_behav = {}
|
|
389
627
|
|
|
390
628
|
for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
|
|
391
|
-
|
|
392
629
|
time_ = event[cfg.EVENT_TIME_FIELD_IDX]
|
|
393
630
|
subject = event[cfg.EVENT_SUBJECT_FIELD_IDX]
|
|
394
631
|
code = event[cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
@@ -396,9 +633,7 @@ def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict
|
|
|
396
633
|
|
|
397
634
|
# check if code is state
|
|
398
635
|
if code in state_events_list:
|
|
399
|
-
|
|
400
636
|
if (subject, code, modifier) in mem_behav and mem_behav[(subject, code, modifier)]:
|
|
401
|
-
|
|
402
637
|
if (subject, code, modifier) not in intervals_behav:
|
|
403
638
|
intervals_behav[(subject, code, modifier)] = []
|
|
404
639
|
intervals_behav[(subject, code, modifier)].append((mem_behav[(subject, code, modifier)], time_))
|
|
@@ -419,8 +654,21 @@ def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict
|
|
|
419
654
|
return {"error": ""}
|
|
420
655
|
|
|
421
656
|
|
|
657
|
+
def flatten_list(nested_list) -> list:
|
|
658
|
+
"""
|
|
659
|
+
Flatten a list of lists.
|
|
660
|
+
"""
|
|
661
|
+
flattened: list = []
|
|
662
|
+
for item in nested_list:
|
|
663
|
+
if isinstance(item, list):
|
|
664
|
+
flattened.extend(flatten_list(item))
|
|
665
|
+
else:
|
|
666
|
+
flattened.append(item)
|
|
667
|
+
return flattened
|
|
668
|
+
|
|
669
|
+
|
|
422
670
|
def get_current_states_modifiers_by_subject(
|
|
423
|
-
state_behaviors_codes: list, events: list, subjects: dict,
|
|
671
|
+
state_behaviors_codes: list, events: list, subjects: dict, time_: dec, include_modifiers: bool = False
|
|
424
672
|
) -> dict:
|
|
425
673
|
"""
|
|
426
674
|
get current states and modifiers (if requested) for subjects at given time
|
|
@@ -436,7 +684,7 @@ def get_current_states_modifiers_by_subject(
|
|
|
436
684
|
dict: current states by subject. dict of list
|
|
437
685
|
"""
|
|
438
686
|
current_states: dict = {}
|
|
439
|
-
if
|
|
687
|
+
if time_.is_nan():
|
|
440
688
|
for idx in subjects:
|
|
441
689
|
current_states[idx] = []
|
|
442
690
|
return current_states
|
|
@@ -449,43 +697,45 @@ def get_current_states_modifiers_by_subject(
|
|
|
449
697
|
|
|
450
698
|
if include_modifiers:
|
|
451
699
|
for idx in subjects:
|
|
452
|
-
current_states[idx] =
|
|
453
|
-
|
|
454
|
-
|
|
700
|
+
current_states[subjects[idx]["name"]] = {}
|
|
701
|
+
for x in events:
|
|
702
|
+
if x[check_index] > time_:
|
|
703
|
+
break
|
|
704
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
|
|
705
|
+
if (x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX]) not in current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]]:
|
|
706
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])] = (
|
|
707
|
+
False
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][
|
|
455
711
|
(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
|
|
456
|
-
|
|
457
|
-
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx][cfg.SUBJECT_NAME]
|
|
458
|
-
and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
|
|
459
|
-
and x[check_index] <= time
|
|
460
|
-
]
|
|
712
|
+
] = not current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])]
|
|
461
713
|
|
|
462
|
-
|
|
463
|
-
|
|
714
|
+
r: dict = {}
|
|
715
|
+
for idx in subjects:
|
|
716
|
+
r[idx] = [f"{bm[0]} ({bm[1]})" for bm in current_states[subjects[idx]["name"]] if current_states[subjects[idx]["name"]][bm]]
|
|
464
717
|
|
|
465
718
|
else:
|
|
466
719
|
for idx in subjects:
|
|
467
|
-
current_states[idx] =
|
|
468
|
-
for
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
current_states[idx].append(sbc)
|
|
720
|
+
current_states[subjects[idx]["name"]] = {}
|
|
721
|
+
for b in state_behaviors_codes:
|
|
722
|
+
current_states[subjects[idx]["name"]][b] = False
|
|
723
|
+
for x in events:
|
|
724
|
+
if x[check_index] > time_:
|
|
725
|
+
break
|
|
726
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
|
|
727
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = not current_states[
|
|
728
|
+
x[cfg.EVENT_SUBJECT_FIELD_IDX]
|
|
729
|
+
][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]]
|
|
730
|
+
|
|
731
|
+
r: dict = {}
|
|
732
|
+
for idx in subjects:
|
|
733
|
+
r[idx] = [b for b in state_behaviors_codes if current_states[subjects[idx]["name"]][b]]
|
|
482
734
|
|
|
483
|
-
return
|
|
735
|
+
return r
|
|
484
736
|
|
|
485
737
|
|
|
486
|
-
def get_current_states_modifiers_by_subject_2(
|
|
487
|
-
state_behaviors_codes: list, events: list, subjects: dict, time: dec
|
|
488
|
-
) -> dict:
|
|
738
|
+
def get_current_states_modifiers_by_subject_2(state_behaviors_codes: list, events: list, subjects: dict, time: dec) -> dict:
|
|
489
739
|
"""
|
|
490
740
|
get current states and modifiers for subjects at given time
|
|
491
741
|
differs from get_current_states_modifiers_by_subject in the output format: [behavior, modifiers]
|
|
@@ -495,7 +745,6 @@ def get_current_states_modifiers_by_subject_2(
|
|
|
495
745
|
events (list): list of events
|
|
496
746
|
subjects (dict): dictionary of subjects
|
|
497
747
|
time (Decimal): time
|
|
498
|
-
include_modifiers (bool): include modifier if True (default: False)
|
|
499
748
|
|
|
500
749
|
Returns:
|
|
501
750
|
dict: current states by subject. dict of list
|
|
@@ -549,7 +798,8 @@ def get_current_points_by_subject(
|
|
|
549
798
|
point_events = [
|
|
550
799
|
(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
|
|
551
800
|
for x in events
|
|
552
|
-
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
|
|
801
|
+
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
|
|
802
|
+
and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
|
|
553
803
|
# and abs(x[EVENT_TIME_FIELD_IDX] - time) <= tolerance
|
|
554
804
|
and time <= x[cfg.EVENT_TIME_FIELD_IDX] < (time + tolerance)
|
|
555
805
|
]
|
|
@@ -566,19 +816,6 @@ def get_current_points_by_subject(
|
|
|
566
816
|
return current_points
|
|
567
817
|
|
|
568
818
|
|
|
569
|
-
def get_ip_address():
|
|
570
|
-
"""Get current IP address
|
|
571
|
-
|
|
572
|
-
Args:
|
|
573
|
-
|
|
574
|
-
Returns:
|
|
575
|
-
str: IP address
|
|
576
|
-
"""
|
|
577
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
578
|
-
s.connect(("8.8.8.8", 80))
|
|
579
|
-
return s.getsockname()[0]
|
|
580
|
-
|
|
581
|
-
|
|
582
819
|
def check_txt_file(file_name: str) -> dict:
|
|
583
820
|
"""
|
|
584
821
|
Extract parameters of txt file (test for tsv csv)
|
|
@@ -598,7 +835,7 @@ def check_txt_file(file_name: str) -> dict:
|
|
|
598
835
|
buff = csvfile.read(4096)
|
|
599
836
|
snif = csv.Sniffer()
|
|
600
837
|
dialect = snif.sniff(buff)
|
|
601
|
-
|
|
838
|
+
has_header = snif.has_header(buff)
|
|
602
839
|
|
|
603
840
|
csv.register_dialect("dialect", dialect)
|
|
604
841
|
rows_len: list = []
|
|
@@ -628,6 +865,7 @@ def check_txt_file(file_name: str) -> dict:
|
|
|
628
865
|
"fields number": rows_len[0],
|
|
629
866
|
"separator": dialect.delimiter,
|
|
630
867
|
"rows number": rows_number,
|
|
868
|
+
"has header": has_header,
|
|
631
869
|
}
|
|
632
870
|
else:
|
|
633
871
|
return {"homogeneous": False}
|
|
@@ -647,15 +885,15 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
|
|
|
647
885
|
str: wav file path or "" if error
|
|
648
886
|
"""
|
|
649
887
|
|
|
650
|
-
wav_file_path =
|
|
888
|
+
wav_file_path = Path(tmp_dir) / Path(media_file_path + ".wav").name
|
|
651
889
|
|
|
652
890
|
# check if media file is a wav file
|
|
653
891
|
try:
|
|
654
892
|
wav = wave.open(media_file_path, "r")
|
|
655
893
|
wav.close()
|
|
656
|
-
|
|
894
|
+
logger.debug(f"{media_file_path} is a WAV file. Copying in the temp directory...")
|
|
657
895
|
copyfile(media_file_path, wav_file_path)
|
|
658
|
-
|
|
896
|
+
logger.debug(f"{media_file_path} copied in {wav_file_path}")
|
|
659
897
|
return str(wav_file_path)
|
|
660
898
|
except Exception:
|
|
661
899
|
if wav_file_path.is_file():
|
|
@@ -670,7 +908,7 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
|
|
|
670
908
|
)
|
|
671
909
|
out, error = p.communicate()
|
|
672
910
|
out, error = out.decode("utf-8"), error.decode("utf-8")
|
|
673
|
-
|
|
911
|
+
logger.debug(f"{out}, {error}")
|
|
674
912
|
|
|
675
913
|
if "does not contain any stream" not in error:
|
|
676
914
|
if wav_file_path.is_file():
|
|
@@ -686,40 +924,46 @@ def decimal_default(obj):
|
|
|
686
924
|
raise TypeError
|
|
687
925
|
|
|
688
926
|
|
|
689
|
-
def complete(
|
|
927
|
+
def complete(lst: list, max_: int) -> list:
|
|
690
928
|
"""
|
|
691
929
|
complete list with empty string ("") until len = max
|
|
692
930
|
|
|
693
931
|
Args:
|
|
694
|
-
|
|
932
|
+
lst (list): list to complete
|
|
695
933
|
max_ (int): number of items to reach
|
|
696
934
|
|
|
697
935
|
Returns:
|
|
698
936
|
list: list completed to max_ items with empty string ("")
|
|
699
937
|
"""
|
|
700
|
-
while len(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
return l
|
|
938
|
+
while len(lst) < max_:
|
|
939
|
+
lst.append("")
|
|
940
|
+
return lst
|
|
704
941
|
|
|
705
942
|
|
|
706
943
|
def datetime_iso8601(dt) -> str:
|
|
707
944
|
"""
|
|
708
|
-
current date time in ISO8601 format without
|
|
945
|
+
current date time in ISO8601 format without microseconds
|
|
709
946
|
example: 2019-06-13 10:01:02
|
|
710
947
|
|
|
711
948
|
Returns:
|
|
712
|
-
str: date time in ISO8601 format
|
|
949
|
+
str: date time in ISO8601 format without microseconds
|
|
713
950
|
"""
|
|
714
|
-
return dt.isoformat(" "
|
|
951
|
+
return dt.isoformat(sep=" ", timespec="seconds")
|
|
715
952
|
|
|
716
953
|
|
|
717
|
-
def seconds_of_day(dt) -> dec:
|
|
954
|
+
def seconds_of_day(timestamp: dt.datetime) -> dec:
|
|
718
955
|
"""
|
|
719
956
|
return the number of seconds since start of the day
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
dev: number of seconds since the start of the day
|
|
720
960
|
"""
|
|
721
961
|
|
|
722
|
-
|
|
962
|
+
# logger.debug("function: seconds_of_day")
|
|
963
|
+
# logger.debug(f"{timestamp = }")
|
|
964
|
+
|
|
965
|
+
t = timestamp.time()
|
|
966
|
+
return dec(t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1000000).quantize(dec("0.001"))
|
|
723
967
|
|
|
724
968
|
|
|
725
969
|
def sorted_keys(d: dict) -> list:
|
|
@@ -749,7 +993,7 @@ def intfloatstr(s: str) -> int:
|
|
|
749
993
|
return s
|
|
750
994
|
|
|
751
995
|
|
|
752
|
-
def distance(p1, p2):
|
|
996
|
+
def distance(p1: tuple, p2: tuple) -> float:
|
|
753
997
|
"""
|
|
754
998
|
euclidean distance between 2 points
|
|
755
999
|
"""
|
|
@@ -781,6 +1025,62 @@ def angle(vertex: tuple, side1: tuple, side2: tuple) -> float:
|
|
|
781
1025
|
)
|
|
782
1026
|
|
|
783
1027
|
|
|
1028
|
+
def oriented_angle(P1: tuple, P2: tuple, P3: tuple) -> float:
|
|
1029
|
+
"""
|
|
1030
|
+
Calculate the oriented angle between two segments.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
P1 (tuple): Coordinates of the vertex
|
|
1034
|
+
P2 (tuple): Coordinates of the first point
|
|
1035
|
+
P3 (tuple): Coordinates of the second point
|
|
1036
|
+
|
|
1037
|
+
Returns:
|
|
1038
|
+
float: The oriented angle between the two segments in degrees.
|
|
1039
|
+
"""
|
|
1040
|
+
|
|
1041
|
+
x1, y1 = P1
|
|
1042
|
+
x2, y2 = P2
|
|
1043
|
+
x3, y3 = P1
|
|
1044
|
+
x4, y4 = P3
|
|
1045
|
+
|
|
1046
|
+
angle_AB = math.atan2(y2 - y1, x2 - x1)
|
|
1047
|
+
angle_CD = math.atan2(y4 - y3, x4 - x3)
|
|
1048
|
+
|
|
1049
|
+
oriented_angle = math.degrees(angle_AB - angle_CD)
|
|
1050
|
+
|
|
1051
|
+
return oriented_angle
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def oriented_angle_trigo(B: Tuple[float, float], A: Tuple[float, float], C: Tuple[float, float]) -> float:
|
|
1055
|
+
"""
|
|
1056
|
+
Calculates the oriented angle between vectors BA and BC, in degrees.
|
|
1057
|
+
The angle is positive in the counter-clockwise (trigonometric) direction.
|
|
1058
|
+
|
|
1059
|
+
Parameters:
|
|
1060
|
+
B: The pivot point (the origin of the vectors BA and BC).
|
|
1061
|
+
A, C: Points that define the vectors.
|
|
1062
|
+
|
|
1063
|
+
Returns:
|
|
1064
|
+
Angle in degrees, between 0 and 360.
|
|
1065
|
+
"""
|
|
1066
|
+
# Vectors BA and BC
|
|
1067
|
+
v1 = (A[0] - B[0], A[1] - B[1])
|
|
1068
|
+
v2 = (C[0] - B[0], C[1] - B[1])
|
|
1069
|
+
|
|
1070
|
+
# Dot product and 2D cross product (determinant)
|
|
1071
|
+
dot = v1[0] * v2[0] + v1[1] * v2[1]
|
|
1072
|
+
det = v1[0] * v2[1] - v1[1] * v2[0]
|
|
1073
|
+
|
|
1074
|
+
# Signed angle in radians, then converted to degrees
|
|
1075
|
+
angle_rad = math.atan2(det, dot)
|
|
1076
|
+
angle_deg = math.degrees(angle_rad)
|
|
1077
|
+
|
|
1078
|
+
if angle_deg < 0:
|
|
1079
|
+
angle_deg += 360
|
|
1080
|
+
|
|
1081
|
+
return angle_deg
|
|
1082
|
+
|
|
1083
|
+
|
|
784
1084
|
def mem_info():
|
|
785
1085
|
"""
|
|
786
1086
|
get info about total mem, used mem and available mem using:
|
|
@@ -798,9 +1098,7 @@ def mem_info():
|
|
|
798
1098
|
process = subprocess.run(["free", "-m"], stdout=subprocess.PIPE)
|
|
799
1099
|
# out, err = process.communicate()
|
|
800
1100
|
out = process.stdout
|
|
801
|
-
_, tot_mem, used_mem, _, _, _, available_mem = [
|
|
802
|
-
x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""
|
|
803
|
-
]
|
|
1101
|
+
_, tot_mem, used_mem, _, _, _, available_mem = [x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""]
|
|
804
1102
|
return False, {
|
|
805
1103
|
"total_memory": int(tot_mem),
|
|
806
1104
|
"used_memory": int(used_mem),
|
|
@@ -819,11 +1117,8 @@ def mem_info():
|
|
|
819
1117
|
return True, {"msg": error_info(sys.exc_info())[0]}
|
|
820
1118
|
|
|
821
1119
|
if sys.platform.startswith("win"):
|
|
822
|
-
|
|
823
1120
|
try:
|
|
824
|
-
output = subprocess.run(
|
|
825
|
-
["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE
|
|
826
|
-
)
|
|
1121
|
+
output = subprocess.run(["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
|
|
827
1122
|
tot_mem = int(output.stdout.strip().split(b"=")[-1].decode("utf-8")) / 1024 / 1024
|
|
828
1123
|
|
|
829
1124
|
output = subprocess.run(["wmic", "OS", "get", "FreePhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
|
|
@@ -836,7 +1131,7 @@ def mem_info():
|
|
|
836
1131
|
return True, {"msg": "Unknown operating system"}
|
|
837
1132
|
|
|
838
1133
|
|
|
839
|
-
def polygon_area(poly):
|
|
1134
|
+
def polygon_area(poly: list) -> float:
|
|
840
1135
|
"""
|
|
841
1136
|
area of polygon
|
|
842
1137
|
from http://www.mathopenref.com/coordpolygonarea.html
|
|
@@ -851,7 +1146,20 @@ def polygon_area(poly):
|
|
|
851
1146
|
return abs(tot / 2)
|
|
852
1147
|
|
|
853
1148
|
|
|
854
|
-
def
|
|
1149
|
+
def polyline_length(poly: list) -> float:
|
|
1150
|
+
"""
|
|
1151
|
+
length of polyline
|
|
1152
|
+
"""
|
|
1153
|
+
tot = 0
|
|
1154
|
+
for p in range(1, len(poly)):
|
|
1155
|
+
x1, y1 = poly[p - 1]
|
|
1156
|
+
x2, y2 = poly[p]
|
|
1157
|
+
tot += ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
|
|
1158
|
+
|
|
1159
|
+
return tot
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def url2path(url: str) -> str:
|
|
855
1163
|
"""
|
|
856
1164
|
convert URL in local path name
|
|
857
1165
|
under windows, check if path name begin with /
|
|
@@ -882,14 +1190,20 @@ def time2seconds(time_: str) -> dec:
|
|
|
882
1190
|
Decimal: time in seconds
|
|
883
1191
|
"""
|
|
884
1192
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1193
|
+
if " " in time_:
|
|
1194
|
+
try:
|
|
1195
|
+
return dec(str(dt.datetime.strptime(time_, "%Y-%m-%d %H:%M:%S.%f").timestamp()))
|
|
1196
|
+
except Exception:
|
|
1197
|
+
return dec("0.000")
|
|
1198
|
+
else:
|
|
1199
|
+
try:
|
|
1200
|
+
flag_neg = "-" in time_
|
|
1201
|
+
time_ = time_.replace("-", "")
|
|
1202
|
+
tsplit = time_.split(":")
|
|
1203
|
+
h, m, s = int(tsplit[0]), int(tsplit[1]), dec(tsplit[2])
|
|
1204
|
+
return dec(-(h * 3600 + m * 60 + s)) if flag_neg else dec(h * 3600 + m * 60 + s)
|
|
1205
|
+
except Exception:
|
|
1206
|
+
return dec("0.000")
|
|
893
1207
|
|
|
894
1208
|
|
|
895
1209
|
def seconds2time(sec: dec) -> str:
|
|
@@ -905,8 +1219,9 @@ def seconds2time(sec: dec) -> str:
|
|
|
905
1219
|
if math.isnan(sec):
|
|
906
1220
|
return cfg.NA
|
|
907
1221
|
|
|
908
|
-
if sec >
|
|
909
|
-
|
|
1222
|
+
# if sec > one day treat as date
|
|
1223
|
+
if sec > cfg.DATE_CUTOFF:
|
|
1224
|
+
t = dt.datetime.fromtimestamp(float(sec))
|
|
910
1225
|
return f"{t:%Y-%m-%d %H:%M:%S}.{t.microsecond / 1000:03.0f}"
|
|
911
1226
|
|
|
912
1227
|
neg_sign = "-" * (sec < 0)
|
|
@@ -965,7 +1280,7 @@ def eol2space(s: str) -> str:
|
|
|
965
1280
|
return s.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
|
|
966
1281
|
|
|
967
1282
|
|
|
968
|
-
def test_ffmpeg_path(FFmpegPath):
|
|
1283
|
+
def test_ffmpeg_path(FFmpegPath: str) -> Tuple[bool, str]:
|
|
969
1284
|
"""
|
|
970
1285
|
test if ffmpeg has valid path
|
|
971
1286
|
|
|
@@ -977,22 +1292,20 @@ def test_ffmpeg_path(FFmpegPath):
|
|
|
977
1292
|
str: message
|
|
978
1293
|
"""
|
|
979
1294
|
|
|
980
|
-
out, error = subprocess.Popen(
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
logging.debug(f"test ffmpeg path output: {out}")
|
|
984
|
-
logging.debug(f"test ffmpeg path error: {error}")
|
|
1295
|
+
out, error = subprocess.Popen(f'"{FFmpegPath}" -version', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
|
|
1296
|
+
logger.debug(f"test ffmpeg path output: {out}")
|
|
1297
|
+
logger.debug(f"test ffmpeg path error: {error}")
|
|
985
1298
|
|
|
986
1299
|
if (b"avconv" in out) or (b"the Libav developers" in error):
|
|
987
1300
|
return False, "Please use FFmpeg from https://www.ffmpeg.org in place of FFmpeg from Libav project."
|
|
988
1301
|
|
|
989
1302
|
if (b"ffmpeg version" not in out) and (b"ffmpeg version" not in error):
|
|
990
|
-
return False, "FFmpeg is required but it was not found
|
|
1303
|
+
return False, "FFmpeg is required but it was not found.<br>See https://www.ffmpeg.org"
|
|
991
1304
|
|
|
992
1305
|
return True, ""
|
|
993
1306
|
|
|
994
1307
|
|
|
995
|
-
def check_ffmpeg_path():
|
|
1308
|
+
def check_ffmpeg_path() -> Tuple[bool, str]:
|
|
996
1309
|
"""
|
|
997
1310
|
check for ffmpeg path
|
|
998
1311
|
firstly search for embedded version
|
|
@@ -1003,45 +1316,24 @@ def check_ffmpeg_path():
|
|
|
1003
1316
|
str: if bool True returns ffmpegpath else returns error message
|
|
1004
1317
|
"""
|
|
1005
1318
|
|
|
1319
|
+
# search embedded ffmpeg
|
|
1006
1320
|
if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
|
|
1321
|
+
ffmpeg_executable = Path("ffmpeg")
|
|
1322
|
+
elif sys.platform.startswith("win"):
|
|
1323
|
+
ffmpeg_executable = Path("ffmpeg.exe")
|
|
1007
1324
|
|
|
1008
|
-
|
|
1009
|
-
# search embedded ffmpeg
|
|
1010
|
-
if sys.argv[0].endswith("start_boris.py"):
|
|
1011
|
-
ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "boris" / "misc" / "ffmpeg"
|
|
1012
|
-
if sys.argv[0].endswith("__main__.py"):
|
|
1013
|
-
ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "misc" / "ffmpeg"
|
|
1014
|
-
|
|
1015
|
-
if not ffmpeg_path.is_file():
|
|
1016
|
-
# search global ffmpeg
|
|
1017
|
-
ffmpeg_path = "ffmpeg"
|
|
1018
|
-
|
|
1019
|
-
# test ffmpeg
|
|
1020
|
-
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1021
|
-
if r:
|
|
1022
|
-
return True, str(ffmpeg_path)
|
|
1023
|
-
else:
|
|
1024
|
-
return False, "FFmpeg is not available"
|
|
1325
|
+
ffmpeg_path = Path(__file__).parent / "misc" / ffmpeg_executable
|
|
1025
1326
|
|
|
1026
|
-
if
|
|
1327
|
+
if not ffmpeg_path.is_file():
|
|
1328
|
+
# search global ffmpeg
|
|
1329
|
+
ffmpeg_path = ffmpeg_executable
|
|
1027
1330
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
if not ffmpeg_path.is_file():
|
|
1036
|
-
# search global ffmpeg
|
|
1037
|
-
ffmpeg_path = "ffmpeg"
|
|
1038
|
-
|
|
1039
|
-
# test ffmpeg
|
|
1040
|
-
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1041
|
-
if r:
|
|
1042
|
-
return True, str(ffmpeg_path)
|
|
1043
|
-
else:
|
|
1044
|
-
return False, "FFmpeg is not available"
|
|
1331
|
+
# test ffmpeg
|
|
1332
|
+
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1333
|
+
if r:
|
|
1334
|
+
return True, str(ffmpeg_path)
|
|
1335
|
+
else:
|
|
1336
|
+
return False, "FFmpeg is not available"
|
|
1045
1337
|
|
|
1046
1338
|
|
|
1047
1339
|
def smart_size_format(n: Union[float, int, str, None]) -> str:
|
|
@@ -1063,6 +1355,73 @@ def smart_size_format(n: Union[float, int, str, None]) -> str:
|
|
|
1063
1355
|
return f"{n / 1_000_000_000:,.1f} Gb"
|
|
1064
1356
|
|
|
1065
1357
|
|
|
1358
|
+
def get_systeminfo() -> str:
|
|
1359
|
+
"""
|
|
1360
|
+
returns info about the system
|
|
1361
|
+
"""
|
|
1362
|
+
|
|
1363
|
+
mpv_lib_version_, mpv_lib_file_path, mpv_api_version = mpv_lib_version()
|
|
1364
|
+
|
|
1365
|
+
system_info = (
|
|
1366
|
+
f"BORIS version: {version.__version__}\n"
|
|
1367
|
+
f"OS: {platform.uname().system} {platform.uname().release} {platform.uname().version}\n"
|
|
1368
|
+
f"CPU: {platform.uname().machine} {platform.uname().processor}\n"
|
|
1369
|
+
f"Python {platform.python_version()} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})\n"
|
|
1370
|
+
f"Qt {qVersion()} - PySide {pyside6_version}\n"
|
|
1371
|
+
f"MPV library version: {mpv_lib_version_}\n"
|
|
1372
|
+
f"MPV API version: {mpv_api_version}\n"
|
|
1373
|
+
f"MPV library file path: {mpv_lib_file_path}\n\n"
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
r, memory = mem_info()
|
|
1377
|
+
if not r:
|
|
1378
|
+
system_info += (
|
|
1379
|
+
f"Memory (RAM) Total: {memory.get('total_memory', 'Not available'):.2f} Mb "
|
|
1380
|
+
f"Free: {memory.get('free_memory', 'Not available'):.2f} Mb\n\n"
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
return system_info
|
|
1384
|
+
|
|
1385
|
+
"""
|
|
1386
|
+
# system info
|
|
1387
|
+
systeminfo = ""
|
|
1388
|
+
if sys.platform.startswith("win"):
|
|
1389
|
+
# systeminfo = subprocess.getoutput("systeminfo")
|
|
1390
|
+
systeminfo = subprocess.run("systeminfo /FO csv /NH", capture_output=True, text=True, encoding="mbcs", shell=True).stdout
|
|
1391
|
+
|
|
1392
|
+
import csv
|
|
1393
|
+
from io import StringIO
|
|
1394
|
+
|
|
1395
|
+
# Parse it as CSV
|
|
1396
|
+
f = StringIO(systeminfo)
|
|
1397
|
+
reader = csv.reader(f)
|
|
1398
|
+
parsed_data = list(reader)[0]
|
|
1399
|
+
# Print specific fields by index
|
|
1400
|
+
info_to_show = ""
|
|
1401
|
+
info_to_show += f"Computer Name: {parsed_data[0]}\n"
|
|
1402
|
+
info_to_show += f"OS Name: {parsed_data[1]}\n"
|
|
1403
|
+
info_to_show += f"OS Version: {parsed_data[2]}\n"
|
|
1404
|
+
info_to_show += f"System Manufacturer: {parsed_data[11]}\n"
|
|
1405
|
+
info_to_show += f"System Model: {parsed_data[12]}\n"
|
|
1406
|
+
info_to_show += f"Processor: {parsed_data[14]}\n"
|
|
1407
|
+
info_to_show += f"Locale: {parsed_data[19]}\n"
|
|
1408
|
+
info_to_show += f"Installed Memory: {parsed_data[22]}\n"
|
|
1409
|
+
|
|
1410
|
+
# info about graphic card
|
|
1411
|
+
graphic_info = subprocess.run(
|
|
1412
|
+
"wmic path win32_videocontroller get name", capture_output=True, text=True, encoding="mbcs", shell=True
|
|
1413
|
+
).stdout
|
|
1414
|
+
info_to_show += graphic_info.replace("\n", "").replace("Name", "Graphic card model")
|
|
1415
|
+
|
|
1416
|
+
systeminfo = info_to_show
|
|
1417
|
+
|
|
1418
|
+
if sys.platform.startswith("linux"):
|
|
1419
|
+
systeminfo = subprocess.getoutput("cat /etc/*rel*; uname -a")
|
|
1420
|
+
|
|
1421
|
+
return systeminfo
|
|
1422
|
+
"""
|
|
1423
|
+
|
|
1424
|
+
|
|
1066
1425
|
def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
1067
1426
|
"""
|
|
1068
1427
|
analyse video parameters with ffprobe (if available)
|
|
@@ -1074,28 +1433,32 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1074
1433
|
Returns:
|
|
1075
1434
|
dict
|
|
1076
1435
|
"""
|
|
1077
|
-
|
|
1436
|
+
|
|
1437
|
+
# check ffprobe executable in same place than ffmpeg
|
|
1078
1438
|
ffprobe_bin = ffmpeg_bin.replace("ffmpeg", "ffprobe")
|
|
1439
|
+
if not Path(ffprobe_bin).is_file():
|
|
1440
|
+
if which(ffprobe_bin) is None:
|
|
1441
|
+
return {"error": "ffprobe not found"}
|
|
1079
1442
|
|
|
1080
1443
|
command = f'"{ffprobe_bin}" -hide_banner -v error -print_format json -show_format -show_streams "{file_name}"'
|
|
1081
1444
|
|
|
1082
|
-
# print(command)
|
|
1083
|
-
|
|
1084
1445
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
1085
1446
|
out, error = p.communicate()
|
|
1086
1447
|
if error:
|
|
1087
|
-
|
|
1448
|
+
if b"invalid data" in error:
|
|
1449
|
+
return {"error": f"{error}"}
|
|
1088
1450
|
|
|
1089
1451
|
try:
|
|
1090
1452
|
hasVideo = False
|
|
1091
1453
|
hasAudio = False
|
|
1092
|
-
bitrate = None
|
|
1454
|
+
"""bitrate = None"""
|
|
1093
1455
|
video_bitrate = None
|
|
1094
1456
|
audio_bitrate = []
|
|
1095
1457
|
resolution = None
|
|
1096
|
-
fps = 0
|
|
1458
|
+
fps: float = 0
|
|
1097
1459
|
sample_rate = None
|
|
1098
|
-
duration =
|
|
1460
|
+
duration = None
|
|
1461
|
+
audio_duration = cfg.NA
|
|
1099
1462
|
frames_number = None
|
|
1100
1463
|
size = None
|
|
1101
1464
|
audio_codec = None
|
|
@@ -1106,7 +1469,6 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1106
1469
|
size = int(video_param["format"]["size"])
|
|
1107
1470
|
|
|
1108
1471
|
for stream in video_param["streams"]:
|
|
1109
|
-
|
|
1110
1472
|
if stream["codec_type"] == "video":
|
|
1111
1473
|
hasVideo = True
|
|
1112
1474
|
video_bitrate = int(stream["bit_rate"]) if "bit_rate" in stream else None
|
|
@@ -1130,9 +1492,16 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1130
1492
|
fps = eval(stream["r_frame_rate"])
|
|
1131
1493
|
except Exception:
|
|
1132
1494
|
fps = 0
|
|
1495
|
+
if fps >= 1000 and "avg_frame_rate" in stream: # case for some h265 video ("r_frame_rate": "1200000/1")
|
|
1496
|
+
try:
|
|
1497
|
+
fps = eval(stream["avg_frame_rate"])
|
|
1498
|
+
except Exception:
|
|
1499
|
+
pass
|
|
1133
1500
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1501
|
+
fps = round(fps, 3)
|
|
1502
|
+
|
|
1503
|
+
if "duration" in stream:
|
|
1504
|
+
duration = float(stream["duration"])
|
|
1136
1505
|
if "duration_ts" in stream:
|
|
1137
1506
|
frames_number = int(stream["duration_ts"])
|
|
1138
1507
|
elif "nb_frames" in stream:
|
|
@@ -1144,11 +1513,19 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1144
1513
|
|
|
1145
1514
|
if stream["codec_type"] == "audio":
|
|
1146
1515
|
hasAudio = True
|
|
1147
|
-
sample_rate = float(stream["sample_rate"])
|
|
1148
|
-
|
|
1516
|
+
sample_rate = float(stream["sample_rate"]) if "sample_rate" in stream else cfg.NA
|
|
1517
|
+
# TODO manage audio_duration parameter
|
|
1518
|
+
audio_duration = float(stream["duration"]) if "duration" in stream else cfg.NA
|
|
1149
1519
|
audio_codec = stream["codec_long_name"]
|
|
1150
1520
|
audio_bitrate.append(int(stream.get("bit_rate", 0)))
|
|
1151
1521
|
|
|
1522
|
+
# check duration
|
|
1523
|
+
if duration is None:
|
|
1524
|
+
if "duration" in video_param["format"]:
|
|
1525
|
+
duration = float(video_param["format"]["duration"])
|
|
1526
|
+
else:
|
|
1527
|
+
duration = 0
|
|
1528
|
+
|
|
1152
1529
|
# check bit rate
|
|
1153
1530
|
if "bit_rate" in video_param["format"]:
|
|
1154
1531
|
all_bitrate = int(video_param["format"]["bit_rate"])
|
|
@@ -1158,11 +1535,22 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1158
1535
|
if video_bitrate is None and all_bitrate is not None:
|
|
1159
1536
|
video_bitrate = all_bitrate - sum(audio_bitrate)
|
|
1160
1537
|
|
|
1538
|
+
# extract format long name
|
|
1539
|
+
format_long_name = video_param["format"]["format_long_name"] if "format_long_name" in video_param["format"] else cfg.NA
|
|
1540
|
+
|
|
1541
|
+
# extract creation time ("creation_time": "2023-03-22T16:50:32.000000Z")
|
|
1542
|
+
creation_time = cfg.NA
|
|
1543
|
+
if "tags" in video_param["format"] and "creation_time" in video_param["format"]["tags"]:
|
|
1544
|
+
creation_time = video_param["format"]["tags"]["creation_time"].replace("T", " ")
|
|
1545
|
+
if "." in creation_time:
|
|
1546
|
+
creation_time = creation_time.split(".")[0]
|
|
1547
|
+
|
|
1161
1548
|
return {
|
|
1162
1549
|
"analysis_program": "ffprobe",
|
|
1163
1550
|
"frames_number": frames_number,
|
|
1164
1551
|
"duration_ms": duration * 1000,
|
|
1165
1552
|
"duration": duration,
|
|
1553
|
+
"audio_duration": audio_duration,
|
|
1166
1554
|
"fps": fps,
|
|
1167
1555
|
"has_video": hasVideo,
|
|
1168
1556
|
"has_audio": hasAudio,
|
|
@@ -1172,6 +1560,8 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1172
1560
|
"file size": size,
|
|
1173
1561
|
"audio_codec": audio_codec,
|
|
1174
1562
|
"video_codec": video_codec,
|
|
1563
|
+
"creation_time": creation_time,
|
|
1564
|
+
"format_long_name": format_long_name,
|
|
1175
1565
|
}
|
|
1176
1566
|
|
|
1177
1567
|
except Exception as e:
|
|
@@ -1180,7 +1570,7 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1180
1570
|
|
|
1181
1571
|
def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
1182
1572
|
"""
|
|
1183
|
-
analyse frame rate and video duration with ffmpeg
|
|
1573
|
+
analyse frame rate and video duration with ffprobe or ffmpeg if ffprobe not available
|
|
1184
1574
|
Returns parameters: duration, duration_ms, bitrate, frames_number, fps, has_video (True/False), has_audio (True/False)
|
|
1185
1575
|
|
|
1186
1576
|
Args:
|
|
@@ -1194,13 +1584,14 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1194
1584
|
|
|
1195
1585
|
ffprobe_results = ffprobe_media_analysis(ffmpeg_bin, file_name)
|
|
1196
1586
|
|
|
1587
|
+
logger.debug(f"file_name: {file_name}")
|
|
1588
|
+
logger.debug(f"ffprobe_results: {ffprobe_results}")
|
|
1589
|
+
|
|
1197
1590
|
if ("error" not in ffprobe_results) and (ffprobe_results["bitrate"] is not None):
|
|
1198
1591
|
return ffprobe_results
|
|
1199
1592
|
else:
|
|
1200
1593
|
# use ffmpeg
|
|
1201
|
-
command = (
|
|
1202
|
-
f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
|
|
1203
|
-
)
|
|
1594
|
+
command = f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
|
|
1204
1595
|
|
|
1205
1596
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
1206
1597
|
|
|
@@ -1258,7 +1649,7 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1258
1649
|
if b"Stream #" in row and b"Video:" in row:
|
|
1259
1650
|
hasVideo = True
|
|
1260
1651
|
# get resolution \d{3,5}x\d{3,5}
|
|
1261
|
-
re_results = re.search(
|
|
1652
|
+
re_results = re.search(r"\d{3,5}x\d{3,5}", row, re.IGNORECASE)
|
|
1262
1653
|
if re_results:
|
|
1263
1654
|
resolution = re_results.group(0).decode("utf-8")
|
|
1264
1655
|
break
|
|
@@ -1283,11 +1674,13 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1283
1674
|
"frames_number": int(fps * duration),
|
|
1284
1675
|
"duration_ms": duration * 1000,
|
|
1285
1676
|
"duration": duration,
|
|
1677
|
+
"audio_duration": cfg.NA,
|
|
1286
1678
|
"fps": fps,
|
|
1287
1679
|
"has_video": hasVideo,
|
|
1288
1680
|
"has_audio": hasAudio,
|
|
1289
1681
|
"bitrate": bitrate,
|
|
1290
1682
|
"resolution": resolution,
|
|
1683
|
+
"format_long_name": "",
|
|
1291
1684
|
}
|
|
1292
1685
|
|
|
1293
1686
|
|
|
@@ -1308,7 +1701,7 @@ def behavior_color(colors_list: list, idx: int, default_color: str = "darkgray")
|
|
|
1308
1701
|
"""
|
|
1309
1702
|
|
|
1310
1703
|
try:
|
|
1311
|
-
return colors_list[idx % len(colors_list)]
|
|
1704
|
+
return colors_list[idx % len(colors_list)].replace("tab:", "")
|
|
1312
1705
|
except Exception:
|
|
1313
1706
|
return default_color
|
|
1314
1707
|
|
|
@@ -1328,12 +1721,37 @@ def all_behaviors(ethogram: dict) -> list:
|
|
|
1328
1721
|
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in sorted_keys(ethogram)]
|
|
1329
1722
|
|
|
1330
1723
|
|
|
1724
|
+
def all_subjects(subjects: dict) -> list:
|
|
1725
|
+
"""
|
|
1726
|
+
extract all subjects from the subject configuration dictionary
|
|
1727
|
+
|
|
1728
|
+
Args:
|
|
1729
|
+
subject configuration (dict)
|
|
1730
|
+
|
|
1731
|
+
Returns:
|
|
1732
|
+
list: subjects name
|
|
1733
|
+
"""
|
|
1734
|
+
|
|
1735
|
+
return [subjects[x][cfg.SUBJECT_NAME] for x in sorted_keys(subjects)]
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def has_coding_map(ethogram: dict, behavior_idx: str) -> bool:
|
|
1739
|
+
"""
|
|
1740
|
+
check if behavior index has a coding map
|
|
1741
|
+
"""
|
|
1742
|
+
if not ethogram.get(behavior_idx, False):
|
|
1743
|
+
return False
|
|
1744
|
+
if not ethogram[behavior_idx].get("coding map", False):
|
|
1745
|
+
return False
|
|
1746
|
+
return True
|
|
1747
|
+
|
|
1748
|
+
|
|
1331
1749
|
def dir_images_number(dir_path_str: str) -> dict:
|
|
1332
1750
|
"""
|
|
1333
1751
|
return number of images in dir_path (see cfg.IMAGE_EXTENSIONS)
|
|
1334
1752
|
"""
|
|
1335
1753
|
|
|
1336
|
-
dir_path =
|
|
1754
|
+
dir_path = Path(dir_path_str)
|
|
1337
1755
|
if not dir_path.is_dir():
|
|
1338
1756
|
return {"error": f"The directory {dir_path_str} does not exists"}
|
|
1339
1757
|
img_count = 0
|
|
@@ -1371,29 +1789,11 @@ def intersection(A, B, C, D):
|
|
|
1371
1789
|
ym = slope * xm + intersept
|
|
1372
1790
|
else:
|
|
1373
1791
|
xm = (
|
|
1374
|
-
(
|
|
1375
|
-
xd * xa * yc
|
|
1376
|
-
- xd * xb * yc
|
|
1377
|
-
- xd * xa * yb
|
|
1378
|
-
- xc * xa * yd
|
|
1379
|
-
+ xc * xa * yb
|
|
1380
|
-
+ xd * ya * xb
|
|
1381
|
-
+ xc * xb * yd
|
|
1382
|
-
- xc * ya * xb
|
|
1383
|
-
)
|
|
1792
|
+
(xd * xa * yc - xd * xb * yc - xd * xa * yb - xc * xa * yd + xc * xa * yb + xd * ya * xb + xc * xb * yd - xc * ya * xb)
|
|
1384
1793
|
/ (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
|
|
1385
1794
|
).quantize(dec(".001"), rounding=ROUND_DOWN)
|
|
1386
1795
|
ym = (
|
|
1387
|
-
(
|
|
1388
|
-
yb * xc * yd
|
|
1389
|
-
- yb * yc * xd
|
|
1390
|
-
- ya * xc * yd
|
|
1391
|
-
+ ya * yc * xd
|
|
1392
|
-
- xa * yb * yd
|
|
1393
|
-
+ xa * yb * yc
|
|
1394
|
-
+ ya * xb * yd
|
|
1395
|
-
- ya * xb * yc
|
|
1396
|
-
)
|
|
1796
|
+
(yb * xc * yd - yb * yc * xd - ya * xc * yd + ya * yc * xd - xa * yb * yd + xa * yb * yc + ya * xb * yd - ya * xb * yc)
|
|
1397
1797
|
/ (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
|
|
1398
1798
|
).quantize(dec(".001"), rounding=ROUND_DOWN)
|
|
1399
1799
|
|
|
@@ -1402,16 +1802,7 @@ def intersection(A, B, C, D):
|
|
|
1402
1802
|
ymin1, ymax1 = min(ya, yb), max(ya, yb)
|
|
1403
1803
|
ymin2, ymax2 = min(yc, yd), max(yc, yd)
|
|
1404
1804
|
|
|
1405
|
-
return
|
|
1406
|
-
xm >= xmin1
|
|
1407
|
-
and xm <= xmax1
|
|
1408
|
-
and xm >= xmin2
|
|
1409
|
-
and xm <= xmax2
|
|
1410
|
-
and ym >= ymin1
|
|
1411
|
-
and ym <= ymax1
|
|
1412
|
-
and ym >= ymin2
|
|
1413
|
-
and ym <= ymax2
|
|
1414
|
-
)
|
|
1805
|
+
return xm >= xmin1 and xm <= xmax1 and xm >= xmin2 and xm <= xmax2 and ym >= ymin1 and ym <= ymax1 and ym >= ymin2 and ym <= ymax2
|
|
1415
1806
|
|
|
1416
1807
|
except Exception: # for cases xa=xb=xc=xd
|
|
1417
1808
|
return True
|