boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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 +28 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +266 -144
- boris/advanced_event_filtering.py +23 -29
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_export_to_feral.py +225 -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 +235 -236
- 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 +19 -36
- boris/config.py +109 -50
- boris/config_file.py +58 -67
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2174 -1303
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +27 -7
- boris/dialog.py +461 -242
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +405 -281
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +180 -203
- boris/export_observation.py +60 -73
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +427 -218
- 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 +16 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv2.py +128 -35
- boris/observation.py +501 -211
- boris/observation_operations.py +1037 -393
- boris/observation_ui.py +573 -363
- boris/observations_list.py +51 -58
- boris/otx_parser.py +74 -68
- boris/param_panel.py +45 -59
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +91 -56
- boris/plot_data_module.py +20 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +83 -56
- boris/plot_waveform_rt.py +27 -49
- boris/plugins.py +468 -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 +307 -123
- boris/preferences_ui.py +686 -227
- boris/project.py +294 -271
- boris/project_functions.py +626 -537
- boris/project_import_export.py +204 -213
- boris/project_ui.py +673 -441
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +62 -90
- boris/select_observations.py +19 -197
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +51 -33
- boris/subjects_pad.py +7 -9
- boris/synthetic_time_budget.py +42 -26
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +77 -89
- boris/transitions.py +41 -41
- boris/utilities.py +594 -226
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +86 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +240 -136
- boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.12.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 -37
- boris/core.ui +0 -1571
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -982
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1074
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
- boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.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,31 +19,252 @@ Copyright 2012-2023 Olivier Friard
|
|
|
19
19
|
MA 02110-1301, USA.
|
|
20
20
|
"""
|
|
21
21
|
|
|
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
|
|
22
28
|
import csv
|
|
23
29
|
import datetime
|
|
24
|
-
import
|
|
30
|
+
import datetime as dt
|
|
31
|
+
import exifread
|
|
25
32
|
import json
|
|
26
33
|
import logging
|
|
27
34
|
import math
|
|
35
|
+
import numpy as np
|
|
28
36
|
import os
|
|
29
|
-
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from PIL.ImageQt import Image
|
|
39
|
+
import platform
|
|
30
40
|
import re
|
|
31
|
-
import
|
|
41
|
+
import shutil
|
|
32
42
|
import subprocess
|
|
33
43
|
import sys
|
|
34
44
|
import urllib.parse
|
|
45
|
+
import urllib.request
|
|
35
46
|
import wave
|
|
36
|
-
|
|
37
|
-
from decimal import getcontext, ROUND_DOWN
|
|
38
|
-
from shutil import copyfile
|
|
39
|
-
from typing import Union, Tuple
|
|
47
|
+
import socket
|
|
40
48
|
|
|
41
|
-
import
|
|
42
|
-
from
|
|
43
|
-
|
|
44
|
-
from PIL.ImageQt import Image
|
|
49
|
+
from PySide6 import __version__ as pyside6_version
|
|
50
|
+
from PySide6.QtGui import QPixmap, QImage
|
|
51
|
+
from PySide6.QtCore import qVersion
|
|
45
52
|
|
|
46
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 is_subdir(a: Path, b: Path) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Return True if directory A is inside directory B.
|
|
132
|
+
"""
|
|
133
|
+
a = a.resolve()
|
|
134
|
+
b = b.resolve()
|
|
135
|
+
return a.is_relative_to(b)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_mpv_ipc(socket_path: str = cfg.MPV_SOCKET) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
test if socket available
|
|
141
|
+
"""
|
|
142
|
+
if not os.path.exists(socket_path):
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
147
|
+
client.connect(socket_path)
|
|
148
|
+
client.close()
|
|
149
|
+
return True
|
|
150
|
+
except Exception:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def extract_exif_DateTimeOriginal(file_path: str) -> int:
|
|
155
|
+
"""
|
|
156
|
+
extract the EXIF DateTimeOriginal tag
|
|
157
|
+
return epoch time
|
|
158
|
+
if the tag is not available return -1
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
file_path (str): path of the media file
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
int: timestamp
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
with open(file_path, "rb") as f_in:
|
|
169
|
+
tags = exifread.process_file(f_in, details=False, stop_tag="EXIF DateTimeOriginal")
|
|
170
|
+
if "EXIF DateTimeOriginal" in tags:
|
|
171
|
+
date_time_original = (
|
|
172
|
+
f"{tags['EXIF DateTimeOriginal'].values[:4]}-"
|
|
173
|
+
f"{tags['EXIF DateTimeOriginal'].values[5:7]}-"
|
|
174
|
+
f"{tags['EXIF DateTimeOriginal'].values[8:10]} "
|
|
175
|
+
f"{tags['EXIF DateTimeOriginal'].values.split(' ')[-1]}"
|
|
176
|
+
)
|
|
177
|
+
return int(datetime.datetime.strptime(date_time_original, "%Y-%m-%d %H:%M:%S").timestamp())
|
|
178
|
+
else:
|
|
179
|
+
try:
|
|
180
|
+
# read from file name (YYYY-MM-DD_HHMMSS)
|
|
181
|
+
return int(datetime.datetime.strptime(Path(file_path).stem, "%Y-%m-%d_%H%M%S").timestamp())
|
|
182
|
+
except Exception:
|
|
183
|
+
# read from file name (YYYY-MM-DD_HH:MM:SS)
|
|
184
|
+
return int(datetime.datetime.strptime(Path(file_path).stem, "%Y-%m-%d_%H:%M:%S").timestamp())
|
|
185
|
+
|
|
186
|
+
except Exception:
|
|
187
|
+
return -1
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def extract_video_creation_date(file_path: str) -> int | None:
|
|
191
|
+
"""
|
|
192
|
+
returns the timestamp of the media creation date time with Hachoir
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
logger.debug(f"extract_video_creation_date for {file_path}")
|
|
196
|
+
|
|
197
|
+
if not Path(file_path).is_file():
|
|
198
|
+
logger.debug(f"{file_path} not found")
|
|
199
|
+
return None
|
|
200
|
+
try:
|
|
201
|
+
parser = createParser(file_path)
|
|
202
|
+
metadata = extractMetadata(parser)
|
|
203
|
+
except Exception:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
if metadata.has("creation_date"):
|
|
207
|
+
if metadata.get("creation_date") == datetime.datetime(1904, 1, 1, 0, 0):
|
|
208
|
+
return None
|
|
209
|
+
return metadata.get("creation_date").timestamp()
|
|
210
|
+
else:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def extract_date_time_from_file_name(file_path: str) -> int | None:
|
|
215
|
+
"""
|
|
216
|
+
extract YYYY-MM-DD_HHMMSS or YYYY-MM-DD_HH:MM:SS from file name
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
patterns = (r"\d{4}-\d{2}-\d{2}_\d{6}", r"\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}")
|
|
220
|
+
for pattern in patterns:
|
|
221
|
+
matches = re.findall(pattern, file_path)
|
|
222
|
+
|
|
223
|
+
if matches:
|
|
224
|
+
if pattern == r"\d{4}-\d{2}-\d{2}_\d{6}":
|
|
225
|
+
logger.debug(
|
|
226
|
+
f"extract_date_time_from_file_name timestamp from {file_path}: {int(datetime.datetime.strptime(matches[0], '%Y-%m-%d_%H%M%S').timestamp())}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return int(datetime.datetime.strptime(matches[0], "%Y-%m-%d_%H%M%S").timestamp())
|
|
230
|
+
|
|
231
|
+
if pattern == r"\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}":
|
|
232
|
+
logger.debug(
|
|
233
|
+
f"extract_date_time_from_file_name timestamp from {file_path}: {int(datetime.datetime.strptime(matches[0], '%Y-%m-%d_%H:%M:%S').timestamp())}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return int(datetime.datetime.strptime(matches[0], "%Y-%m-%d_%H:%M:%S").timestamp())
|
|
237
|
+
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def mpv_lib_version() -> Tuple[str, str, str]:
|
|
242
|
+
"""
|
|
243
|
+
Version of MPV library
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
str: MPV library version
|
|
247
|
+
"""
|
|
248
|
+
if ("-i" in sys.argv) or ("--ipc" in sys.argv) or sys.platform.startswith("darwin"):
|
|
249
|
+
return "MPV IPC mode", "", ""
|
|
250
|
+
|
|
251
|
+
mpv_lib_file = None
|
|
252
|
+
if sys.platform.startswith("linux"):
|
|
253
|
+
mpv_lib_file = mpv.sofile
|
|
254
|
+
if sys.platform.startswith("win"):
|
|
255
|
+
mpv_lib_file = mpv.dll
|
|
256
|
+
|
|
257
|
+
return (".".join([str(x) for x in mpv._mpv_client_api_version()]), mpv_lib_file, mpv.MPV_VERSION)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def python_mpv_script_version() -> str:
|
|
261
|
+
"""
|
|
262
|
+
version of python-mpv script
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
return mpv.__version__
|
|
266
|
+
except Exception:
|
|
267
|
+
return "Not found"
|
|
47
268
|
|
|
48
269
|
|
|
49
270
|
def error_info(exc_info: tuple) -> tuple:
|
|
@@ -154,28 +375,7 @@ def return_file_header_footer(file_name: str, file_row_number: int = 0, row_numb
|
|
|
154
375
|
return header, footer
|
|
155
376
|
|
|
156
377
|
|
|
157
|
-
def
|
|
158
|
-
"""
|
|
159
|
-
Translate bytes to string.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
b (bytes): byte to convert
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
str: converted byte
|
|
166
|
-
"""
|
|
167
|
-
|
|
168
|
-
if isinstance(b, bytes):
|
|
169
|
-
fileSystemEncoding = sys.getfilesystemencoding()
|
|
170
|
-
# hack for PyInstaller
|
|
171
|
-
if fileSystemEncoding is None:
|
|
172
|
-
fileSystemEncoding = "UTF-8"
|
|
173
|
-
return b.decode(fileSystemEncoding)
|
|
174
|
-
else:
|
|
175
|
-
return b
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def convertTime(time_format: str, sec: Union[float, dec]) -> str:
|
|
378
|
+
def convertTime(time_format: str, sec: Union[float, dec]) -> Union[str, None]:
|
|
179
379
|
"""
|
|
180
380
|
convert time in base at the current format (S or HHMMSS)
|
|
181
381
|
|
|
@@ -198,10 +398,12 @@ def convertTime(time_format: str, sec: Union[float, dec]) -> str:
|
|
|
198
398
|
return None
|
|
199
399
|
|
|
200
400
|
|
|
201
|
-
def
|
|
401
|
+
def smart_time_format(sec: Union[float, dec], time_format: str = cfg.S, cutoff: dec = cfg.SMART_TIME_CUTOFF_DEFAULT) -> str:
|
|
202
402
|
"""
|
|
403
|
+
Smart time format
|
|
203
404
|
returns time in seconds if <= cutoff else in HH:MM:SS.ZZZ format
|
|
204
405
|
"""
|
|
406
|
+
# cutoff = 0 follows the time format selectd by user
|
|
205
407
|
if cutoff == 0:
|
|
206
408
|
return convertTime(time_format, sec)
|
|
207
409
|
if sec <= cutoff:
|
|
@@ -222,7 +424,10 @@ def convert_time_to_decimal(pj: dict) -> dict:
|
|
|
222
424
|
"""
|
|
223
425
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
224
426
|
if cfg.TIME_OFFSET in pj[cfg.OBSERVATIONS][obs_id]:
|
|
225
|
-
pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
|
|
427
|
+
if pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] is not None:
|
|
428
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec(str(pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]))
|
|
429
|
+
else:
|
|
430
|
+
pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec("0.000")
|
|
226
431
|
for idx, _ in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
227
432
|
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX] = dec(
|
|
228
433
|
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX]
|
|
@@ -238,20 +443,6 @@ def count_media_file(media_files: dict) -> int:
|
|
|
238
443
|
return sum([len(media_files[idx]) for idx in media_files])
|
|
239
444
|
|
|
240
445
|
|
|
241
|
-
def file_content_md5(file_name: str) -> str:
|
|
242
|
-
"""
|
|
243
|
-
returns the MD5 sum of file content
|
|
244
|
-
"""
|
|
245
|
-
hash_md5 = hashlib.md5()
|
|
246
|
-
try:
|
|
247
|
-
with open(file_name, "rb") as f:
|
|
248
|
-
for chunk in iter(lambda: f.read(4096), b""):
|
|
249
|
-
hash_md5.update(chunk)
|
|
250
|
-
return hash_md5.hexdigest()
|
|
251
|
-
except FileNotFoundError:
|
|
252
|
-
return ""
|
|
253
|
-
|
|
254
|
-
|
|
255
446
|
def txt2np_array(
|
|
256
447
|
file_name: str, columns_str: str, substract_first_value: str, converters=None, column_converter=None
|
|
257
448
|
) -> Tuple[bool, str, np.array]:
|
|
@@ -289,20 +480,37 @@ def txt2np_array(
|
|
|
289
480
|
conv_name = column_converter[column_idx]
|
|
290
481
|
|
|
291
482
|
function = f"""def {conv_name}(INPUT):\n"""
|
|
292
|
-
function += """ INPUT = INPUT.decode("utf-8") if isinstance(INPUT, bytes) else INPUT"""
|
|
483
|
+
function += """ INPUT = INPUT.decode("utf-8") if isinstance(INPUT, bytes) else INPUT\n\n"""
|
|
293
484
|
for line in converters[conv_name]["code"].split("\n"):
|
|
294
485
|
function += f" {line}\n"
|
|
295
486
|
function += """ return OUTPUT"""
|
|
296
487
|
|
|
488
|
+
print("=============")
|
|
489
|
+
print(function)
|
|
490
|
+
print("=============")
|
|
491
|
+
|
|
492
|
+
import types
|
|
493
|
+
|
|
494
|
+
mod = types.ModuleType("converter_module")
|
|
495
|
+
exec(function, mod.__dict__)
|
|
496
|
+
|
|
497
|
+
"""
|
|
297
498
|
try:
|
|
298
499
|
exec(function)
|
|
299
500
|
except Exception:
|
|
300
501
|
return False, f"error in converter: {sys.exc_info()[1]}", np.array([])
|
|
301
502
|
|
|
302
|
-
|
|
503
|
+
print(f"{converters=}")
|
|
504
|
+
print(f"{column_converter=}")
|
|
505
|
+
print(locals())
|
|
506
|
+
print(f"{conv_name=}")
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
# np_converters[column_idx - 1] = locals()['conv_name']
|
|
510
|
+
np_converters[column_idx - 1] = getattr(mod, conv_name)
|
|
303
511
|
|
|
304
512
|
else:
|
|
305
|
-
return False, f"converter {
|
|
513
|
+
return False, f"converter {column_converter[column_idx]} not found", np.array([])
|
|
306
514
|
|
|
307
515
|
# snif txt file
|
|
308
516
|
try:
|
|
@@ -324,9 +532,7 @@ def txt2np_array(
|
|
|
324
532
|
return False, f"{sys.exc_info()[1]}", np.array([])
|
|
325
533
|
|
|
326
534
|
try:
|
|
327
|
-
data = np.loadtxt(
|
|
328
|
-
file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters
|
|
329
|
-
)
|
|
535
|
+
data = np.loadtxt(file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters)
|
|
330
536
|
|
|
331
537
|
except Exception:
|
|
332
538
|
return False, f"{sys.exc_info()[1]}", np.array([])
|
|
@@ -338,7 +544,7 @@ def txt2np_array(
|
|
|
338
544
|
return True, "", data
|
|
339
545
|
|
|
340
546
|
|
|
341
|
-
def versiontuple(version_str: str):
|
|
547
|
+
def versiontuple(version_str: str) -> tuple:
|
|
342
548
|
"""
|
|
343
549
|
Convert version from str to tuple of str
|
|
344
550
|
|
|
@@ -381,7 +587,7 @@ def behav_category_user_color(behavioral_categories: dict, name: str) -> Union[s
|
|
|
381
587
|
|
|
382
588
|
def state_behavior_codes(ethogram: dict) -> list:
|
|
383
589
|
"""
|
|
384
|
-
behavior codes defined as STATE event
|
|
590
|
+
returns a list of behavior codes defined as STATE event
|
|
385
591
|
|
|
386
592
|
Args:
|
|
387
593
|
ethogram (dict): ethogram dictionary
|
|
@@ -390,12 +596,12 @@ def state_behavior_codes(ethogram: dict) -> list:
|
|
|
390
596
|
list: list of behavior codes defined as STATE event
|
|
391
597
|
|
|
392
598
|
"""
|
|
393
|
-
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if
|
|
599
|
+
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if ethogram[x][cfg.TYPE] in cfg.STATE_EVENT_TYPES]
|
|
394
600
|
|
|
395
601
|
|
|
396
602
|
def point_behavior_codes(ethogram: dict) -> list:
|
|
397
603
|
"""
|
|
398
|
-
behavior codes defined as POINT event
|
|
604
|
+
returns a list of behavior codes defined as POINT event
|
|
399
605
|
|
|
400
606
|
Args:
|
|
401
607
|
ethogram (dict): ethogram dictionary
|
|
@@ -404,7 +610,7 @@ def point_behavior_codes(ethogram: dict) -> list:
|
|
|
404
610
|
list: list of behavior codes defined as POINT event
|
|
405
611
|
|
|
406
612
|
"""
|
|
407
|
-
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if
|
|
613
|
+
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)]
|
|
408
614
|
|
|
409
615
|
|
|
410
616
|
def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict:
|
|
@@ -457,8 +663,21 @@ def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict
|
|
|
457
663
|
return {"error": ""}
|
|
458
664
|
|
|
459
665
|
|
|
666
|
+
def flatten_list(nested_list) -> list:
|
|
667
|
+
"""
|
|
668
|
+
Flatten a list of lists.
|
|
669
|
+
"""
|
|
670
|
+
flattened: list = []
|
|
671
|
+
for item in nested_list:
|
|
672
|
+
if isinstance(item, list):
|
|
673
|
+
flattened.extend(flatten_list(item))
|
|
674
|
+
else:
|
|
675
|
+
flattened.append(item)
|
|
676
|
+
return flattened
|
|
677
|
+
|
|
678
|
+
|
|
460
679
|
def get_current_states_modifiers_by_subject(
|
|
461
|
-
state_behaviors_codes: list, events: list, subjects: dict,
|
|
680
|
+
state_behaviors_codes: list, events: list, subjects: dict, time_: dec, include_modifiers: bool = False
|
|
462
681
|
) -> dict:
|
|
463
682
|
"""
|
|
464
683
|
get current states and modifiers (if requested) for subjects at given time
|
|
@@ -474,7 +693,7 @@ def get_current_states_modifiers_by_subject(
|
|
|
474
693
|
dict: current states by subject. dict of list
|
|
475
694
|
"""
|
|
476
695
|
current_states: dict = {}
|
|
477
|
-
if
|
|
696
|
+
if time_.is_nan():
|
|
478
697
|
for idx in subjects:
|
|
479
698
|
current_states[idx] = []
|
|
480
699
|
return current_states
|
|
@@ -487,43 +706,45 @@ def get_current_states_modifiers_by_subject(
|
|
|
487
706
|
|
|
488
707
|
if include_modifiers:
|
|
489
708
|
for idx in subjects:
|
|
490
|
-
current_states[idx] =
|
|
491
|
-
|
|
492
|
-
|
|
709
|
+
current_states[subjects[idx]["name"]] = {}
|
|
710
|
+
for x in events:
|
|
711
|
+
if x[check_index] > time_:
|
|
712
|
+
break
|
|
713
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
|
|
714
|
+
if (x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX]) not in current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]]:
|
|
715
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])] = (
|
|
716
|
+
False
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][
|
|
493
720
|
(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
|
|
494
|
-
|
|
495
|
-
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx][cfg.SUBJECT_NAME]
|
|
496
|
-
and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
|
|
497
|
-
and x[check_index] <= time
|
|
498
|
-
]
|
|
721
|
+
] = not current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])]
|
|
499
722
|
|
|
500
|
-
|
|
501
|
-
|
|
723
|
+
r: dict = {}
|
|
724
|
+
for idx in subjects:
|
|
725
|
+
r[idx] = [f"{bm[0]} ({bm[1]})" for bm in current_states[subjects[idx]["name"]] if current_states[subjects[idx]["name"]][bm]]
|
|
502
726
|
|
|
503
727
|
else:
|
|
504
728
|
for idx in subjects:
|
|
505
|
-
current_states[idx] =
|
|
506
|
-
for
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
current_states[idx].append(sbc)
|
|
729
|
+
current_states[subjects[idx]["name"]] = {}
|
|
730
|
+
for b in state_behaviors_codes:
|
|
731
|
+
current_states[subjects[idx]["name"]][b] = False
|
|
732
|
+
for x in events:
|
|
733
|
+
if x[check_index] > time_:
|
|
734
|
+
break
|
|
735
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
|
|
736
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = not current_states[
|
|
737
|
+
x[cfg.EVENT_SUBJECT_FIELD_IDX]
|
|
738
|
+
][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]]
|
|
739
|
+
|
|
740
|
+
r: dict = {}
|
|
741
|
+
for idx in subjects:
|
|
742
|
+
r[idx] = [b for b in state_behaviors_codes if current_states[subjects[idx]["name"]][b]]
|
|
520
743
|
|
|
521
|
-
return
|
|
744
|
+
return r
|
|
522
745
|
|
|
523
746
|
|
|
524
|
-
def get_current_states_modifiers_by_subject_2(
|
|
525
|
-
state_behaviors_codes: list, events: list, subjects: dict, time: dec
|
|
526
|
-
) -> dict:
|
|
747
|
+
def get_current_states_modifiers_by_subject_2(state_behaviors_codes: list, events: list, subjects: dict, time: dec) -> dict:
|
|
527
748
|
"""
|
|
528
749
|
get current states and modifiers for subjects at given time
|
|
529
750
|
differs from get_current_states_modifiers_by_subject in the output format: [behavior, modifiers]
|
|
@@ -533,7 +754,6 @@ def get_current_states_modifiers_by_subject_2(
|
|
|
533
754
|
events (list): list of events
|
|
534
755
|
subjects (dict): dictionary of subjects
|
|
535
756
|
time (Decimal): time
|
|
536
|
-
include_modifiers (bool): include modifier if True (default: False)
|
|
537
757
|
|
|
538
758
|
Returns:
|
|
539
759
|
dict: current states by subject. dict of list
|
|
@@ -587,7 +807,8 @@ def get_current_points_by_subject(
|
|
|
587
807
|
point_events = [
|
|
588
808
|
(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
|
|
589
809
|
for x in events
|
|
590
|
-
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
|
|
810
|
+
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
|
|
811
|
+
and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
|
|
591
812
|
# and abs(x[EVENT_TIME_FIELD_IDX] - time) <= tolerance
|
|
592
813
|
and time <= x[cfg.EVENT_TIME_FIELD_IDX] < (time + tolerance)
|
|
593
814
|
]
|
|
@@ -604,19 +825,6 @@ def get_current_points_by_subject(
|
|
|
604
825
|
return current_points
|
|
605
826
|
|
|
606
827
|
|
|
607
|
-
def get_ip_address():
|
|
608
|
-
"""Get current IP address
|
|
609
|
-
|
|
610
|
-
Args:
|
|
611
|
-
|
|
612
|
-
Returns:
|
|
613
|
-
str: IP address
|
|
614
|
-
"""
|
|
615
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
616
|
-
s.connect(("8.8.8.8", 80))
|
|
617
|
-
return s.getsockname()[0]
|
|
618
|
-
|
|
619
|
-
|
|
620
828
|
def check_txt_file(file_name: str) -> dict:
|
|
621
829
|
"""
|
|
622
830
|
Extract parameters of txt file (test for tsv csv)
|
|
@@ -686,15 +894,15 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
|
|
|
686
894
|
str: wav file path or "" if error
|
|
687
895
|
"""
|
|
688
896
|
|
|
689
|
-
wav_file_path =
|
|
897
|
+
wav_file_path = Path(tmp_dir) / Path(media_file_path + ".wav").name
|
|
690
898
|
|
|
691
899
|
# check if media file is a wav file
|
|
692
900
|
try:
|
|
693
901
|
wav = wave.open(media_file_path, "r")
|
|
694
902
|
wav.close()
|
|
695
|
-
|
|
903
|
+
logger.debug(f"{media_file_path} is a WAV file. Copying in the temp directory...")
|
|
696
904
|
copyfile(media_file_path, wav_file_path)
|
|
697
|
-
|
|
905
|
+
logger.debug(f"{media_file_path} copied in {wav_file_path}")
|
|
698
906
|
return str(wav_file_path)
|
|
699
907
|
except Exception:
|
|
700
908
|
if wav_file_path.is_file():
|
|
@@ -709,7 +917,7 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
|
|
|
709
917
|
)
|
|
710
918
|
out, error = p.communicate()
|
|
711
919
|
out, error = out.decode("utf-8"), error.decode("utf-8")
|
|
712
|
-
|
|
920
|
+
logger.debug(f"{out}, {error}")
|
|
713
921
|
|
|
714
922
|
if "does not contain any stream" not in error:
|
|
715
923
|
if wav_file_path.is_file():
|
|
@@ -743,21 +951,28 @@ def complete(lst: list, max_: int) -> list:
|
|
|
743
951
|
|
|
744
952
|
def datetime_iso8601(dt) -> str:
|
|
745
953
|
"""
|
|
746
|
-
current date time in ISO8601 format without
|
|
954
|
+
current date time in ISO8601 format without microseconds
|
|
747
955
|
example: 2019-06-13 10:01:02
|
|
748
956
|
|
|
749
957
|
Returns:
|
|
750
|
-
str: date time in ISO8601 format
|
|
958
|
+
str: date time in ISO8601 format without microseconds
|
|
751
959
|
"""
|
|
752
|
-
return dt.isoformat(" "
|
|
960
|
+
return dt.isoformat(sep=" ", timespec="seconds")
|
|
753
961
|
|
|
754
962
|
|
|
755
|
-
def seconds_of_day(dt) -> dec:
|
|
963
|
+
def seconds_of_day(timestamp: dt.datetime) -> dec:
|
|
756
964
|
"""
|
|
757
965
|
return the number of seconds since start of the day
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
dev: number of seconds since the start of the day
|
|
758
969
|
"""
|
|
759
970
|
|
|
760
|
-
|
|
971
|
+
# logger.debug("function: seconds_of_day")
|
|
972
|
+
# logger.debug(f"{timestamp = }")
|
|
973
|
+
|
|
974
|
+
t = timestamp.time()
|
|
975
|
+
return dec(t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1000000).quantize(dec("0.001"))
|
|
761
976
|
|
|
762
977
|
|
|
763
978
|
def sorted_keys(d: dict) -> list:
|
|
@@ -787,7 +1002,7 @@ def intfloatstr(s: str) -> int:
|
|
|
787
1002
|
return s
|
|
788
1003
|
|
|
789
1004
|
|
|
790
|
-
def distance(p1, p2):
|
|
1005
|
+
def distance(p1: tuple, p2: tuple) -> float:
|
|
791
1006
|
"""
|
|
792
1007
|
euclidean distance between 2 points
|
|
793
1008
|
"""
|
|
@@ -819,6 +1034,62 @@ def angle(vertex: tuple, side1: tuple, side2: tuple) -> float:
|
|
|
819
1034
|
)
|
|
820
1035
|
|
|
821
1036
|
|
|
1037
|
+
def oriented_angle(P1: tuple, P2: tuple, P3: tuple) -> float:
|
|
1038
|
+
"""
|
|
1039
|
+
Calculate the oriented angle between two segments.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
P1 (tuple): Coordinates of the vertex
|
|
1043
|
+
P2 (tuple): Coordinates of the first point
|
|
1044
|
+
P3 (tuple): Coordinates of the second point
|
|
1045
|
+
|
|
1046
|
+
Returns:
|
|
1047
|
+
float: The oriented angle between the two segments in degrees.
|
|
1048
|
+
"""
|
|
1049
|
+
|
|
1050
|
+
x1, y1 = P1
|
|
1051
|
+
x2, y2 = P2
|
|
1052
|
+
x3, y3 = P1
|
|
1053
|
+
x4, y4 = P3
|
|
1054
|
+
|
|
1055
|
+
angle_AB = math.atan2(y2 - y1, x2 - x1)
|
|
1056
|
+
angle_CD = math.atan2(y4 - y3, x4 - x3)
|
|
1057
|
+
|
|
1058
|
+
oriented_angle = math.degrees(angle_AB - angle_CD)
|
|
1059
|
+
|
|
1060
|
+
return oriented_angle
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def oriented_angle_trigo(B: Tuple[float, float], A: Tuple[float, float], C: Tuple[float, float]) -> float:
|
|
1064
|
+
"""
|
|
1065
|
+
Calculates the oriented angle between vectors BA and BC, in degrees.
|
|
1066
|
+
The angle is positive in the counter-clockwise (trigonometric) direction.
|
|
1067
|
+
|
|
1068
|
+
Parameters:
|
|
1069
|
+
B: The pivot point (the origin of the vectors BA and BC).
|
|
1070
|
+
A, C: Points that define the vectors.
|
|
1071
|
+
|
|
1072
|
+
Returns:
|
|
1073
|
+
Angle in degrees, between 0 and 360.
|
|
1074
|
+
"""
|
|
1075
|
+
# Vectors BA and BC
|
|
1076
|
+
v1 = (A[0] - B[0], A[1] - B[1])
|
|
1077
|
+
v2 = (C[0] - B[0], C[1] - B[1])
|
|
1078
|
+
|
|
1079
|
+
# Dot product and 2D cross product (determinant)
|
|
1080
|
+
dot = v1[0] * v2[0] + v1[1] * v2[1]
|
|
1081
|
+
det = v1[0] * v2[1] - v1[1] * v2[0]
|
|
1082
|
+
|
|
1083
|
+
# Signed angle in radians, then converted to degrees
|
|
1084
|
+
angle_rad = math.atan2(det, dot)
|
|
1085
|
+
angle_deg = math.degrees(angle_rad)
|
|
1086
|
+
|
|
1087
|
+
if angle_deg < 0:
|
|
1088
|
+
angle_deg += 360
|
|
1089
|
+
|
|
1090
|
+
return angle_deg
|
|
1091
|
+
|
|
1092
|
+
|
|
822
1093
|
def mem_info():
|
|
823
1094
|
"""
|
|
824
1095
|
get info about total mem, used mem and available mem using:
|
|
@@ -836,9 +1107,7 @@ def mem_info():
|
|
|
836
1107
|
process = subprocess.run(["free", "-m"], stdout=subprocess.PIPE)
|
|
837
1108
|
# out, err = process.communicate()
|
|
838
1109
|
out = process.stdout
|
|
839
|
-
_, tot_mem, used_mem, _, _, _, available_mem = [
|
|
840
|
-
x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""
|
|
841
|
-
]
|
|
1110
|
+
_, tot_mem, used_mem, _, _, _, available_mem = [x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""]
|
|
842
1111
|
return False, {
|
|
843
1112
|
"total_memory": int(tot_mem),
|
|
844
1113
|
"used_memory": int(used_mem),
|
|
@@ -858,9 +1127,7 @@ def mem_info():
|
|
|
858
1127
|
|
|
859
1128
|
if sys.platform.startswith("win"):
|
|
860
1129
|
try:
|
|
861
|
-
output = subprocess.run(
|
|
862
|
-
["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE
|
|
863
|
-
)
|
|
1130
|
+
output = subprocess.run(["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
|
|
864
1131
|
tot_mem = int(output.stdout.strip().split(b"=")[-1].decode("utf-8")) / 1024 / 1024
|
|
865
1132
|
|
|
866
1133
|
output = subprocess.run(["wmic", "OS", "get", "FreePhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
|
|
@@ -873,7 +1140,7 @@ def mem_info():
|
|
|
873
1140
|
return True, {"msg": "Unknown operating system"}
|
|
874
1141
|
|
|
875
1142
|
|
|
876
|
-
def polygon_area(poly):
|
|
1143
|
+
def polygon_area(poly: list) -> float:
|
|
877
1144
|
"""
|
|
878
1145
|
area of polygon
|
|
879
1146
|
from http://www.mathopenref.com/coordpolygonarea.html
|
|
@@ -888,7 +1155,20 @@ def polygon_area(poly):
|
|
|
888
1155
|
return abs(tot / 2)
|
|
889
1156
|
|
|
890
1157
|
|
|
891
|
-
def
|
|
1158
|
+
def polyline_length(poly: list) -> float:
|
|
1159
|
+
"""
|
|
1160
|
+
length of polyline
|
|
1161
|
+
"""
|
|
1162
|
+
tot = 0
|
|
1163
|
+
for p in range(1, len(poly)):
|
|
1164
|
+
x1, y1 = poly[p - 1]
|
|
1165
|
+
x2, y2 = poly[p]
|
|
1166
|
+
tot += ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
|
|
1167
|
+
|
|
1168
|
+
return tot
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def url2path(url: str) -> str:
|
|
892
1172
|
"""
|
|
893
1173
|
convert URL in local path name
|
|
894
1174
|
under windows, check if path name begin with /
|
|
@@ -919,17 +1199,23 @@ def time2seconds(time_: str) -> dec:
|
|
|
919
1199
|
Decimal: time in seconds
|
|
920
1200
|
"""
|
|
921
1201
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1202
|
+
if " " in time_:
|
|
1203
|
+
try:
|
|
1204
|
+
return dec(str(dt.datetime.strptime(time_, "%Y-%m-%d %H:%M:%S.%f").timestamp()))
|
|
1205
|
+
except Exception:
|
|
1206
|
+
return dec("0.000")
|
|
1207
|
+
else:
|
|
1208
|
+
try:
|
|
1209
|
+
flag_neg = "-" in time_
|
|
1210
|
+
time_ = time_.replace("-", "")
|
|
1211
|
+
tsplit = time_.split(":")
|
|
1212
|
+
h, m, s = int(tsplit[0]), int(tsplit[1]), dec(tsplit[2])
|
|
1213
|
+
return dec(-(h * 3600 + m * 60 + s)) if flag_neg else dec(h * 3600 + m * 60 + s)
|
|
1214
|
+
except Exception:
|
|
1215
|
+
return dec("0.000")
|
|
930
1216
|
|
|
931
1217
|
|
|
932
|
-
def seconds2time(sec: dec) -> str:
|
|
1218
|
+
def seconds2time(sec: dec | None) -> str:
|
|
933
1219
|
"""
|
|
934
1220
|
convert seconds to hh:mm:ss.sss format
|
|
935
1221
|
|
|
@@ -938,12 +1224,15 @@ def seconds2time(sec: dec) -> str:
|
|
|
938
1224
|
Returns:
|
|
939
1225
|
str: time in format hh:mm:ss
|
|
940
1226
|
"""
|
|
1227
|
+
if sec is None:
|
|
1228
|
+
return cfg.NA
|
|
941
1229
|
|
|
942
1230
|
if math.isnan(sec):
|
|
943
1231
|
return cfg.NA
|
|
944
1232
|
|
|
945
|
-
if sec >
|
|
946
|
-
|
|
1233
|
+
# if sec > one day treat as date
|
|
1234
|
+
if sec > cfg.DATE_CUTOFF:
|
|
1235
|
+
t = dt.datetime.fromtimestamp(float(sec))
|
|
947
1236
|
return f"{t:%Y-%m-%d %H:%M:%S}.{t.microsecond / 1000:03.0f}"
|
|
948
1237
|
|
|
949
1238
|
neg_sign = "-" * (sec < 0)
|
|
@@ -1002,7 +1291,7 @@ def eol2space(s: str) -> str:
|
|
|
1002
1291
|
return s.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
|
|
1003
1292
|
|
|
1004
1293
|
|
|
1005
|
-
def test_ffmpeg_path(FFmpegPath):
|
|
1294
|
+
def test_ffmpeg_path(FFmpegPath: str) -> Tuple[bool, str]:
|
|
1006
1295
|
"""
|
|
1007
1296
|
test if ffmpeg has valid path
|
|
1008
1297
|
|
|
@@ -1014,22 +1303,20 @@ def test_ffmpeg_path(FFmpegPath):
|
|
|
1014
1303
|
str: message
|
|
1015
1304
|
"""
|
|
1016
1305
|
|
|
1017
|
-
out, error = subprocess.Popen(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
logging.debug(f"test ffmpeg path output: {out}")
|
|
1021
|
-
logging.debug(f"test ffmpeg path error: {error}")
|
|
1306
|
+
out, error = subprocess.Popen(f'"{FFmpegPath}" -version', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
|
|
1307
|
+
logger.debug(f"test ffmpeg path output: {out}")
|
|
1308
|
+
logger.debug(f"test ffmpeg path error: {error}")
|
|
1022
1309
|
|
|
1023
1310
|
if (b"avconv" in out) or (b"the Libav developers" in error):
|
|
1024
1311
|
return False, "Please use FFmpeg from https://www.ffmpeg.org in place of FFmpeg from Libav project."
|
|
1025
1312
|
|
|
1026
1313
|
if (b"ffmpeg version" not in out) and (b"ffmpeg version" not in error):
|
|
1027
|
-
return False, "FFmpeg is required but it was not found
|
|
1314
|
+
return False, "FFmpeg is required but it was not found.<br>See https://www.ffmpeg.org"
|
|
1028
1315
|
|
|
1029
1316
|
return True, ""
|
|
1030
1317
|
|
|
1031
1318
|
|
|
1032
|
-
def check_ffmpeg_path():
|
|
1319
|
+
def check_ffmpeg_path() -> Tuple[bool, str]:
|
|
1033
1320
|
"""
|
|
1034
1321
|
check for ffmpeg path
|
|
1035
1322
|
firstly search for embedded version
|
|
@@ -1040,43 +1327,24 @@ def check_ffmpeg_path():
|
|
|
1040
1327
|
str: if bool True returns ffmpegpath else returns error message
|
|
1041
1328
|
"""
|
|
1042
1329
|
|
|
1330
|
+
# search embedded ffmpeg
|
|
1043
1331
|
if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "boris" / "misc" / "ffmpeg"
|
|
1048
|
-
if sys.argv[0].endswith("__main__.py"):
|
|
1049
|
-
ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "misc" / "ffmpeg"
|
|
1050
|
-
|
|
1051
|
-
if not ffmpeg_path.is_file():
|
|
1052
|
-
# search global ffmpeg
|
|
1053
|
-
ffmpeg_path = "ffmpeg"
|
|
1054
|
-
|
|
1055
|
-
# test ffmpeg
|
|
1056
|
-
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1057
|
-
if r:
|
|
1058
|
-
return True, str(ffmpeg_path)
|
|
1059
|
-
else:
|
|
1060
|
-
return False, "FFmpeg is not available"
|
|
1332
|
+
ffmpeg_executable = Path("ffmpeg")
|
|
1333
|
+
elif sys.platform.startswith("win"):
|
|
1334
|
+
ffmpeg_executable = Path("ffmpeg.exe")
|
|
1061
1335
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
# test ffmpeg
|
|
1075
|
-
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1076
|
-
if r:
|
|
1077
|
-
return True, str(ffmpeg_path)
|
|
1078
|
-
else:
|
|
1079
|
-
return False, "FFmpeg is not available"
|
|
1336
|
+
ffmpeg_path = Path(__file__).parent / "misc" / ffmpeg_executable
|
|
1337
|
+
|
|
1338
|
+
if not ffmpeg_path.is_file():
|
|
1339
|
+
# search global ffmpeg
|
|
1340
|
+
ffmpeg_path = ffmpeg_executable
|
|
1341
|
+
|
|
1342
|
+
# test ffmpeg
|
|
1343
|
+
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1344
|
+
if r:
|
|
1345
|
+
return True, str(ffmpeg_path)
|
|
1346
|
+
else:
|
|
1347
|
+
return False, "FFmpeg is not available"
|
|
1080
1348
|
|
|
1081
1349
|
|
|
1082
1350
|
def smart_size_format(n: Union[float, int, str, None]) -> str:
|
|
@@ -1098,6 +1366,73 @@ def smart_size_format(n: Union[float, int, str, None]) -> str:
|
|
|
1098
1366
|
return f"{n / 1_000_000_000:,.1f} Gb"
|
|
1099
1367
|
|
|
1100
1368
|
|
|
1369
|
+
def get_systeminfo() -> str:
|
|
1370
|
+
"""
|
|
1371
|
+
returns info about the system
|
|
1372
|
+
"""
|
|
1373
|
+
|
|
1374
|
+
mpv_lib_version_, mpv_lib_file_path, mpv_api_version = mpv_lib_version()
|
|
1375
|
+
|
|
1376
|
+
system_info = (
|
|
1377
|
+
f"BORIS version: {version.__version__}\n"
|
|
1378
|
+
f"OS: {platform.uname().system} {platform.uname().release} {platform.uname().version}\n"
|
|
1379
|
+
f"CPU: {platform.uname().machine} {platform.uname().processor}\n"
|
|
1380
|
+
f"Python {platform.python_version()} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})\n"
|
|
1381
|
+
f"Qt {qVersion()} - PySide {pyside6_version}\n"
|
|
1382
|
+
f"MPV library version: {mpv_lib_version_}\n"
|
|
1383
|
+
f"MPV API version: {mpv_api_version}\n"
|
|
1384
|
+
f"MPV library file path: {mpv_lib_file_path}\n\n"
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
r, memory = mem_info()
|
|
1388
|
+
if not r:
|
|
1389
|
+
system_info += (
|
|
1390
|
+
f"Memory (RAM) Total: {memory.get('total_memory', 'Not available'):.2f} Mb "
|
|
1391
|
+
f"Free: {memory.get('free_memory', 'Not available'):.2f} Mb\n\n"
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
return system_info
|
|
1395
|
+
|
|
1396
|
+
"""
|
|
1397
|
+
# system info
|
|
1398
|
+
systeminfo = ""
|
|
1399
|
+
if sys.platform.startswith("win"):
|
|
1400
|
+
# systeminfo = subprocess.getoutput("systeminfo")
|
|
1401
|
+
systeminfo = subprocess.run("systeminfo /FO csv /NH", capture_output=True, text=True, encoding="mbcs", shell=True).stdout
|
|
1402
|
+
|
|
1403
|
+
import csv
|
|
1404
|
+
from io import StringIO
|
|
1405
|
+
|
|
1406
|
+
# Parse it as CSV
|
|
1407
|
+
f = StringIO(systeminfo)
|
|
1408
|
+
reader = csv.reader(f)
|
|
1409
|
+
parsed_data = list(reader)[0]
|
|
1410
|
+
# Print specific fields by index
|
|
1411
|
+
info_to_show = ""
|
|
1412
|
+
info_to_show += f"Computer Name: {parsed_data[0]}\n"
|
|
1413
|
+
info_to_show += f"OS Name: {parsed_data[1]}\n"
|
|
1414
|
+
info_to_show += f"OS Version: {parsed_data[2]}\n"
|
|
1415
|
+
info_to_show += f"System Manufacturer: {parsed_data[11]}\n"
|
|
1416
|
+
info_to_show += f"System Model: {parsed_data[12]}\n"
|
|
1417
|
+
info_to_show += f"Processor: {parsed_data[14]}\n"
|
|
1418
|
+
info_to_show += f"Locale: {parsed_data[19]}\n"
|
|
1419
|
+
info_to_show += f"Installed Memory: {parsed_data[22]}\n"
|
|
1420
|
+
|
|
1421
|
+
# info about graphic card
|
|
1422
|
+
graphic_info = subprocess.run(
|
|
1423
|
+
"wmic path win32_videocontroller get name", capture_output=True, text=True, encoding="mbcs", shell=True
|
|
1424
|
+
).stdout
|
|
1425
|
+
info_to_show += graphic_info.replace("\n", "").replace("Name", "Graphic card model")
|
|
1426
|
+
|
|
1427
|
+
systeminfo = info_to_show
|
|
1428
|
+
|
|
1429
|
+
if sys.platform.startswith("linux"):
|
|
1430
|
+
systeminfo = subprocess.getoutput("cat /etc/*rel*; uname -a")
|
|
1431
|
+
|
|
1432
|
+
return systeminfo
|
|
1433
|
+
"""
|
|
1434
|
+
|
|
1435
|
+
|
|
1101
1436
|
def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
1102
1437
|
"""
|
|
1103
1438
|
analyse video parameters with ffprobe (if available)
|
|
@@ -1109,28 +1444,32 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1109
1444
|
Returns:
|
|
1110
1445
|
dict
|
|
1111
1446
|
"""
|
|
1112
|
-
|
|
1447
|
+
|
|
1448
|
+
# check ffprobe executable in same place than ffmpeg
|
|
1113
1449
|
ffprobe_bin = ffmpeg_bin.replace("ffmpeg", "ffprobe")
|
|
1450
|
+
if not Path(ffprobe_bin).is_file():
|
|
1451
|
+
if which(ffprobe_bin) is None:
|
|
1452
|
+
return {"error": "ffprobe not found"}
|
|
1114
1453
|
|
|
1115
1454
|
command = f'"{ffprobe_bin}" -hide_banner -v error -print_format json -show_format -show_streams "{file_name}"'
|
|
1116
1455
|
|
|
1117
|
-
# print(command)
|
|
1118
|
-
|
|
1119
1456
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
1120
1457
|
out, error = p.communicate()
|
|
1121
1458
|
if error:
|
|
1122
|
-
|
|
1459
|
+
if b"invalid data" in error:
|
|
1460
|
+
return {"error": f"{error}"}
|
|
1123
1461
|
|
|
1124
1462
|
try:
|
|
1125
1463
|
hasVideo = False
|
|
1126
1464
|
hasAudio = False
|
|
1127
|
-
|
|
1465
|
+
"""bitrate = None"""
|
|
1128
1466
|
video_bitrate = None
|
|
1129
1467
|
audio_bitrate = []
|
|
1130
1468
|
resolution = None
|
|
1131
|
-
fps = 0
|
|
1469
|
+
fps: float = 0
|
|
1132
1470
|
sample_rate = None
|
|
1133
|
-
duration =
|
|
1471
|
+
duration = None
|
|
1472
|
+
audio_duration = cfg.NA
|
|
1134
1473
|
frames_number = None
|
|
1135
1474
|
size = None
|
|
1136
1475
|
audio_codec = None
|
|
@@ -1164,9 +1503,16 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1164
1503
|
fps = eval(stream["r_frame_rate"])
|
|
1165
1504
|
except Exception:
|
|
1166
1505
|
fps = 0
|
|
1506
|
+
if fps >= 1000 and "avg_frame_rate" in stream: # case for some h265 video ("r_frame_rate": "1200000/1")
|
|
1507
|
+
try:
|
|
1508
|
+
fps = eval(stream["avg_frame_rate"])
|
|
1509
|
+
except Exception:
|
|
1510
|
+
pass
|
|
1167
1511
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1512
|
+
fps = round(fps, 3)
|
|
1513
|
+
|
|
1514
|
+
if "duration" in stream:
|
|
1515
|
+
duration = float(stream["duration"])
|
|
1170
1516
|
if "duration_ts" in stream:
|
|
1171
1517
|
frames_number = int(stream["duration_ts"])
|
|
1172
1518
|
elif "nb_frames" in stream:
|
|
@@ -1178,11 +1524,19 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1178
1524
|
|
|
1179
1525
|
if stream["codec_type"] == "audio":
|
|
1180
1526
|
hasAudio = True
|
|
1181
|
-
sample_rate = float(stream["sample_rate"])
|
|
1182
|
-
|
|
1527
|
+
sample_rate = float(stream["sample_rate"]) if "sample_rate" in stream else cfg.NA
|
|
1528
|
+
# TODO manage audio_duration parameter
|
|
1529
|
+
audio_duration = float(stream["duration"]) if "duration" in stream else cfg.NA
|
|
1183
1530
|
audio_codec = stream["codec_long_name"]
|
|
1184
1531
|
audio_bitrate.append(int(stream.get("bit_rate", 0)))
|
|
1185
1532
|
|
|
1533
|
+
# check duration
|
|
1534
|
+
if duration is None:
|
|
1535
|
+
if "duration" in video_param["format"]:
|
|
1536
|
+
duration = float(video_param["format"]["duration"])
|
|
1537
|
+
else:
|
|
1538
|
+
duration = 0
|
|
1539
|
+
|
|
1186
1540
|
# check bit rate
|
|
1187
1541
|
if "bit_rate" in video_param["format"]:
|
|
1188
1542
|
all_bitrate = int(video_param["format"]["bit_rate"])
|
|
@@ -1192,11 +1546,22 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1192
1546
|
if video_bitrate is None and all_bitrate is not None:
|
|
1193
1547
|
video_bitrate = all_bitrate - sum(audio_bitrate)
|
|
1194
1548
|
|
|
1549
|
+
# extract format long name
|
|
1550
|
+
format_long_name = video_param["format"]["format_long_name"] if "format_long_name" in video_param["format"] else cfg.NA
|
|
1551
|
+
|
|
1552
|
+
# extract creation time ("creation_time": "2023-03-22T16:50:32.000000Z")
|
|
1553
|
+
creation_time = cfg.NA
|
|
1554
|
+
if "tags" in video_param["format"] and "creation_time" in video_param["format"]["tags"]:
|
|
1555
|
+
creation_time = video_param["format"]["tags"]["creation_time"].replace("T", " ")
|
|
1556
|
+
if "." in creation_time:
|
|
1557
|
+
creation_time = creation_time.split(".")[0]
|
|
1558
|
+
|
|
1195
1559
|
return {
|
|
1196
1560
|
"analysis_program": "ffprobe",
|
|
1197
1561
|
"frames_number": frames_number,
|
|
1198
1562
|
"duration_ms": duration * 1000,
|
|
1199
1563
|
"duration": duration,
|
|
1564
|
+
"audio_duration": audio_duration,
|
|
1200
1565
|
"fps": fps,
|
|
1201
1566
|
"has_video": hasVideo,
|
|
1202
1567
|
"has_audio": hasAudio,
|
|
@@ -1206,6 +1571,8 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1206
1571
|
"file size": size,
|
|
1207
1572
|
"audio_codec": audio_codec,
|
|
1208
1573
|
"video_codec": video_codec,
|
|
1574
|
+
"creation_time": creation_time,
|
|
1575
|
+
"format_long_name": format_long_name,
|
|
1209
1576
|
}
|
|
1210
1577
|
|
|
1211
1578
|
except Exception as e:
|
|
@@ -1214,7 +1581,7 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1214
1581
|
|
|
1215
1582
|
def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
1216
1583
|
"""
|
|
1217
|
-
analyse frame rate and video duration with ffmpeg
|
|
1584
|
+
analyse frame rate and video duration with ffprobe or ffmpeg if ffprobe not available
|
|
1218
1585
|
Returns parameters: duration, duration_ms, bitrate, frames_number, fps, has_video (True/False), has_audio (True/False)
|
|
1219
1586
|
|
|
1220
1587
|
Args:
|
|
@@ -1228,13 +1595,14 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1228
1595
|
|
|
1229
1596
|
ffprobe_results = ffprobe_media_analysis(ffmpeg_bin, file_name)
|
|
1230
1597
|
|
|
1598
|
+
logger.debug(f"file_name: {file_name}")
|
|
1599
|
+
logger.debug(f"ffprobe_results: {ffprobe_results}")
|
|
1600
|
+
|
|
1231
1601
|
if ("error" not in ffprobe_results) and (ffprobe_results["bitrate"] is not None):
|
|
1232
1602
|
return ffprobe_results
|
|
1233
1603
|
else:
|
|
1234
1604
|
# use ffmpeg
|
|
1235
|
-
command = (
|
|
1236
|
-
f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
|
|
1237
|
-
)
|
|
1605
|
+
command = f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
|
|
1238
1606
|
|
|
1239
1607
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
1240
1608
|
|
|
@@ -1292,7 +1660,7 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1292
1660
|
if b"Stream #" in row and b"Video:" in row:
|
|
1293
1661
|
hasVideo = True
|
|
1294
1662
|
# get resolution \d{3,5}x\d{3,5}
|
|
1295
|
-
re_results = re.search(
|
|
1663
|
+
re_results = re.search(r"\d{3,5}x\d{3,5}", row, re.IGNORECASE)
|
|
1296
1664
|
if re_results:
|
|
1297
1665
|
resolution = re_results.group(0).decode("utf-8")
|
|
1298
1666
|
break
|
|
@@ -1317,11 +1685,13 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1317
1685
|
"frames_number": int(fps * duration),
|
|
1318
1686
|
"duration_ms": duration * 1000,
|
|
1319
1687
|
"duration": duration,
|
|
1688
|
+
"audio_duration": cfg.NA,
|
|
1320
1689
|
"fps": fps,
|
|
1321
1690
|
"has_video": hasVideo,
|
|
1322
1691
|
"has_audio": hasAudio,
|
|
1323
1692
|
"bitrate": bitrate,
|
|
1324
1693
|
"resolution": resolution,
|
|
1694
|
+
"format_long_name": "",
|
|
1325
1695
|
}
|
|
1326
1696
|
|
|
1327
1697
|
|
|
@@ -1362,12 +1732,37 @@ def all_behaviors(ethogram: dict) -> list:
|
|
|
1362
1732
|
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in sorted_keys(ethogram)]
|
|
1363
1733
|
|
|
1364
1734
|
|
|
1735
|
+
def all_subjects(subjects: dict) -> list:
|
|
1736
|
+
"""
|
|
1737
|
+
extract all subjects from the subject configuration dictionary
|
|
1738
|
+
|
|
1739
|
+
Args:
|
|
1740
|
+
subject configuration (dict)
|
|
1741
|
+
|
|
1742
|
+
Returns:
|
|
1743
|
+
list: subjects name
|
|
1744
|
+
"""
|
|
1745
|
+
|
|
1746
|
+
return [subjects[x][cfg.SUBJECT_NAME] for x in sorted_keys(subjects)]
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
def has_coding_map(ethogram: dict, behavior_idx: str) -> bool:
|
|
1750
|
+
"""
|
|
1751
|
+
check if behavior index has a coding map
|
|
1752
|
+
"""
|
|
1753
|
+
if not ethogram.get(behavior_idx, False):
|
|
1754
|
+
return False
|
|
1755
|
+
if not ethogram[behavior_idx].get("coding map", False):
|
|
1756
|
+
return False
|
|
1757
|
+
return True
|
|
1758
|
+
|
|
1759
|
+
|
|
1365
1760
|
def dir_images_number(dir_path_str: str) -> dict:
|
|
1366
1761
|
"""
|
|
1367
1762
|
return number of images in dir_path (see cfg.IMAGE_EXTENSIONS)
|
|
1368
1763
|
"""
|
|
1369
1764
|
|
|
1370
|
-
dir_path =
|
|
1765
|
+
dir_path = Path(dir_path_str)
|
|
1371
1766
|
if not dir_path.is_dir():
|
|
1372
1767
|
return {"error": f"The directory {dir_path_str} does not exists"}
|
|
1373
1768
|
img_count = 0
|
|
@@ -1405,29 +1800,11 @@ def intersection(A, B, C, D):
|
|
|
1405
1800
|
ym = slope * xm + intersept
|
|
1406
1801
|
else:
|
|
1407
1802
|
xm = (
|
|
1408
|
-
(
|
|
1409
|
-
xd * xa * yc
|
|
1410
|
-
- xd * xb * yc
|
|
1411
|
-
- xd * xa * yb
|
|
1412
|
-
- xc * xa * yd
|
|
1413
|
-
+ xc * xa * yb
|
|
1414
|
-
+ xd * ya * xb
|
|
1415
|
-
+ xc * xb * yd
|
|
1416
|
-
- xc * ya * xb
|
|
1417
|
-
)
|
|
1803
|
+
(xd * xa * yc - xd * xb * yc - xd * xa * yb - xc * xa * yd + xc * xa * yb + xd * ya * xb + xc * xb * yd - xc * ya * xb)
|
|
1418
1804
|
/ (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
|
|
1419
1805
|
).quantize(dec(".001"), rounding=ROUND_DOWN)
|
|
1420
1806
|
ym = (
|
|
1421
|
-
(
|
|
1422
|
-
yb * xc * yd
|
|
1423
|
-
- yb * yc * xd
|
|
1424
|
-
- ya * xc * yd
|
|
1425
|
-
+ ya * yc * xd
|
|
1426
|
-
- xa * yb * yd
|
|
1427
|
-
+ xa * yb * yc
|
|
1428
|
-
+ ya * xb * yd
|
|
1429
|
-
- ya * xb * yc
|
|
1430
|
-
)
|
|
1807
|
+
(yb * xc * yd - yb * yc * xd - ya * xc * yd + ya * yc * xd - xa * yb * yd + xa * yb * yc + ya * xb * yd - ya * xb * yc)
|
|
1431
1808
|
/ (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
|
|
1432
1809
|
).quantize(dec(".001"), rounding=ROUND_DOWN)
|
|
1433
1810
|
|
|
@@ -1436,16 +1813,7 @@ def intersection(A, B, C, D):
|
|
|
1436
1813
|
ymin1, ymax1 = min(ya, yb), max(ya, yb)
|
|
1437
1814
|
ymin2, ymax2 = min(yc, yd), max(yc, yd)
|
|
1438
1815
|
|
|
1439
|
-
return
|
|
1440
|
-
xm >= xmin1
|
|
1441
|
-
and xm <= xmax1
|
|
1442
|
-
and xm >= xmin2
|
|
1443
|
-
and xm <= xmax2
|
|
1444
|
-
and ym >= ymin1
|
|
1445
|
-
and ym <= ymax1
|
|
1446
|
-
and ym >= ymin2
|
|
1447
|
-
and ym <= ymax2
|
|
1448
|
-
)
|
|
1816
|
+
return xm >= xmin1 and xm <= xmax1 and xm >= xmin2 and xm <= xmax2 and ym >= ymin1 and ym <= ymax1 and ym >= ymin2 and ym <= ymax2
|
|
1449
1817
|
|
|
1450
1818
|
except Exception: # for cases xa=xb=xc=xd
|
|
1451
1819
|
return True
|