From c759554759b76dee7eb4a7fbfe7b80bc9206d075 Mon Sep 17 00:00:00 2001 From: Terry Fu Date: Sun, 4 Aug 2024 15:24:27 +0800 Subject: [PATCH] Add a New Executable Module `leads_vec_dp` (#344) * Optimized dependency profiles. (#340) * Added `leads_vec_dp`. (#340) * Docs. (#340) * Improved workflow. (#340) * Updated pyproject.toml. (#340) * Code reformatted. (#340) * Code reformatted. (#340) * Added dependency `pyyaml`. (#340) * Docs. (#340) * Supported inferences. (#340) * Added an output for `InferredDataset.complete()`. (#340) * Code reformatted. (#340) * Added `latency_invalid()`. (#340) * Bug fixed: new latency is negative. (#340) * Code reformatted. (#340) * Code reformatted. (#340) * Bug fixed: avoided `nan`s. (#340) * Bug fixed: avoided `nan`s. (#340) * Supported visual data analysis. (#340) * Added commands. (#340) * Code reformatted. (#340) * Docs. (#340) * Added command `save-as`. (#340) * Updated LEADS.pptx. (#316) (#340) --------- Signed-off-by: Terry Fu --- README.md | 96 +++++++++++++++---- docs/LEADS.pptx | Bin 270264 -> 271746 bytes leads/data_persistence/analyzer/inference.py | 13 ++- leads/data_persistence/analyzer/processor.py | 23 ++++- leads/data_persistence/analyzer/utils.py | 14 ++- leads/data_persistence/core.py | 4 +- leads/dt/registry.py | 4 +- leads_vec/cli.py | 4 +- leads_vec/run.py | 2 + leads_vec_dp/__entry__.py | 13 +++ leads_vec_dp/__init__.py | 7 ++ leads_vec_dp/__main__.py | 4 + leads_vec_dp/run.py | 79 +++++++++++++++ leads_vec_rc/__entry__.py | 2 +- leads_video/types.py | 3 - leads_video/utils.py | 8 +- pyproject.toml | 22 ++--- 17 files changed, 242 insertions(+), 56 deletions(-) create mode 100644 leads_vec_dp/__entry__.py create mode 100644 leads_vec_dp/__init__.py create mode 100644 leads_vec_dp/__main__.py create mode 100644 leads_vec_dp/run.py delete mode 100644 leads_video/types.py diff --git a/README.md b/README.md index 880b7758..f7ae8a4e 100644 --- a/README.md +++ b/README.md @@ -189,23 +189,23 @@ command, see [Environment Setup](#environment-setup). pip install "leads[standard]" ``` -If your platform does not support GPIO, use profile "no-gpio". - -```shell -pip install "leads[no-gpio]" -``` - If you only want the framework, run the following. ```shell pip install leads ``` -#### Verify +This table lists all installation profiles. -```shell -leads-vec info -``` +| Profile | Content | For | All Platforms | +|----------------------|-----------------------------------------------------------------|--------------------------------------------------|---------------| +| leads | Only the framework | LEADS Framework | ✓ | +| "leads[standard]" | The framework and necessary dependencies | LEADS Framework | ✓ | +| "leads[gpio]" | Everything "leads[standard]" has plug `lgpio` | LEADS Framework | ✗ | +| "leads[vec]" | Everything "leads[gpio]" has plus `pynput` | LEADS VeC | ✗ | +| "leads[vec-no-gpio]" | Everything "leads[standard]" has plus `pynput` | LEADS VeC (if you are not using any GPIO device) | ✓ | +| "leads[vec-rc]" | Everything "leads[standard]" has plus `"fastapi[standard]` | LEADS VeC Remote Analyst | ✓ | +| "leads[vec-dp]" | Everything "leads[standard]" has plus `matplotlib` and `pyyaml` | LEADS VeC Data Processor | ✓ | ### Arduino @@ -225,6 +225,12 @@ the framework in your project. leads-vec run ``` +#### Verify + +```shell +leads-vec info +``` + #### Replay ```shell @@ -356,12 +362,6 @@ automatically calculate the best factor to keep the original proportion as desig ### Remote Analyst -The remote analyst requires additional dependencies. Install them through the following command. - -```shell -pip install "leads[all]" -``` - ```shell leads-vec-rc ``` @@ -394,6 +394,14 @@ If not specified, all configurations will be default values. To learn about the configuration file, read [Configurations](#configurations). +### Data Processor + +```shell +leads-vec-dp path/to/the/workflow.yml +``` + +To learn more about workflows, read [Workflows](#workflows). + ## Environment Setup This section helps you set up the identical environment we have for the VeC project. A more detailed guide of @@ -497,6 +505,62 @@ Note that a purely empty file could cause an error. | `data_dir` | `str` | Directory for the data recording system | Remote | `"data"` | | `save_data` | `bool` | `True`: save data; `False`: discard data | Remote | `False` | +## Workflows + +This only applies to LEADS VeC Data Processor. Please find a more detailed version +[here](https://leads-docs.projectneura.org/en/latest/vec/index.html#workflows). + +```yaml +dataset: "data/main.csv" +inferences: + repeat: 100 # default: 1 + enhanced: true # default: false + assume_initial_zeros: true # default: false + methods: + - safe-speed + - speed-by-acceleration + - speed-by-mileage + - speed-by-gps-ground-speed + - speed-by-gps-position + - forward-acceleration-by-speed + - milage-by-speed + - milage-by-gps-position + - visual-data-realignment-by-latency + +jobs: + - name: Task 1 + uses: bake + - name: Task 2 + uses: process + with: + lap_time_assertions: # default: [] + - 120 # lap 1 duration (seconds) + - 180 # lap 2 duration (seconds) + vehicle_hit_box: 5 # default: 3 + min_lap_time: 60 # default: 30 (seconds) + - name: Draw Lap 5 + uses: draw-lap + with: + lap_index: 4 # default: -1 + - name: Suggest on Lap 5 + uses: suggest-on-lap + with: + lap_index: 4 + - name: Draw Comparison of Laps + uses: draw-comparison-of-laps + with: + width: 0.5 # default: 0.3 + - name: Extract Video + uses: extract-video + with: + file: rear-view.mp4 # destination to save the video + tag: rear # front, left, right, or rear + - name: Save + uses: save-as + with: + file: data/new.csv +``` + ## Devices Module ### Example diff --git a/docs/LEADS.pptx b/docs/LEADS.pptx index 27bdf8910b9681452b1d55ff7359a665db045899..77d8f7e318627f7ffbef1f5fe717ab5c88dd9457 100644 GIT binary patch delta 26082 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 g4Z?r`El>yv;BXIujjw^GfmTnBhPHu)3Y0z>% diff --git a/leads/data_persistence/analyzer/inference.py b/leads/data_persistence/analyzer/inference.py index 0fce4368..0c474ca2 100644 --- a/leads/data_persistence/analyzer/inference.py +++ b/leads/data_persistence/analyzer/inference.py @@ -245,7 +245,7 @@ def complete(self, *rows: dict[str, _Any], backward: bool = False) -> dict[str, original_target = target.copy() t_0, t = target["t"], base["t"] for channel in self._channels: - if (new_latency := t_0 - t + base[f"{channel}_view_latency"]) > 0: + if (new_latency := t - t_0 - base[f"{channel}_view_latency"]) < 0: continue target[f"{channel}_view_base64"] = base[f"{channel}_view_base64"] target[f"{channel}_view_latency"] = new_latency @@ -272,8 +272,9 @@ def merge(raw: dict[str, _Any], inferred: dict[str, _Any]) -> None: for key in inferred.keys(): raw[key] = inferred[key] - def _complete(self, inferences: tuple[Inference, ...], enhanced: bool, backward: bool) -> None: + def _complete(self, inferences: tuple[Inference, ...], enhanced: bool, backward: bool) -> int: num_rows = len(self._raw_data) + num_affected_rows = 0 for i in range(num_rows - 1, -1, -1) if backward else range(num_rows): for inference in inferences: p, f = inference.depth() @@ -287,7 +288,9 @@ def _complete(self, inferences: tuple[Inference, ...], enhanced: bool, backward: InferredDataset.merge(row, self._inferred_data[j]) d.append(row) if (r := inference.complete(*d, backward=backward)) is not None: + num_affected_rows += 1 InferredDataset.merge(self._inferred_data[i], r) + return num_affected_rows @_override def load(self) -> None: @@ -311,20 +314,20 @@ def assume_initial_zeros(self) -> None: injection["mileage"] = 0 InferredDataset.merge(row, injection) - def complete(self, *inferences: Inference, enhanced: bool = False, assume_initial_zeros: bool = False) -> None: + def complete(self, *inferences: Inference, enhanced: bool = False, assume_initial_zeros: bool = False) -> int: """ Infer the missing values in the dataset. :param inferences: the inferences to apply :param enhanced: True: use inferred data to infer other data; False: use only raw data to infer other data :param assume_initial_zeros: True: reasonably set any missing data in the first row to zero; False: no change + :return: the number of affected rows """ for inference in inferences: if not set(rh := inference.header()).issubset(ah := self.read_header()): raise KeyError(f"Inference {inference} requires header {rh} but the dataset only contains {ah}") if assume_initial_zeros: self.assume_initial_zeros() - self._complete(inferences, enhanced, False) - self._complete(inferences, enhanced, True) + return self._complete(inferences, enhanced, False) + self._complete(inferences, enhanced, True) @_override def __iter__(self) -> _Generator[dict[str, _Any], None, None]: diff --git a/leads/data_persistence/analyzer/processor.py b/leads/data_persistence/analyzer/processor.py index 348ad446..c8370910 100644 --- a/leads/data_persistence/analyzer/processor.py +++ b/leads/data_persistence/analyzer/processor.py @@ -10,7 +10,7 @@ from leads.data import dlat2meters, dlon2meters, format_duration from leads.data_persistence.analyzer.utils import time_invalid, speed_invalid, mileage_invalid, latitude_invalid, \ - longitude_invalid + longitude_invalid, latency_invalid from leads.data_persistence.core import CSVDataset, DEFAULT_HEADER from .._computational import sqrt as _sqrt @@ -38,6 +38,9 @@ def __init__(self, dataset: CSVDataset) -> None: self._gps_invalid_rows: list[int] = [] self._min_lat: float | None = None self._min_lon: float | None = None + # visual + self._min_latency: float | None = None + self._max_latency: float | None = None # process variables self._laps: list[tuple[int, int, int, float, float]] = [] @@ -69,8 +72,7 @@ def unit(row: dict[str, _Any], i: int) -> None: t = int(row["t"]) speed = row["speed"] mileage = row["mileage"] - if time_invalid(t) or speed_invalid( - speed) or mileage_invalid(mileage): + if time_invalid(t) or speed_invalid(speed) or mileage_invalid(mileage): self._invalid_rows.append(i) return if self._start_time is None: @@ -94,6 +96,15 @@ def unit(row: dict[str, _Any], i: int) -> None: self._min_lon = lon self._gps_valid_count += 1 self._valid_rows_count += 1 + # visual + latencies = [row[key] for key in ("front_view_latency", "left_view_latency", "right_view_latency", + "rear_view_latency") if key in row.keys()] + latency = min(latencies) + if not latency_invalid(latency) and (self._min_latency is None or latency < self._min_latency): + self._min_latency = latency + latency = max(latencies) + if not latency_invalid(latency) and (self._max_latency is None or latency > self._max_latency): + self._max_latency = latency self.foreach(unit, False) if self._valid_rows_count == 0: @@ -107,7 +118,7 @@ def _hide_others(seq: _Sequence[_Any], limit: int) -> str: return f"[{", ".join(map(str, seq[:limit]))}, and {diff} others]" if (diff := len(seq) - limit) > 0 else str( seq) - def baking_results(self) -> tuple[str, str, str, str, str, str, str, str, str, str, str, str]: + def baking_results(self) -> tuple[str, str, str, str, str, str, str, str, str, str, str, str, str, str]: """ Get the results of the baking process. :return: the results in sentences @@ -129,7 +140,9 @@ def baking_results(self) -> tuple[str, str, str, str, str, str, str, str, str, s f"v\u2098\u2090\u2093: {self._max_speed:.2f} KM / H", f"v\u2090\u1D65\u1D4D: {self._avg_speed:.2f} KM / H", f"GPS Hit Rate: {100 * self._gps_valid_count / self._valid_rows_count:.2f}%", - f"GPS Skipped Rows: {Processor._hide_others(self._gps_invalid_rows, 5)}" + f"GPS Skipped Rows: {Processor._hide_others(self._gps_invalid_rows, 5)}", + "Min Video Latency: N/A" if self._min_latency is None else f"Min Video Latency: {self._min_latency:.2f} MS", + "Max Video Latency: N/A" if self._max_latency is None else f"Max Video Latency: {self._max_latency:.2f} MS" ) def erase_unit_cache(self) -> None: diff --git a/leads/data_persistence/analyzer/utils.py b/leads/data_persistence/analyzer/utils.py index 2277a9c2..a8e6fee9 100644 --- a/leads/data_persistence/analyzer/utils.py +++ b/leads/data_persistence/analyzer/utils.py @@ -9,23 +9,27 @@ def time_invalid(o: _Any) -> bool: def speed_invalid(o: _Any) -> bool: - return not isinstance(o, int | float) or o != o or o < 0 + return not isinstance(o, int | float) or o < 0 def acceleration_invalid(o: _Any) -> bool: - return not isinstance(o, int | float) or o != o + return not isinstance(o, int | float) def mileage_invalid(o: _Any) -> bool: - return not isinstance(o, int | float) or o != o + return not isinstance(o, int | float) def latitude_invalid(o: _Any) -> bool: - return not isinstance(o, int | float) or o != o or not -90 < o < 90 + return not isinstance(o, int | float) or not -90 < o < 90 def longitude_invalid(o: _Any) -> bool: - return not isinstance(o, int | float) or o != o or not -180 < o < 180 + return not isinstance(o, int | float) or not -180 < o < 180 + + +def latency_invalid(o: _Any) -> bool: + return not isinstance(o, int | float) def distance_between(lat_0: float, lon_0: float, lat: float, lon: float) -> float: diff --git a/leads/data_persistence/core.py b/leads/data_persistence/core.py index 236a39b5..7fe5f308 100644 --- a/leads/data_persistence/core.py +++ b/leads/data_persistence/core.py @@ -4,6 +4,8 @@ override as _override, Self as _Self, Iterator as _Iterator, Callable as _Callable, Iterable as _Iterable, \ Generator as _Generator, Any as _Any +from numpy import nan as _nan + from leads.types import Compressor as _Compressor, VisualHeader as _VisualHeader, VisualHeaderFull as _VisualHeaderFull from ._computational import mean as _mean, array as _array, norm as _norm, read_csv as _read_csv, \ DataFrame as _DataFrame, TextFileReader as _TextFileReader @@ -218,7 +220,7 @@ def __iter__(self) -> _Generator[dict[str, _Any], None, None]: except StopIteration: break for i in range(len(chunk)): - r = chunk.iloc[i].to_dict() + r = chunk.iloc[i].replace(_nan, None).to_dict() if self._contains_index: r.pop("index") yield r diff --git a/leads/dt/registry.py b/leads/dt/registry.py index 36cdd043..9603e097 100644 --- a/leads/dt/registry.py +++ b/leads/dt/registry.py @@ -61,7 +61,7 @@ def register_controller(tag: str, c: Controller, parent: str | None = None) -> N def has_controller(tag: str) -> bool: - return tag in _controllers.keys() + return tag in _controllers def get_controller(tag: str) -> Controller: @@ -79,7 +79,7 @@ def _register_device(prototype: type[Device], def has_device(tag: str) -> bool: - return tag in _devices.keys() + return tag in _devices def get_device(tag: str) -> Device: diff --git a/leads_vec/cli.py b/leads_vec/cli.py index 7c049816..de2c4f26 100644 --- a/leads_vec/cli.py +++ b/leads_vec/cli.py @@ -214,9 +214,9 @@ def render(manager: ContextManager) -> None: if cfg.comm_stream: manager["comm_stream_status"] = _Label(root, text="STM OFFLINE", text_color="gray", font=("Arial", cfg.font_size_small)) - i = 0 + j = 0 for system in SystemLiteral: - i += 1 + j += 1 system_lower = system.lower() manager[f"{system_lower}_status"] = _Label(root, text=f"{system} READY", text_color="green", font=("Arial", cfg.font_size_small)) diff --git a/leads_vec/run.py b/leads_vec/run.py index 48ebc1f1..5689bcaa 100644 --- a/leads_vec/run.py +++ b/leads_vec/run.py @@ -17,6 +17,7 @@ def run(config: str | None, devices: str, main: str, register: _Literal["systemd _create_service() _L.debug("Service registered") _L.debug(f"Service script is located at \"{_abspath(__file__)[:-6]}_bootloader/leads-vec.service.sh\"") + return 0 case "config": if _exists("config.json"): r = input("\"config.json\" already exists. Overwrite? (Y/n) >>>").lower() @@ -26,6 +27,7 @@ def run(config: str | None, devices: str, main: str, register: _Literal["systemd with open("config.json", "w") as f: f.write(str(Config({}))) _L.debug("Configuration file saved to \"config.json\"") + return 0 case "reverse_proxy": from ._bootloader import start_frpc as _start_frpc diff --git a/leads_vec_dp/__entry__.py b/leads_vec_dp/__entry__.py new file mode 100644 index 00000000..085666a1 --- /dev/null +++ b/leads_vec_dp/__entry__.py @@ -0,0 +1,13 @@ +from argparse import ArgumentParser as _ArgumentParser +from sys import exit as _exit + +from leads_vec_dp.run import run + + +def __entry__() -> None: + parser = _ArgumentParser(prog="LEADS VeC DP", + description="Lightweight Embedded Assisted Driving System VeC Data Processor", + epilog="GitHub: https://github.com/ProjectNeura/LEADS") + parser.add_argument("workflow", help="specify a workflow file") + args = parser.parse_args() + _exit(run(args.workflow)) diff --git a/leads_vec_dp/__init__.py b/leads_vec_dp/__init__.py new file mode 100644 index 00000000..8a221d84 --- /dev/null +++ b/leads_vec_dp/__init__.py @@ -0,0 +1,7 @@ +from importlib.util import find_spec as _find_spec + +if not _find_spec("yaml"): + raise ImportError("Please install `pyyaml` to run this module\n>>>pip install pyyaml") + +from leads_vec_dp.__entry__ import __entry__ +from leads_vec_dp.run import * diff --git a/leads_vec_dp/__main__.py b/leads_vec_dp/__main__.py new file mode 100644 index 00000000..cf452e20 --- /dev/null +++ b/leads_vec_dp/__main__.py @@ -0,0 +1,4 @@ +from leads_vec_dp.__entry__ import __entry__ + +if __name__ == "__main__": + __entry__() diff --git a/leads_vec_dp/run.py b/leads_vec_dp/run.py new file mode 100644 index 00000000..934c7285 --- /dev/null +++ b/leads_vec_dp/run.py @@ -0,0 +1,79 @@ +from atexit import register as _register +from typing import Any as _Any + +from yaml import load as _load, SafeLoader as _SafeLoader + +from leads import L as _L +from leads.data_persistence import CSVDataset as _CSVDataset +from leads.data_persistence.analyzer import InferredDataset as _InferredDataset, Inference as _Inference, \ + SafeSpeedInference as _SafeSpeedInference, SpeedInferenceByAcceleration as _SpeedInferenceByAcceleration, \ + SpeedInferenceByMileage as _SpeedInferenceByMileage, \ + SpeedInferenceByGPSGroundSpeed as _SpeedInferenceByGPSGroundSpeed, \ + SpeedInferenceByGPSPosition as _SpeedInferenceByGPSPosition, \ + ForwardAccelerationInferenceBySpeed as _ForwardAccelerationInferenceBySpeed, \ + MileageInferenceBySpeed as _MileageInferenceBySpeed, \ + MileageInferenceByGPSPosition as _MileageInferenceByGPSPosition, \ + VisualDataRealignmentByLatency as _VisualDataRealignmentByLatency +from leads.data_persistence.analyzer.processor import Processor as _Processor +from leads_video import extract_video as _extract_video + +INFERENCE_METHODS: dict[str, type[_Inference]] = { + "safe-speed": _SafeSpeedInference, + "speed-by-acceleration": _SpeedInferenceByAcceleration, + "speed-by-mileage": _SpeedInferenceByMileage, + "speed-by-gps-ground-speed": _SpeedInferenceByGPSGroundSpeed, + "speed-by-gps-position": _SpeedInferenceByGPSPosition, + "forward-acceleration-by-speed": _ForwardAccelerationInferenceBySpeed, + "milage-by-speed": _MileageInferenceBySpeed, + "milage-by-gps-position": _MileageInferenceByGPSPosition, + "visual-data-realignment-by-latency": _VisualDataRealignmentByLatency +} + + +def _optional_kwargs(source: dict[str, _Any], key: str) -> dict[str, _Any]: + return source[key] if key in source else {} + + +def run(target: str) -> int: + with open(target) as f: + target = _load(f.read(), _SafeLoader) + if "inferences" in target: + dataset = _InferredDataset(target["dataset"]) + inferences = target["inferences"] + methods = [] + for method in inferences["methods"]: + methods.append(INFERENCE_METHODS[method]()) + inferences.pop("methods") + repeat = 1 + if "repeat" in inferences: + repeat = inferences["repeat"] + inferences.pop("repeat") + for _ in range(repeat): + _L.info(f"Affected {(n := dataset.complete(*methods, **inferences))} row{"s" if n > 1 else ""}") + else: + dataset = _CSVDataset(target["dataset"]) + _register(dataset.close) + processor = _Processor(dataset) + for job in target["jobs"]: + _L.info(f"Executing job {job["name"]}...") + match job["uses"]: + case "bake": + processor.bake() + _L.info("Baking Results", *processor.baking_results(), sep="\n") + case "process": + processor.process(**_optional_kwargs(job, "with")) + _L.info("Results", *processor.results(), sep="\n") + case "draw-lap": + processor.draw_lap(**_optional_kwargs(job, "with")) + case "suggest-on-lap": + _L.info(*processor.suggest_on_lap(job["with"]["lap_index"]), sep="\n") + case "draw-comparison-of-laps": + processor.draw_comparison_of_laps(**_optional_kwargs(job, "with")) + case "extract-video": + _extract_video(dataset, file := job["with"]["file"], job["with"]["tag"]) + _L.info(f"Video saved as {file}") + case "save-as": + dataset.save(file := job["with"]["file"]) + _L.info(f"Dataset saved as {file}") + + return 0 diff --git a/leads_vec_rc/__entry__.py b/leads_vec_rc/__entry__.py index a1908fe0..abe757d4 100644 --- a/leads_vec_rc/__entry__.py +++ b/leads_vec_rc/__entry__.py @@ -16,4 +16,4 @@ def __entry__() -> None: _register_config(_load_config(args.config, Config) if args.config else Config({})) from leads_vec_rc.cli import app - _run(app, host="0.0.0.0", port=args.port, log_level="warning") \ No newline at end of file + _run(app, host="0.0.0.0", port=args.port, log_level="warning") diff --git a/leads_video/types.py b/leads_video/types.py deleted file mode 100644 index 6e29066c..00000000 --- a/leads_video/types.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Literal as _Literal - -type VideoTag = _Literal["front_view_base64", "left_view_base64", "right_view_base64", "rear_view_base64"] diff --git a/leads_video/utils.py b/leads_video/utils.py index 3948212e..3395c9c3 100644 --- a/leads_video/utils.py +++ b/leads_video/utils.py @@ -1,7 +1,7 @@ from base64 import b64decode as _b64decode from binascii import Error as _BinasciiError from io import BytesIO as _BytesIO -from typing import Any as _Any +from typing import Any as _Any, Literal as _Literal from PIL.Image import Image as _Image, open as _open, UnidentifiedImageError as _UnidentifiedImageError from cv2 import VideoWriter as _VideoWriter, VideoWriter_fourcc as _VideoWriter_fourcc, cvtColor as _cvtColor, \ @@ -11,7 +11,6 @@ from leads import has_device as _has_device, get_device as _get_device from leads.data_persistence import CSVDataset as _CSVDataset from leads_video.camera import Camera -from leads_video.types import VideoTag as _VideoTag def get_camera(tag: str, required_type: type[Camera] = Camera) -> Camera | None: @@ -23,15 +22,16 @@ def get_camera(tag: str, required_type: type[Camera] = Camera) -> Camera | None: return cam -def _decode_frame(row: dict[str, _Any], tag: _VideoTag) -> _Image: +def _decode_frame(row: dict[str, _Any], tag: str) -> _Image: if not (frame := row[tag]): raise ValueError return _open(_BytesIO(_b64decode(frame))) -def extract_video(file: str, dataset: _CSVDataset, tag: _VideoTag) -> None: +def extract_video(dataset: _CSVDataset, file: str, tag: _Literal["front", "left", "right", "rear"]) -> None: if not file.endswith(".mp4"): file += ".mp4" + tag = f"{tag}_view_base64" prev_row = None resolution = None fps = 0 diff --git a/pyproject.toml b/pyproject.toml index f16ad5b5..168fee8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,25 +16,22 @@ dependencies = ["numpy", "pandas"] [project.optional-dependencies] standard = [ - "Pillow", "PySDL2", "customtkinter", "gpiozero", "lgpio", "opencv-python-headless", "pynmea2", "pynput", - "pysdl2-dll", "pyserial", "screeninfo" -] -no-gpio = [ - "Pillow", "PySDL2", "customtkinter", "gpiozero", "opencv-python-headless", "pynmea2", "pynput", "pysdl2-dll", - "pyserial", "screeninfo" -] -all = [ - "Pillow", "PySDL2", "customtkinter", "fastapi[standard]", "gpiozero", "lgpio", "opencv-python-headless", "pynmea2", - "pynput", "pysdl2-dll", "pyserial", "screeninfo" + "Pillow", "PySDL2", "customtkinter", "gpiozero", "opencv-python-headless", "pynmea2", "pysdl2-dll", "pyserial", + "screeninfo" ] +gpio = ["leads[standard]", "lgpio"] +vec = ["leads[gpio]", "pynput"] +vec-no-gpio = ["leads[standard]", "pynput"] +vec-rc = ["leads[standard]", "fastapi[standard]"] +vec-dp = ["leads[standard]", "matplotlib", "pyyaml"] [tool.hatch.build.targets.sdist] only-include = ["leads", "leads_arduino", "leads_audio", "leads_can", "leads_comm_serial", "leads_emulation", - "leads_gpio", "leads_gui", "leads_video", "leads_vec", "leads_vec_rc", "design", "docs"] + "leads_gpio", "leads_gui", "leads_video", "leads_vec", "leads_vec_rc", "leads_vec_dp", "design", "docs"] [tool.hatch.build.targets.wheel] packages = ["leads", "leads_arduino", "leads_audio", "leads_can", "leads_comm_serial", "leads_emulation", "leads_gpio", - "leads_gui", "leads_video", "leads_vec", "leads_vec_rc"] + "leads_gui", "leads_video", "leads_vec", "leads_vec_rc", "leads_vec_dp"] [project.urls] Homepage = "https://leads.projectneura.org" @@ -43,6 +40,7 @@ Repository = "https://github.com/ProjectNeura/LEADS" [project.scripts] leads-vec-rc = "leads_vec_rc:__entry__" +leads-vec-dp = "leads_vec_dp:__entry__" [project.gui-scripts] leads-vec = "leads_vec:__entry__"