boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.1__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 +24 -36
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +235 -131
- boris/advanced_event_filtering.py +23 -29
- 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 +16 -34
- boris/config.py +102 -50
- boris/config_file.py +55 -64
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2108 -1275
- 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 +304 -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 +493 -210
- boris/observation_operations.py +1010 -391
- 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 +18 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +80 -56
- boris/plot_waveform_rt.py +23 -48
- 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 +298 -123
- boris/preferences_ui.py +664 -225
- boris/project.py +293 -270
- boris/project_functions.py +610 -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 +6 -8
- 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 +562 -222
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +78 -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.1.dist-info/METADATA +140 -0
- boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.1.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.1.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.1.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,243 @@ 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
|
|
40
|
-
|
|
41
|
-
import numpy as np
|
|
42
|
-
from PyQt5.QtGui import QPixmap, QImage
|
|
47
|
+
import socket
|
|
43
48
|
|
|
44
|
-
from
|
|
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 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"
|
|
47
259
|
|
|
48
260
|
|
|
49
261
|
def error_info(exc_info: tuple) -> tuple:
|
|
@@ -154,28 +366,7 @@ def return_file_header_footer(file_name: str, file_row_number: int = 0, row_numb
|
|
|
154
366
|
return header, footer
|
|
155
367
|
|
|
156
368
|
|
|
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:
|
|
369
|
+
def convertTime(time_format: str, sec: Union[float, dec]) -> Union[str, None]:
|
|
179
370
|
"""
|
|
180
371
|
convert time in base at the current format (S or HHMMSS)
|
|
181
372
|
|
|
@@ -198,10 +389,12 @@ def convertTime(time_format: str, sec: Union[float, dec]) -> str:
|
|
|
198
389
|
return None
|
|
199
390
|
|
|
200
391
|
|
|
201
|
-
def
|
|
392
|
+
def smart_time_format(sec: Union[float, dec], time_format: str = cfg.S, cutoff: dec = cfg.SMART_TIME_CUTOFF_DEFAULT) -> str:
|
|
202
393
|
"""
|
|
394
|
+
Smart time format
|
|
203
395
|
returns time in seconds if <= cutoff else in HH:MM:SS.ZZZ format
|
|
204
396
|
"""
|
|
397
|
+
# cutoff = 0 follows the time format selectd by user
|
|
205
398
|
if cutoff == 0:
|
|
206
399
|
return convertTime(time_format, sec)
|
|
207
400
|
if sec <= cutoff:
|
|
@@ -222,7 +415,10 @@ def convert_time_to_decimal(pj: dict) -> dict:
|
|
|
222
415
|
"""
|
|
223
416
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
224
417
|
if cfg.TIME_OFFSET in pj[cfg.OBSERVATIONS][obs_id]:
|
|
225
|
-
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")
|
|
226
422
|
for idx, _ in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
|
|
227
423
|
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX] = dec(
|
|
228
424
|
pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX]
|
|
@@ -238,20 +434,6 @@ def count_media_file(media_files: dict) -> int:
|
|
|
238
434
|
return sum([len(media_files[idx]) for idx in media_files])
|
|
239
435
|
|
|
240
436
|
|
|
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
437
|
def txt2np_array(
|
|
256
438
|
file_name: str, columns_str: str, substract_first_value: str, converters=None, column_converter=None
|
|
257
439
|
) -> Tuple[bool, str, np.array]:
|
|
@@ -324,9 +506,7 @@ def txt2np_array(
|
|
|
324
506
|
return False, f"{sys.exc_info()[1]}", np.array([])
|
|
325
507
|
|
|
326
508
|
try:
|
|
327
|
-
data = np.loadtxt(
|
|
328
|
-
file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters
|
|
329
|
-
)
|
|
509
|
+
data = np.loadtxt(file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters)
|
|
330
510
|
|
|
331
511
|
except Exception:
|
|
332
512
|
return False, f"{sys.exc_info()[1]}", np.array([])
|
|
@@ -338,7 +518,7 @@ def txt2np_array(
|
|
|
338
518
|
return True, "", data
|
|
339
519
|
|
|
340
520
|
|
|
341
|
-
def versiontuple(version_str: str):
|
|
521
|
+
def versiontuple(version_str: str) -> tuple:
|
|
342
522
|
"""
|
|
343
523
|
Convert version from str to tuple of str
|
|
344
524
|
|
|
@@ -381,7 +561,7 @@ def behav_category_user_color(behavioral_categories: dict, name: str) -> Union[s
|
|
|
381
561
|
|
|
382
562
|
def state_behavior_codes(ethogram: dict) -> list:
|
|
383
563
|
"""
|
|
384
|
-
behavior codes defined as STATE event
|
|
564
|
+
returns a list of behavior codes defined as STATE event
|
|
385
565
|
|
|
386
566
|
Args:
|
|
387
567
|
ethogram (dict): ethogram dictionary
|
|
@@ -390,12 +570,12 @@ def state_behavior_codes(ethogram: dict) -> list:
|
|
|
390
570
|
list: list of behavior codes defined as STATE event
|
|
391
571
|
|
|
392
572
|
"""
|
|
393
|
-
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if
|
|
573
|
+
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if ethogram[x][cfg.TYPE] in cfg.STATE_EVENT_TYPES]
|
|
394
574
|
|
|
395
575
|
|
|
396
576
|
def point_behavior_codes(ethogram: dict) -> list:
|
|
397
577
|
"""
|
|
398
|
-
behavior codes defined as POINT event
|
|
578
|
+
returns a list of behavior codes defined as POINT event
|
|
399
579
|
|
|
400
580
|
Args:
|
|
401
581
|
ethogram (dict): ethogram dictionary
|
|
@@ -404,7 +584,7 @@ def point_behavior_codes(ethogram: dict) -> list:
|
|
|
404
584
|
list: list of behavior codes defined as POINT event
|
|
405
585
|
|
|
406
586
|
"""
|
|
407
|
-
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if
|
|
587
|
+
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
588
|
|
|
409
589
|
|
|
410
590
|
def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict:
|
|
@@ -457,8 +637,21 @@ def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict
|
|
|
457
637
|
return {"error": ""}
|
|
458
638
|
|
|
459
639
|
|
|
640
|
+
def flatten_list(nested_list) -> list:
|
|
641
|
+
"""
|
|
642
|
+
Flatten a list of lists.
|
|
643
|
+
"""
|
|
644
|
+
flattened: list = []
|
|
645
|
+
for item in nested_list:
|
|
646
|
+
if isinstance(item, list):
|
|
647
|
+
flattened.extend(flatten_list(item))
|
|
648
|
+
else:
|
|
649
|
+
flattened.append(item)
|
|
650
|
+
return flattened
|
|
651
|
+
|
|
652
|
+
|
|
460
653
|
def get_current_states_modifiers_by_subject(
|
|
461
|
-
state_behaviors_codes: list, events: list, subjects: dict,
|
|
654
|
+
state_behaviors_codes: list, events: list, subjects: dict, time_: dec, include_modifiers: bool = False
|
|
462
655
|
) -> dict:
|
|
463
656
|
"""
|
|
464
657
|
get current states and modifiers (if requested) for subjects at given time
|
|
@@ -474,7 +667,7 @@ def get_current_states_modifiers_by_subject(
|
|
|
474
667
|
dict: current states by subject. dict of list
|
|
475
668
|
"""
|
|
476
669
|
current_states: dict = {}
|
|
477
|
-
if
|
|
670
|
+
if time_.is_nan():
|
|
478
671
|
for idx in subjects:
|
|
479
672
|
current_states[idx] = []
|
|
480
673
|
return current_states
|
|
@@ -487,43 +680,45 @@ def get_current_states_modifiers_by_subject(
|
|
|
487
680
|
|
|
488
681
|
if include_modifiers:
|
|
489
682
|
for idx in subjects:
|
|
490
|
-
current_states[idx] =
|
|
491
|
-
|
|
492
|
-
|
|
683
|
+
current_states[subjects[idx]["name"]] = {}
|
|
684
|
+
for x in events:
|
|
685
|
+
if x[check_index] > time_:
|
|
686
|
+
break
|
|
687
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
|
|
688
|
+
if (x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX]) not in current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]]:
|
|
689
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])] = (
|
|
690
|
+
False
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][
|
|
493
694
|
(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
|
-
]
|
|
695
|
+
] = not current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])]
|
|
499
696
|
|
|
500
|
-
|
|
501
|
-
|
|
697
|
+
r: dict = {}
|
|
698
|
+
for idx in subjects:
|
|
699
|
+
r[idx] = [f"{bm[0]} ({bm[1]})" for bm in current_states[subjects[idx]["name"]] if current_states[subjects[idx]["name"]][bm]]
|
|
502
700
|
|
|
503
701
|
else:
|
|
504
702
|
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)
|
|
703
|
+
current_states[subjects[idx]["name"]] = {}
|
|
704
|
+
for b in state_behaviors_codes:
|
|
705
|
+
current_states[subjects[idx]["name"]][b] = False
|
|
706
|
+
for x in events:
|
|
707
|
+
if x[check_index] > time_:
|
|
708
|
+
break
|
|
709
|
+
if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
|
|
710
|
+
current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = not current_states[
|
|
711
|
+
x[cfg.EVENT_SUBJECT_FIELD_IDX]
|
|
712
|
+
][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]]
|
|
713
|
+
|
|
714
|
+
r: dict = {}
|
|
715
|
+
for idx in subjects:
|
|
716
|
+
r[idx] = [b for b in state_behaviors_codes if current_states[subjects[idx]["name"]][b]]
|
|
520
717
|
|
|
521
|
-
return
|
|
718
|
+
return r
|
|
522
719
|
|
|
523
720
|
|
|
524
|
-
def get_current_states_modifiers_by_subject_2(
|
|
525
|
-
state_behaviors_codes: list, events: list, subjects: dict, time: dec
|
|
526
|
-
) -> dict:
|
|
721
|
+
def get_current_states_modifiers_by_subject_2(state_behaviors_codes: list, events: list, subjects: dict, time: dec) -> dict:
|
|
527
722
|
"""
|
|
528
723
|
get current states and modifiers for subjects at given time
|
|
529
724
|
differs from get_current_states_modifiers_by_subject in the output format: [behavior, modifiers]
|
|
@@ -533,7 +728,6 @@ def get_current_states_modifiers_by_subject_2(
|
|
|
533
728
|
events (list): list of events
|
|
534
729
|
subjects (dict): dictionary of subjects
|
|
535
730
|
time (Decimal): time
|
|
536
|
-
include_modifiers (bool): include modifier if True (default: False)
|
|
537
731
|
|
|
538
732
|
Returns:
|
|
539
733
|
dict: current states by subject. dict of list
|
|
@@ -587,7 +781,8 @@ def get_current_points_by_subject(
|
|
|
587
781
|
point_events = [
|
|
588
782
|
(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
|
|
589
783
|
for x in events
|
|
590
|
-
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
|
|
784
|
+
if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
|
|
785
|
+
and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
|
|
591
786
|
# and abs(x[EVENT_TIME_FIELD_IDX] - time) <= tolerance
|
|
592
787
|
and time <= x[cfg.EVENT_TIME_FIELD_IDX] < (time + tolerance)
|
|
593
788
|
]
|
|
@@ -604,19 +799,6 @@ def get_current_points_by_subject(
|
|
|
604
799
|
return current_points
|
|
605
800
|
|
|
606
801
|
|
|
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
802
|
def check_txt_file(file_name: str) -> dict:
|
|
621
803
|
"""
|
|
622
804
|
Extract parameters of txt file (test for tsv csv)
|
|
@@ -686,15 +868,15 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
|
|
|
686
868
|
str: wav file path or "" if error
|
|
687
869
|
"""
|
|
688
870
|
|
|
689
|
-
wav_file_path =
|
|
871
|
+
wav_file_path = Path(tmp_dir) / Path(media_file_path + ".wav").name
|
|
690
872
|
|
|
691
873
|
# check if media file is a wav file
|
|
692
874
|
try:
|
|
693
875
|
wav = wave.open(media_file_path, "r")
|
|
694
876
|
wav.close()
|
|
695
|
-
|
|
877
|
+
logger.debug(f"{media_file_path} is a WAV file. Copying in the temp directory...")
|
|
696
878
|
copyfile(media_file_path, wav_file_path)
|
|
697
|
-
|
|
879
|
+
logger.debug(f"{media_file_path} copied in {wav_file_path}")
|
|
698
880
|
return str(wav_file_path)
|
|
699
881
|
except Exception:
|
|
700
882
|
if wav_file_path.is_file():
|
|
@@ -709,7 +891,7 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
|
|
|
709
891
|
)
|
|
710
892
|
out, error = p.communicate()
|
|
711
893
|
out, error = out.decode("utf-8"), error.decode("utf-8")
|
|
712
|
-
|
|
894
|
+
logger.debug(f"{out}, {error}")
|
|
713
895
|
|
|
714
896
|
if "does not contain any stream" not in error:
|
|
715
897
|
if wav_file_path.is_file():
|
|
@@ -743,21 +925,28 @@ def complete(lst: list, max_: int) -> list:
|
|
|
743
925
|
|
|
744
926
|
def datetime_iso8601(dt) -> str:
|
|
745
927
|
"""
|
|
746
|
-
current date time in ISO8601 format without
|
|
928
|
+
current date time in ISO8601 format without microseconds
|
|
747
929
|
example: 2019-06-13 10:01:02
|
|
748
930
|
|
|
749
931
|
Returns:
|
|
750
|
-
str: date time in ISO8601 format
|
|
932
|
+
str: date time in ISO8601 format without microseconds
|
|
751
933
|
"""
|
|
752
|
-
return dt.isoformat(" "
|
|
934
|
+
return dt.isoformat(sep=" ", timespec="seconds")
|
|
753
935
|
|
|
754
936
|
|
|
755
|
-
def seconds_of_day(dt) -> dec:
|
|
937
|
+
def seconds_of_day(timestamp: dt.datetime) -> dec:
|
|
756
938
|
"""
|
|
757
939
|
return the number of seconds since start of the day
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
dev: number of seconds since the start of the day
|
|
758
943
|
"""
|
|
759
944
|
|
|
760
|
-
|
|
945
|
+
# logger.debug("function: seconds_of_day")
|
|
946
|
+
# logger.debug(f"{timestamp = }")
|
|
947
|
+
|
|
948
|
+
t = timestamp.time()
|
|
949
|
+
return dec(t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1000000).quantize(dec("0.001"))
|
|
761
950
|
|
|
762
951
|
|
|
763
952
|
def sorted_keys(d: dict) -> list:
|
|
@@ -787,7 +976,7 @@ def intfloatstr(s: str) -> int:
|
|
|
787
976
|
return s
|
|
788
977
|
|
|
789
978
|
|
|
790
|
-
def distance(p1, p2):
|
|
979
|
+
def distance(p1: tuple, p2: tuple) -> float:
|
|
791
980
|
"""
|
|
792
981
|
euclidean distance between 2 points
|
|
793
982
|
"""
|
|
@@ -819,6 +1008,62 @@ def angle(vertex: tuple, side1: tuple, side2: tuple) -> float:
|
|
|
819
1008
|
)
|
|
820
1009
|
|
|
821
1010
|
|
|
1011
|
+
def oriented_angle(P1: tuple, P2: tuple, P3: tuple) -> float:
|
|
1012
|
+
"""
|
|
1013
|
+
Calculate the oriented angle between two segments.
|
|
1014
|
+
|
|
1015
|
+
Args:
|
|
1016
|
+
P1 (tuple): Coordinates of the vertex
|
|
1017
|
+
P2 (tuple): Coordinates of the first point
|
|
1018
|
+
P3 (tuple): Coordinates of the second point
|
|
1019
|
+
|
|
1020
|
+
Returns:
|
|
1021
|
+
float: The oriented angle between the two segments in degrees.
|
|
1022
|
+
"""
|
|
1023
|
+
|
|
1024
|
+
x1, y1 = P1
|
|
1025
|
+
x2, y2 = P2
|
|
1026
|
+
x3, y3 = P1
|
|
1027
|
+
x4, y4 = P3
|
|
1028
|
+
|
|
1029
|
+
angle_AB = math.atan2(y2 - y1, x2 - x1)
|
|
1030
|
+
angle_CD = math.atan2(y4 - y3, x4 - x3)
|
|
1031
|
+
|
|
1032
|
+
oriented_angle = math.degrees(angle_AB - angle_CD)
|
|
1033
|
+
|
|
1034
|
+
return oriented_angle
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def oriented_angle_trigo(B: Tuple[float, float], A: Tuple[float, float], C: Tuple[float, float]) -> float:
|
|
1038
|
+
"""
|
|
1039
|
+
Calculates the oriented angle between vectors BA and BC, in degrees.
|
|
1040
|
+
The angle is positive in the counter-clockwise (trigonometric) direction.
|
|
1041
|
+
|
|
1042
|
+
Parameters:
|
|
1043
|
+
B: The pivot point (the origin of the vectors BA and BC).
|
|
1044
|
+
A, C: Points that define the vectors.
|
|
1045
|
+
|
|
1046
|
+
Returns:
|
|
1047
|
+
Angle in degrees, between 0 and 360.
|
|
1048
|
+
"""
|
|
1049
|
+
# Vectors BA and BC
|
|
1050
|
+
v1 = (A[0] - B[0], A[1] - B[1])
|
|
1051
|
+
v2 = (C[0] - B[0], C[1] - B[1])
|
|
1052
|
+
|
|
1053
|
+
# Dot product and 2D cross product (determinant)
|
|
1054
|
+
dot = v1[0] * v2[0] + v1[1] * v2[1]
|
|
1055
|
+
det = v1[0] * v2[1] - v1[1] * v2[0]
|
|
1056
|
+
|
|
1057
|
+
# Signed angle in radians, then converted to degrees
|
|
1058
|
+
angle_rad = math.atan2(det, dot)
|
|
1059
|
+
angle_deg = math.degrees(angle_rad)
|
|
1060
|
+
|
|
1061
|
+
if angle_deg < 0:
|
|
1062
|
+
angle_deg += 360
|
|
1063
|
+
|
|
1064
|
+
return angle_deg
|
|
1065
|
+
|
|
1066
|
+
|
|
822
1067
|
def mem_info():
|
|
823
1068
|
"""
|
|
824
1069
|
get info about total mem, used mem and available mem using:
|
|
@@ -836,9 +1081,7 @@ def mem_info():
|
|
|
836
1081
|
process = subprocess.run(["free", "-m"], stdout=subprocess.PIPE)
|
|
837
1082
|
# out, err = process.communicate()
|
|
838
1083
|
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
|
-
]
|
|
1084
|
+
_, tot_mem, used_mem, _, _, _, available_mem = [x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""]
|
|
842
1085
|
return False, {
|
|
843
1086
|
"total_memory": int(tot_mem),
|
|
844
1087
|
"used_memory": int(used_mem),
|
|
@@ -858,9 +1101,7 @@ def mem_info():
|
|
|
858
1101
|
|
|
859
1102
|
if sys.platform.startswith("win"):
|
|
860
1103
|
try:
|
|
861
|
-
output = subprocess.run(
|
|
862
|
-
["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE
|
|
863
|
-
)
|
|
1104
|
+
output = subprocess.run(["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
|
|
864
1105
|
tot_mem = int(output.stdout.strip().split(b"=")[-1].decode("utf-8")) / 1024 / 1024
|
|
865
1106
|
|
|
866
1107
|
output = subprocess.run(["wmic", "OS", "get", "FreePhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
|
|
@@ -873,7 +1114,7 @@ def mem_info():
|
|
|
873
1114
|
return True, {"msg": "Unknown operating system"}
|
|
874
1115
|
|
|
875
1116
|
|
|
876
|
-
def polygon_area(poly):
|
|
1117
|
+
def polygon_area(poly: list) -> float:
|
|
877
1118
|
"""
|
|
878
1119
|
area of polygon
|
|
879
1120
|
from http://www.mathopenref.com/coordpolygonarea.html
|
|
@@ -888,7 +1129,20 @@ def polygon_area(poly):
|
|
|
888
1129
|
return abs(tot / 2)
|
|
889
1130
|
|
|
890
1131
|
|
|
891
|
-
def
|
|
1132
|
+
def polyline_length(poly: list) -> float:
|
|
1133
|
+
"""
|
|
1134
|
+
length of polyline
|
|
1135
|
+
"""
|
|
1136
|
+
tot = 0
|
|
1137
|
+
for p in range(1, len(poly)):
|
|
1138
|
+
x1, y1 = poly[p - 1]
|
|
1139
|
+
x2, y2 = poly[p]
|
|
1140
|
+
tot += ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
|
|
1141
|
+
|
|
1142
|
+
return tot
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def url2path(url: str) -> str:
|
|
892
1146
|
"""
|
|
893
1147
|
convert URL in local path name
|
|
894
1148
|
under windows, check if path name begin with /
|
|
@@ -919,14 +1173,20 @@ def time2seconds(time_: str) -> dec:
|
|
|
919
1173
|
Decimal: time in seconds
|
|
920
1174
|
"""
|
|
921
1175
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1176
|
+
if " " in time_:
|
|
1177
|
+
try:
|
|
1178
|
+
return dec(str(dt.datetime.strptime(time_, "%Y-%m-%d %H:%M:%S.%f").timestamp()))
|
|
1179
|
+
except Exception:
|
|
1180
|
+
return dec("0.000")
|
|
1181
|
+
else:
|
|
1182
|
+
try:
|
|
1183
|
+
flag_neg = "-" in time_
|
|
1184
|
+
time_ = time_.replace("-", "")
|
|
1185
|
+
tsplit = time_.split(":")
|
|
1186
|
+
h, m, s = int(tsplit[0]), int(tsplit[1]), dec(tsplit[2])
|
|
1187
|
+
return dec(-(h * 3600 + m * 60 + s)) if flag_neg else dec(h * 3600 + m * 60 + s)
|
|
1188
|
+
except Exception:
|
|
1189
|
+
return dec("0.000")
|
|
930
1190
|
|
|
931
1191
|
|
|
932
1192
|
def seconds2time(sec: dec) -> str:
|
|
@@ -942,8 +1202,9 @@ def seconds2time(sec: dec) -> str:
|
|
|
942
1202
|
if math.isnan(sec):
|
|
943
1203
|
return cfg.NA
|
|
944
1204
|
|
|
945
|
-
if sec >
|
|
946
|
-
|
|
1205
|
+
# if sec > one day treat as date
|
|
1206
|
+
if sec > cfg.DATE_CUTOFF:
|
|
1207
|
+
t = dt.datetime.fromtimestamp(float(sec))
|
|
947
1208
|
return f"{t:%Y-%m-%d %H:%M:%S}.{t.microsecond / 1000:03.0f}"
|
|
948
1209
|
|
|
949
1210
|
neg_sign = "-" * (sec < 0)
|
|
@@ -1002,7 +1263,7 @@ def eol2space(s: str) -> str:
|
|
|
1002
1263
|
return s.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
|
|
1003
1264
|
|
|
1004
1265
|
|
|
1005
|
-
def test_ffmpeg_path(FFmpegPath):
|
|
1266
|
+
def test_ffmpeg_path(FFmpegPath: str) -> Tuple[bool, str]:
|
|
1006
1267
|
"""
|
|
1007
1268
|
test if ffmpeg has valid path
|
|
1008
1269
|
|
|
@@ -1014,22 +1275,20 @@ def test_ffmpeg_path(FFmpegPath):
|
|
|
1014
1275
|
str: message
|
|
1015
1276
|
"""
|
|
1016
1277
|
|
|
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}")
|
|
1278
|
+
out, error = subprocess.Popen(f'"{FFmpegPath}" -version', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
|
|
1279
|
+
logger.debug(f"test ffmpeg path output: {out}")
|
|
1280
|
+
logger.debug(f"test ffmpeg path error: {error}")
|
|
1022
1281
|
|
|
1023
1282
|
if (b"avconv" in out) or (b"the Libav developers" in error):
|
|
1024
1283
|
return False, "Please use FFmpeg from https://www.ffmpeg.org in place of FFmpeg from Libav project."
|
|
1025
1284
|
|
|
1026
1285
|
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
|
|
1286
|
+
return False, "FFmpeg is required but it was not found.<br>See https://www.ffmpeg.org"
|
|
1028
1287
|
|
|
1029
1288
|
return True, ""
|
|
1030
1289
|
|
|
1031
1290
|
|
|
1032
|
-
def check_ffmpeg_path():
|
|
1291
|
+
def check_ffmpeg_path() -> Tuple[bool, str]:
|
|
1033
1292
|
"""
|
|
1034
1293
|
check for ffmpeg path
|
|
1035
1294
|
firstly search for embedded version
|
|
@@ -1040,43 +1299,24 @@ def check_ffmpeg_path():
|
|
|
1040
1299
|
str: if bool True returns ffmpegpath else returns error message
|
|
1041
1300
|
"""
|
|
1042
1301
|
|
|
1302
|
+
# search embedded ffmpeg
|
|
1043
1303
|
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"
|
|
1304
|
+
ffmpeg_executable = Path("ffmpeg")
|
|
1305
|
+
elif sys.platform.startswith("win"):
|
|
1306
|
+
ffmpeg_executable = Path("ffmpeg.exe")
|
|
1061
1307
|
|
|
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"
|
|
1308
|
+
ffmpeg_path = Path(__file__).parent / "misc" / ffmpeg_executable
|
|
1309
|
+
|
|
1310
|
+
if not ffmpeg_path.is_file():
|
|
1311
|
+
# search global ffmpeg
|
|
1312
|
+
ffmpeg_path = ffmpeg_executable
|
|
1313
|
+
|
|
1314
|
+
# test ffmpeg
|
|
1315
|
+
r, msg = test_ffmpeg_path(str(ffmpeg_path))
|
|
1316
|
+
if r:
|
|
1317
|
+
return True, str(ffmpeg_path)
|
|
1318
|
+
else:
|
|
1319
|
+
return False, "FFmpeg is not available"
|
|
1080
1320
|
|
|
1081
1321
|
|
|
1082
1322
|
def smart_size_format(n: Union[float, int, str, None]) -> str:
|
|
@@ -1098,6 +1338,73 @@ def smart_size_format(n: Union[float, int, str, None]) -> str:
|
|
|
1098
1338
|
return f"{n / 1_000_000_000:,.1f} Gb"
|
|
1099
1339
|
|
|
1100
1340
|
|
|
1341
|
+
def get_systeminfo() -> str:
|
|
1342
|
+
"""
|
|
1343
|
+
returns info about the system
|
|
1344
|
+
"""
|
|
1345
|
+
|
|
1346
|
+
mpv_lib_version_, mpv_lib_file_path, mpv_api_version = mpv_lib_version()
|
|
1347
|
+
|
|
1348
|
+
system_info = (
|
|
1349
|
+
f"BORIS version: {version.__version__}\n"
|
|
1350
|
+
f"OS: {platform.uname().system} {platform.uname().release} {platform.uname().version}\n"
|
|
1351
|
+
f"CPU: {platform.uname().machine} {platform.uname().processor}\n"
|
|
1352
|
+
f"Python {platform.python_version()} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})\n"
|
|
1353
|
+
f"Qt {qVersion()} - PySide {pyside6_version}\n"
|
|
1354
|
+
f"MPV library version: {mpv_lib_version_}\n"
|
|
1355
|
+
f"MPV API version: {mpv_api_version}\n"
|
|
1356
|
+
f"MPV library file path: {mpv_lib_file_path}\n\n"
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
r, memory = mem_info()
|
|
1360
|
+
if not r:
|
|
1361
|
+
system_info += (
|
|
1362
|
+
f"Memory (RAM) Total: {memory.get('total_memory', 'Not available'):.2f} Mb "
|
|
1363
|
+
f"Free: {memory.get('free_memory', 'Not available'):.2f} Mb\n\n"
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
return system_info
|
|
1367
|
+
|
|
1368
|
+
"""
|
|
1369
|
+
# system info
|
|
1370
|
+
systeminfo = ""
|
|
1371
|
+
if sys.platform.startswith("win"):
|
|
1372
|
+
# systeminfo = subprocess.getoutput("systeminfo")
|
|
1373
|
+
systeminfo = subprocess.run("systeminfo /FO csv /NH", capture_output=True, text=True, encoding="mbcs", shell=True).stdout
|
|
1374
|
+
|
|
1375
|
+
import csv
|
|
1376
|
+
from io import StringIO
|
|
1377
|
+
|
|
1378
|
+
# Parse it as CSV
|
|
1379
|
+
f = StringIO(systeminfo)
|
|
1380
|
+
reader = csv.reader(f)
|
|
1381
|
+
parsed_data = list(reader)[0]
|
|
1382
|
+
# Print specific fields by index
|
|
1383
|
+
info_to_show = ""
|
|
1384
|
+
info_to_show += f"Computer Name: {parsed_data[0]}\n"
|
|
1385
|
+
info_to_show += f"OS Name: {parsed_data[1]}\n"
|
|
1386
|
+
info_to_show += f"OS Version: {parsed_data[2]}\n"
|
|
1387
|
+
info_to_show += f"System Manufacturer: {parsed_data[11]}\n"
|
|
1388
|
+
info_to_show += f"System Model: {parsed_data[12]}\n"
|
|
1389
|
+
info_to_show += f"Processor: {parsed_data[14]}\n"
|
|
1390
|
+
info_to_show += f"Locale: {parsed_data[19]}\n"
|
|
1391
|
+
info_to_show += f"Installed Memory: {parsed_data[22]}\n"
|
|
1392
|
+
|
|
1393
|
+
# info about graphic card
|
|
1394
|
+
graphic_info = subprocess.run(
|
|
1395
|
+
"wmic path win32_videocontroller get name", capture_output=True, text=True, encoding="mbcs", shell=True
|
|
1396
|
+
).stdout
|
|
1397
|
+
info_to_show += graphic_info.replace("\n", "").replace("Name", "Graphic card model")
|
|
1398
|
+
|
|
1399
|
+
systeminfo = info_to_show
|
|
1400
|
+
|
|
1401
|
+
if sys.platform.startswith("linux"):
|
|
1402
|
+
systeminfo = subprocess.getoutput("cat /etc/*rel*; uname -a")
|
|
1403
|
+
|
|
1404
|
+
return systeminfo
|
|
1405
|
+
"""
|
|
1406
|
+
|
|
1407
|
+
|
|
1101
1408
|
def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
1102
1409
|
"""
|
|
1103
1410
|
analyse video parameters with ffprobe (if available)
|
|
@@ -1109,28 +1416,32 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1109
1416
|
Returns:
|
|
1110
1417
|
dict
|
|
1111
1418
|
"""
|
|
1112
|
-
|
|
1419
|
+
|
|
1420
|
+
# check ffprobe executable in same place than ffmpeg
|
|
1113
1421
|
ffprobe_bin = ffmpeg_bin.replace("ffmpeg", "ffprobe")
|
|
1422
|
+
if not Path(ffprobe_bin).is_file():
|
|
1423
|
+
if which(ffprobe_bin) is None:
|
|
1424
|
+
return {"error": "ffprobe not found"}
|
|
1114
1425
|
|
|
1115
1426
|
command = f'"{ffprobe_bin}" -hide_banner -v error -print_format json -show_format -show_streams "{file_name}"'
|
|
1116
1427
|
|
|
1117
|
-
# print(command)
|
|
1118
|
-
|
|
1119
1428
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
1120
1429
|
out, error = p.communicate()
|
|
1121
1430
|
if error:
|
|
1122
|
-
|
|
1431
|
+
if b"invalid data" in error:
|
|
1432
|
+
return {"error": f"{error}"}
|
|
1123
1433
|
|
|
1124
1434
|
try:
|
|
1125
1435
|
hasVideo = False
|
|
1126
1436
|
hasAudio = False
|
|
1127
|
-
|
|
1437
|
+
"""bitrate = None"""
|
|
1128
1438
|
video_bitrate = None
|
|
1129
1439
|
audio_bitrate = []
|
|
1130
1440
|
resolution = None
|
|
1131
|
-
fps = 0
|
|
1441
|
+
fps: float = 0
|
|
1132
1442
|
sample_rate = None
|
|
1133
|
-
duration =
|
|
1443
|
+
duration = None
|
|
1444
|
+
audio_duration = cfg.NA
|
|
1134
1445
|
frames_number = None
|
|
1135
1446
|
size = None
|
|
1136
1447
|
audio_codec = None
|
|
@@ -1164,9 +1475,16 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1164
1475
|
fps = eval(stream["r_frame_rate"])
|
|
1165
1476
|
except Exception:
|
|
1166
1477
|
fps = 0
|
|
1478
|
+
if fps >= 1000 and "avg_frame_rate" in stream: # case for some h265 video ("r_frame_rate": "1200000/1")
|
|
1479
|
+
try:
|
|
1480
|
+
fps = eval(stream["avg_frame_rate"])
|
|
1481
|
+
except Exception:
|
|
1482
|
+
pass
|
|
1167
1483
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1484
|
+
fps = round(fps, 3)
|
|
1485
|
+
|
|
1486
|
+
if "duration" in stream:
|
|
1487
|
+
duration = float(stream["duration"])
|
|
1170
1488
|
if "duration_ts" in stream:
|
|
1171
1489
|
frames_number = int(stream["duration_ts"])
|
|
1172
1490
|
elif "nb_frames" in stream:
|
|
@@ -1178,11 +1496,19 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1178
1496
|
|
|
1179
1497
|
if stream["codec_type"] == "audio":
|
|
1180
1498
|
hasAudio = True
|
|
1181
|
-
sample_rate = float(stream["sample_rate"])
|
|
1182
|
-
|
|
1499
|
+
sample_rate = float(stream["sample_rate"]) if "sample_rate" in stream else cfg.NA
|
|
1500
|
+
# TODO manage audio_duration parameter
|
|
1501
|
+
audio_duration = float(stream["duration"]) if "duration" in stream else cfg.NA
|
|
1183
1502
|
audio_codec = stream["codec_long_name"]
|
|
1184
1503
|
audio_bitrate.append(int(stream.get("bit_rate", 0)))
|
|
1185
1504
|
|
|
1505
|
+
# check duration
|
|
1506
|
+
if duration is None:
|
|
1507
|
+
if "duration" in video_param["format"]:
|
|
1508
|
+
duration = float(video_param["format"]["duration"])
|
|
1509
|
+
else:
|
|
1510
|
+
duration = 0
|
|
1511
|
+
|
|
1186
1512
|
# check bit rate
|
|
1187
1513
|
if "bit_rate" in video_param["format"]:
|
|
1188
1514
|
all_bitrate = int(video_param["format"]["bit_rate"])
|
|
@@ -1192,11 +1518,22 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1192
1518
|
if video_bitrate is None and all_bitrate is not None:
|
|
1193
1519
|
video_bitrate = all_bitrate - sum(audio_bitrate)
|
|
1194
1520
|
|
|
1521
|
+
# extract format long name
|
|
1522
|
+
format_long_name = video_param["format"]["format_long_name"] if "format_long_name" in video_param["format"] else cfg.NA
|
|
1523
|
+
|
|
1524
|
+
# extract creation time ("creation_time": "2023-03-22T16:50:32.000000Z")
|
|
1525
|
+
creation_time = cfg.NA
|
|
1526
|
+
if "tags" in video_param["format"] and "creation_time" in video_param["format"]["tags"]:
|
|
1527
|
+
creation_time = video_param["format"]["tags"]["creation_time"].replace("T", " ")
|
|
1528
|
+
if "." in creation_time:
|
|
1529
|
+
creation_time = creation_time.split(".")[0]
|
|
1530
|
+
|
|
1195
1531
|
return {
|
|
1196
1532
|
"analysis_program": "ffprobe",
|
|
1197
1533
|
"frames_number": frames_number,
|
|
1198
1534
|
"duration_ms": duration * 1000,
|
|
1199
1535
|
"duration": duration,
|
|
1536
|
+
"audio_duration": audio_duration,
|
|
1200
1537
|
"fps": fps,
|
|
1201
1538
|
"has_video": hasVideo,
|
|
1202
1539
|
"has_audio": hasAudio,
|
|
@@ -1206,6 +1543,8 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1206
1543
|
"file size": size,
|
|
1207
1544
|
"audio_codec": audio_codec,
|
|
1208
1545
|
"video_codec": video_codec,
|
|
1546
|
+
"creation_time": creation_time,
|
|
1547
|
+
"format_long_name": format_long_name,
|
|
1209
1548
|
}
|
|
1210
1549
|
|
|
1211
1550
|
except Exception as e:
|
|
@@ -1214,7 +1553,7 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1214
1553
|
|
|
1215
1554
|
def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
1216
1555
|
"""
|
|
1217
|
-
analyse frame rate and video duration with ffmpeg
|
|
1556
|
+
analyse frame rate and video duration with ffprobe or ffmpeg if ffprobe not available
|
|
1218
1557
|
Returns parameters: duration, duration_ms, bitrate, frames_number, fps, has_video (True/False), has_audio (True/False)
|
|
1219
1558
|
|
|
1220
1559
|
Args:
|
|
@@ -1228,13 +1567,14 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1228
1567
|
|
|
1229
1568
|
ffprobe_results = ffprobe_media_analysis(ffmpeg_bin, file_name)
|
|
1230
1569
|
|
|
1570
|
+
logger.debug(f"file_name: {file_name}")
|
|
1571
|
+
logger.debug(f"ffprobe_results: {ffprobe_results}")
|
|
1572
|
+
|
|
1231
1573
|
if ("error" not in ffprobe_results) and (ffprobe_results["bitrate"] is not None):
|
|
1232
1574
|
return ffprobe_results
|
|
1233
1575
|
else:
|
|
1234
1576
|
# use ffmpeg
|
|
1235
|
-
command = (
|
|
1236
|
-
f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
|
|
1237
|
-
)
|
|
1577
|
+
command = f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
|
|
1238
1578
|
|
|
1239
1579
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
1240
1580
|
|
|
@@ -1292,7 +1632,7 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1292
1632
|
if b"Stream #" in row and b"Video:" in row:
|
|
1293
1633
|
hasVideo = True
|
|
1294
1634
|
# get resolution \d{3,5}x\d{3,5}
|
|
1295
|
-
re_results = re.search(
|
|
1635
|
+
re_results = re.search(r"\d{3,5}x\d{3,5}", row, re.IGNORECASE)
|
|
1296
1636
|
if re_results:
|
|
1297
1637
|
resolution = re_results.group(0).decode("utf-8")
|
|
1298
1638
|
break
|
|
@@ -1317,11 +1657,13 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
|
|
|
1317
1657
|
"frames_number": int(fps * duration),
|
|
1318
1658
|
"duration_ms": duration * 1000,
|
|
1319
1659
|
"duration": duration,
|
|
1660
|
+
"audio_duration": cfg.NA,
|
|
1320
1661
|
"fps": fps,
|
|
1321
1662
|
"has_video": hasVideo,
|
|
1322
1663
|
"has_audio": hasAudio,
|
|
1323
1664
|
"bitrate": bitrate,
|
|
1324
1665
|
"resolution": resolution,
|
|
1666
|
+
"format_long_name": "",
|
|
1325
1667
|
}
|
|
1326
1668
|
|
|
1327
1669
|
|
|
@@ -1362,12 +1704,37 @@ def all_behaviors(ethogram: dict) -> list:
|
|
|
1362
1704
|
return [ethogram[x][cfg.BEHAVIOR_CODE] for x in sorted_keys(ethogram)]
|
|
1363
1705
|
|
|
1364
1706
|
|
|
1707
|
+
def all_subjects(subjects: dict) -> list:
|
|
1708
|
+
"""
|
|
1709
|
+
extract all subjects from the subject configuration dictionary
|
|
1710
|
+
|
|
1711
|
+
Args:
|
|
1712
|
+
subject configuration (dict)
|
|
1713
|
+
|
|
1714
|
+
Returns:
|
|
1715
|
+
list: subjects name
|
|
1716
|
+
"""
|
|
1717
|
+
|
|
1718
|
+
return [subjects[x][cfg.SUBJECT_NAME] for x in sorted_keys(subjects)]
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
def has_coding_map(ethogram: dict, behavior_idx: str) -> bool:
|
|
1722
|
+
"""
|
|
1723
|
+
check if behavior index has a coding map
|
|
1724
|
+
"""
|
|
1725
|
+
if not ethogram.get(behavior_idx, False):
|
|
1726
|
+
return False
|
|
1727
|
+
if not ethogram[behavior_idx].get("coding map", False):
|
|
1728
|
+
return False
|
|
1729
|
+
return True
|
|
1730
|
+
|
|
1731
|
+
|
|
1365
1732
|
def dir_images_number(dir_path_str: str) -> dict:
|
|
1366
1733
|
"""
|
|
1367
1734
|
return number of images in dir_path (see cfg.IMAGE_EXTENSIONS)
|
|
1368
1735
|
"""
|
|
1369
1736
|
|
|
1370
|
-
dir_path =
|
|
1737
|
+
dir_path = Path(dir_path_str)
|
|
1371
1738
|
if not dir_path.is_dir():
|
|
1372
1739
|
return {"error": f"The directory {dir_path_str} does not exists"}
|
|
1373
1740
|
img_count = 0
|
|
@@ -1405,29 +1772,11 @@ def intersection(A, B, C, D):
|
|
|
1405
1772
|
ym = slope * xm + intersept
|
|
1406
1773
|
else:
|
|
1407
1774
|
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
|
-
)
|
|
1775
|
+
(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
1776
|
/ (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
|
|
1419
1777
|
).quantize(dec(".001"), rounding=ROUND_DOWN)
|
|
1420
1778
|
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
|
-
)
|
|
1779
|
+
(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
1780
|
/ (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
|
|
1432
1781
|
).quantize(dec(".001"), rounding=ROUND_DOWN)
|
|
1433
1782
|
|
|
@@ -1436,16 +1785,7 @@ def intersection(A, B, C, D):
|
|
|
1436
1785
|
ymin1, ymax1 = min(ya, yb), max(ya, yb)
|
|
1437
1786
|
ymin2, ymax2 = min(yc, yd), max(yc, yd)
|
|
1438
1787
|
|
|
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
|
-
)
|
|
1788
|
+
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
1789
|
|
|
1450
1790
|
except Exception: # for cases xa=xb=xc=xd
|
|
1451
1791
|
return True
|