diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..d7618774ee543ce634b879238ef4765fd70b4680 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.deb filter=lfs diff=lfs merge=lfs -text diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d8c88a3df4116b7e552b183562ecc58582bea378 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 +FROM python:3.11 + +RUN apt-get update && apt-get install -y ffmpeg libopenblas-dev libopencv-dev qt6-base-dev qt6-multimedia-dev libssl-dev mesa-opencl-icd libboost-all-dev libturbojpeg0-dev ocl-icd-opencl-dev clinfo ocl-icd-libopencl1 && rm -rf /var/lib/apt/lists + +ARG HF_TOKEN="" + +RUN pip install --no-cache-dir huggingface-hub +COPY install_eve.py /tmp/install_eve.py +RUN --mount=type=secret,id=MODEL_ACCESS_TOKEN \ + HF_TOKEN="$HF_TOKEN" python /tmp/install_eve.py && rm /tmp/install_eve.py + +RUN useradd -m -u 1000 user +USER user +ENV PATH="/home/user/.local/bin:$PATH" + +WORKDIR /app + +COPY --chown=user ./requirements.txt requirements.txt +RUN pip install --no-cache-dir --upgrade -r requirements.txt + +RUN mkdir -p /home/user/.config + +COPY --chown=user . /app +CMD ["python", "app.py"] diff --git a/README.md b/README.md index 47f20f072508ea3d560adac95e3e0736f9478e51..c2b2da4a38c063aeaa8f518e4d8dba634d231bc4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,65 @@ ---- -title: SensAI Generic Object Detection -emoji: 💻 -colorFrom: pink -colorTo: purple -sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +--- +title: Lattice sensAI Generic Object Detection +slug: sensAI-Generic-Object-Detection +short_description: Object detection — GMOD-80 / AMOD-8 / OMOD on the EVE SDK +emoji: 📦 +colorFrom: blue +colorTo: indigo +sdk: docker +pinned: false +tags: + - computer-vision + - real-time + - edge-ai + - on-device + - embedded + - low-power + - object-detection + - gmod + - amod + - omod + - npu + - soc +--- + +The Lattice sensAI Generic Object Detection demo runs the EVE SDK's MOD +(Multi-Object Detection) subsystem against a frame from an uploaded image, +an uploaded video, or a live webcam feed. Pick exactly one of three models: + +| Model | Description | +| --- | --- | +| **GMOD-80** | Generic 80-class object detector (active baseline) | +| **AMOD-8** | 8-class automotive object detector — person, bicycle, car, motorcycle, bus, truck, traffic light, stop sign *(preview — placeholder)* | +| **OMOD** | Many-class office-objects detector *(preview — placeholder)* | + +> **Preview note.** AMOD and OMOD currently fall back to GMOD-80 weights — +> the EVE C SDK ships a single hardcoded MOD model and does not yet expose a +> runtime model-switch entry point. Once the SDK gains +> `EveLoadObjectDetectionModel(...)` (or equivalent), the placeholders go +> live without any UI changes — only the EVE wrapper and +> `download_models.py` need updates. + +To preview the demo, use the tabs: + +- Live Inference — webcam → annotated frames in real time. +- Offline Inference — upload an image **or** a video; mutually + exclusive accordions keep the UI focused on one input at a time. + +> Note: For demo purposes, the AI pipeline and image-draw operations run on +> a Hugging Face CPU server. Performance varies with concurrent users. + +For SDK access, fill the form on +[this page](https://huggingface.co/LatticeSemi/sensAI-Edge-Vision-Engine-SDK-Packages) +and follow the download instructions. Support: evehelp@latticesemi.com. + +## About + +This demo is maintained by **Lattice Semiconductor** (LatticeSemi). + +## License + +Proprietary - Lattice Semiconductor Corporation. All rights reserved. + +## Need a Longer or Commercial License? + +- **Email**: evehelp@latticesemi.com diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..bc23145fd7a170300275b9cb50c449139fdba7c9 --- /dev/null +++ b/app.py @@ -0,0 +1,335 @@ +import atexit +import os +import sys +from pathlib import Path + +import gradio as gr + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "shared")) +sys.path.insert(0, str(Path(__file__).resolve().parent / "shared")) + +from env_utils import load_dotenv_if_present, require_secrets +from eula_tab import build_eula_tab +from eve_app_tabs import build_image_or_video_offline_tab, build_live_inference_tab +from eve_inference_handlers import EveAppHandlers, patch_video_for_external_urls +from eve_worker_pool import EveWorkerPool +from live_inference import ( + TAB_SWITCH_AUTO_STOP_JS, + RtcConfigProvider, + patch_aioice_stun_transaction, + patch_aiortc_h264_nvenc, + patch_fastrtc_frame_queue, + patch_fastrtc_yuv420p_output, +) +from live_stream_manager import LiveStreamManager +from log_utils import log_cpu_info, setup_logger +from session_tracker import SessionTracker +from usage_analytics import UsageTracker +from video_processing import VideoLimits + +MOD_MODELS = ["GMOD-80", "AMOD-8", "OMOD"] +DEFAULT_MOD_MODEL = "GMOD-80" + +_IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".webp") +_VIDEO_EXTS = (".mp4", ".avi", ".mov", ".mkv", ".webm") + + +def _build_feature_radio(hint: str = "") -> gr.Radio: + """Build the model-selection radio — exactly one MOD model active at a time.""" + label = "Object Detection Model" + if hint: + label += f" — {hint}" + return gr.Radio( + choices=MOD_MODELS, + value=DEFAULT_MOD_MODEL, + label=label, + info="Only GMOD-80 is active. AMOD-8 / OMOD are placeholders pending an " + "EVE SDK update that adds runtime model switching.", + interactive=False, + ) + + +def _scan_examples(folder: Path, exts: tuple[str, ...]) -> list[list[str]]: + """Scan ``folder`` for files with any extension in ``exts``.""" + if not folder.is_dir(): + return [] + return [[str(p)] for p in sorted(folder.iterdir()) if p.suffix.lower() in exts] + + +if __name__ == "__main__": + patch_fastrtc_frame_queue() + patch_fastrtc_yuv420p_output() + patch_aioice_stun_transaction() + patch_aiortc_h264_nvenc() + + load_dotenv_if_present() + logger = setup_logger(name="app") + log_cpu_info(logger) + require_secrets("MODEL_ACCESS_TOKEN") + + tracker = UsageTracker( + repo_id=os.environ.get("ANALYTICS_REPO_ID", "LatticeSemi/PRIVATE-Demo-Analytics-v1.0"), + ) + tracker.log("server", "server_start") + + max_workers = int(os.environ.get("MAX_WORKERS", os.cpu_count())) + max_ram_gb = float(os.environ.get("MAX_RAM_GB", 32)) + pool = EveWorkerPool(max_workers=max_workers, max_ram_gb=max_ram_gb, ram_headroom_gb=2.0) + + max_fps_raw = float(os.environ.get("MAX_TARGET_FPS", "24")) + min_fps_raw = float(os.environ.get("MIN_TARGET_FPS", "15")) + max_fps: float | None = max_fps_raw if max_fps_raw > 0 else None + min_fps: float | None = min_fps_raw if min_fps_raw > 0 else None + + camera_width = int(os.environ.get("CAMERA_WIDTH", 640)) + camera_height = int(os.environ.get("CAMERA_HEIGHT", 360)) + + stream_manager = LiveStreamManager( + pool, + session_lifetime_seconds=60 * 4, + max_fps=max_fps, + min_fps=min_fps, + tracker=tracker, + ) + + session_tracker = SessionTracker(pool=pool, tracker=tracker, logger=logger) + + handlers = EveAppHandlers( + pool=pool, + stream_manager=stream_manager, + sessions=session_tracker, + logger=logger, + max_fps=max_fps, + min_fps=min_fps, + ) + + def _shutdown() -> None: + session_tracker.shutdown() + stream_manager.shutdown() + tracker.log("server", "server_stop") + tracker.shutdown() + + atexit.register(_shutdown) + + examples_dir = Path(__file__).resolve().parent / "examples" + image_examples = _scan_examples(examples_dir, _IMAGE_EXTS) + video_examples = _scan_examples(examples_dir, _VIDEO_EXTS) + video_limits = VideoLimits() + rtc_config_provider = RtcConfigProvider() + + with gr.Blocks( + title="GMOD / AMOD / OMOD Object Detection Demo", + theme=gr.themes.Default( + text_size=gr.themes.sizes.text_lg, + primary_hue=gr.themes.colors.yellow, + ), + css=( + f"#webrtc-stream-col {{ max-width: {camera_width}px !important; margin: 0 auto; }}" + " .gradio-container h1, .gradio-container .md h1 { font-size: 2.25rem !important; }" + " .gradio-container h2, .gradio-container .md h2 { font-size: 1.75rem !important; }" + " .gradio-container h3, .gradio-container .md h3 { font-size: 1.4rem !important; }" + " .gradio-container button[role='tab']," + " .gradio-container button[role='tab'] *" + " { text-decoration: underline !important; }" + " .gradio-container .tab-container {" + " height: auto !important;" + " overflow: visible !important;" + " gap: 4px !important;" + " border-bottom: 2px solid var(--border-color-primary) !important; }" + " .gradio-container .tab-container::after { display: none !important; }" + " .gradio-container button[role='tab'] {" + " height: auto !important;" + " padding: 10px 20px !important;" + " border: 1px solid var(--border-color-primary) !important;" + " border-bottom: none !important;" + " border-radius: 8px 8px 0 0 !important;" + " background: var(--background-fill-secondary) !important;" + " margin-bottom: -2px !important; }" + " .gradio-container button[role='tab'].selected {" + " background: var(--primary-500) !important;" + " color: var(--neutral-950) !important;" + " border-color: var(--primary-500) !important;" + " font-weight: 600 !important; }" + " .gradio-container button[role='tab'].selected::after { display: none !important; }" + ), + head=TAB_SWITCH_AUTO_STOP_JS, + ) as demo: + gr.Markdown("# Lattice sensAI — Generic Object Detection") + gr.Markdown( + "Run object detection on images, videos, or a live camera feed using " + "one of three EVE SDK models:\n\n" + "- **GMOD-80** — generic 80-class detector (active baseline)\n" + "- **AMOD-8** — 8-class automotive object detector " + "(person, bicycle, car, motorcycle, bus, truck, traffic light, " + "stop sign) *(preview)*\n" + "- **OMOD** — many-class office-objects detector *(preview)*\n\n" + "Only one model is active at a time. Pick the model with the radio " + "in each tab.\n\n" + "> **Preview note:** AMOD and OMOD slots currently fall back to GMOD-80 " + "weights — pending an EVE SDK update that adds runtime model switching." + ) + + # Required by EveAppHandlers signatures (Face ID gallery state). Empty + # for this demo — Face ID is not exposed. + session_registry = gr.State(value={}) + session_hash_state = gr.State(value="unknown") + # Constant False states for the four Face/Person/FaceID/Hand flags + # that EveAppHandlers expects but this demo does not expose. + false_state = gr.State(value=False) + + with gr.Tabs() as tabs: + (live_tab, webrtc_stream, live_radio) = build_live_inference_tab( + rtc_configuration=lambda: rtc_config_provider.get(), + feature_checkbox_builder=_build_feature_radio, + max_fps=int(max_fps) if max_fps else 30, + width=camera_width, + height=camera_height, + ) + + offline = build_image_or_video_offline_tab( + feature_checkbox_builder=_build_feature_radio, + image_examples=image_examples, + video_examples=video_examples, + video_limits=video_limits, + ) + patch_video_for_external_urls(offline.video_output) + offline_radio = offline.checkboxes # gr.Radio in this demo + + build_eula_tab() + + # --- Mutually exclusive accordions on the offline tab --- + offline.image_accordion.expand( + fn=lambda: gr.update(open=False), + outputs=[offline.video_accordion], + ) + offline.video_accordion.expand( + fn=lambda: gr.update(open=False), + outputs=[offline.image_accordion], + ) + + # --- Examples wire-up: clicking a sample loads it into the matching input --- + if offline.image_example_dataset is not None: + offline.image_example_dataset.click( + fn=lambda sample: sample[0], + inputs=[offline.image_example_dataset], + outputs=[offline.image_input], + ) + if offline.video_example_dataset is not None: + offline.video_example_dataset.click( + fn=lambda sample: sample[0], + inputs=[offline.video_example_dataset], + outputs=[offline.video_input], + ) + + # --- Process button gating --- + def _on_input_change(image_path: str | None, video_path: str | None) -> dict: + return gr.update(interactive=bool(image_path) or bool(video_path)) + + for src in (offline.image_input, offline.video_input): + src.change( + fn=_on_input_change, + inputs=[offline.image_input, offline.video_input], + outputs=[offline.process_btn], + ) + + # --- Process dispatcher: image vs video --- + def _process_dispatch( + image_path: str | None, + video_path: str | None, + mod_model: str, + registry: dict, + request: gr.Request, + progress: gr.Progress = gr.Progress(), + ): + if image_path: + annotated, registry = handlers.run_eve_image_inference( + image_path, + False, + False, + False, + False, + registry, + mod_model, + request, + progress, + ) + return ( + gr.update(value=annotated, visible=True), + gr.update(value=None, visible=False), + registry, + ) + if video_path: + output_path, registry = handlers.run_eve_inference( + video_path, + False, + False, + False, + False, + registry, + mod_model, + request, + progress, + ) + return ( + gr.update(value=None, visible=False), + gr.update(value=output_path, visible=True), + registry, + ) + raise gr.Error("Please upload an image or video.") + + offline.process_btn.click( + fn=_process_dispatch, + inputs=[ + offline.image_input, + offline.video_input, + offline_radio, + session_registry, + ], + outputs=[ + offline.image_output, + offline.video_output, + session_registry, + ], + concurrency_limit=pool.worker_count, + ) + + # --- Live feature change tracking --- + live_radio.change( + fn=handlers.on_live_feature_change, + inputs=[false_state, false_state, false_state, false_state, live_radio], + outputs=[], + ) + + # --- Live inference wiring --- + webrtc_stream.stream( + fn=handlers.process_live_frame, + inputs=[ + webrtc_stream, + false_state, + false_state, + false_state, + false_state, + session_registry, + session_hash_state, + live_radio, + ], + outputs=[webrtc_stream], + concurrency_limit=pool.worker_count + 8, + ) + + # --- Tab switching analytics --- + tabs.select(fn=session_tracker.on_tab_switch, inputs=[], outputs=[]) + + # --- Session lifecycle --- + demo.load(session_tracker.on_load, outputs=[session_hash_state]) + demo.unload(handlers.cleanup_session) + + if tracker.enabled: + gr.HTML( + "
" + "This demo collects anonymous usage data (session activity, feature usage) " + "to improve the experience. No personal information is stored.
" + ) + + demo.queue() + demo.launch(server_name="0.0.0.0", server_port=7860, share=False) diff --git a/download_models.py b/download_models.py new file mode 100644 index 0000000000000000000000000000000000000000..9b88ef20d7dd2a696f9b896a37b13b42106eff1e --- /dev/null +++ b/download_models.py @@ -0,0 +1,46 @@ +"""Placeholder downloader for GMOD / AMOD / OMOD .h5 model artifacts. + +The EVE C SDK currently ships a single hardcoded MOD model (see +``EveEthosNpu/Models.h`` -> ``ObjectDetection = "gmod-cpu-...dat"``) and has +no runtime model-switch entry point. Until that lands, this script is a +**stub** — it does nothing at runtime, but documents the URLs we plan to +fetch so the wiring is obvious when the SDK gains the switch API. + +When the EVE SDK gains a model-load function: + +1. Fill in the real URLs below (likely a public GitHub release, possibly a + gated ``LatticeSemi/PRIVATE-*`` HF repo). +2. Have the Dockerfile invoke this script after ``install_eve.py`` so the + ``.h5`` files land in ``/opt/eve_models/`` (or wherever the SDK looks). +3. Hand-add a binding for the new SDK function in + ``src/shared/eve_python/eve_sdk.py`` and update + ``eve_wrapper.enable_object_detection`` to call it. +""" + +from __future__ import annotations + +import sys + +# TODO: replace with real download URLs (GitHub Releases or +# LatticeSemi/PRIVATE-* HF repo) once available. +MODELS: dict[str, str] = { + # "GMOD-80": "https://github.com/Use your webcam for real-time inference. " + "Select features below, then grant camera access when prompted.
" + ), +) -> tuple[gr.TabItem, object, TCheckboxes]: + """Build the Live Inference tab with feature checkboxes + WebRTC stream. + + Args: + rtc_configuration: ICE configuration dict, callable that returns + one, or ``None`` for direct connection. A callable is invoked + per-connection by FastRTC, allowing credential refresh. + feature_checkbox_builder: Callable that builds the demo's feature + checkboxes (same shape as in the offline tab). + extras_builder: Optional callable invoked after the WebRTC stream + for demo-specific widgets (e.g. Face ID summary). + max_fps: Maximum frame rate requested from the browser camera. + width / height: Camera frame dimensions in pixels. + tab_label: Tab label text. + description_html: Optional HTML shown above the stream. + + Returns: + ``(tab, webrtc_stream, checkboxes)``. + """ + with gr.TabItem(tab_label) as tab: + if description_html: + gr.HTML(description_html) + checkboxes = feature_checkbox_builder() + webrtc_stream = build_webrtc_stream( + rtc_configuration, max_fps=max_fps, width=width, height=height + ) + if extras_builder is not None: + extras_builder() + + return tab, webrtc_stream, checkboxes diff --git a/shared/eve_inference_handlers.py b/shared/eve_inference_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..282777c4e4c2c1553242cf5f2eccad342406a451 --- /dev/null +++ b/shared/eve_inference_handlers.py @@ -0,0 +1,485 @@ +"""Offline + live inference handlers shared by EVE-based Gradio demos. + +The two main handlers (``run_eve_inference`` for the Offline Inference tab, +``process_live_frame`` for the Live Inference tab) used to live in +``eve_hmi/app.py``. They are bundled on an ``EveAppHandlers`` instance so +demos can wire them with one ``handlers = EveAppHandlers(...)`` line and +re-use them as Gradio callbacks. + +Face ID is opt-in: pass an empty / ``None`` ``registry`` and Face ID +behaviour is bypassed (gallery restore is skipped, the ``face_id`` flag +still travels through ``FeatureFlags`` so the EVE SDK can act on it). +""" + +from __future__ import annotations + +import logging +import os +import shutil +import tempfile +import threading +import time +import uuid +from typing import TYPE_CHECKING + +import cv2 +import gradio as gr +import numpy as np + +from eve_messages import FeatureFlags +from eve_worker_pool import log_worker_activity +from face_id_tab import FaceEntry # runtime import: Gradio resolves type hints at wire time +from frame_drawing import draw_countdown_banner, draw_overlay, draw_session_timer + +if TYPE_CHECKING: + from eve_worker_pool import EveWorkerPool + from live_stream_manager import LiveStreamManager + from session_tracker import SessionTracker + from video_file_server import VideoFileServer + + +def patch_video_for_external_urls(video_component: gr.Video) -> None: + """Patch a ``gr.Video`` so HTTP(S) URLs bypass Gradio's safehttpx download. + + Gradio's default postprocessing fetches HTTP URLs via ``safehttpx``, + which refuses localhost / private IPs. Setting both ``FileData.path`` + and ``FileData.url`` short-circuits the cache logic in + ``async_move_files_to_cache`` so the browser plays the URL directly. + """ + from gradio.components.video import VideoData + from gradio.data_classes import FileData + + original = video_component.postprocess + + def _postprocess(value): # type: ignore[no-untyped-def] + if isinstance(value, str) and value.startswith(("http://", "https://")): + return VideoData(video=FileData(path=value, url=value)) + return original(value) + + video_component.postprocess = _postprocess # type: ignore[assignment] + + +class EveAppHandlers: + """Gradio-callable handlers for EVE offline + live inference. + + Args: + pool: The shared worker pool. + stream_manager: ``LiveStreamManager`` driving WebRTC streams. + sessions: ``SessionTracker`` for analytics + idle reaping. + logger: Logger used for per-session log lines. + max_fps / min_fps: FPS bounds forwarded to ``send_process_video`` + (controls the worker-side encoding rate). + video_server: Optional local HTTP server for processed videos. + When set, processed clips are returned as URLs instead of raw + paths so Chrome's per-origin connection limit doesn't block + playback while SSE is open. + """ + + def __init__( + self, + pool: EveWorkerPool, + stream_manager: LiveStreamManager, + sessions: SessionTracker, + logger: logging.Logger, + max_fps: float | None, + min_fps: float | None, + video_server: VideoFileServer | None = None, + ) -> None: + self._pool = pool + self._stream = stream_manager + self._sessions = sessions + self._logger = logger + self._max_fps = max_fps + self._min_fps = min_fps + self._video_server = video_server + self._live_logged: set[str] = set() + self._live_logged_lock = threading.Lock() + + # ----- offline inference ------------------------------------------------- + + def run_eve_inference( + self, + input_video: str, + face_detection: bool, + person_detection: bool, + face_id: bool, + hand_gesture: bool, + registry: dict[int, FaceEntry] | None, + mod_model: str | None = None, + request: gr.Request | None = None, + progress: gr.Progress = gr.Progress(), + ) -> tuple[str | None, dict[int, FaceEntry]]: + """Process an uploaded video through one EVE worker, end-to-end. + + The whole read → infer → encode → write loop happens inside the + worker process so the asyncio event loop stays free for SSE + delivery. The main thread only forwards the path/config and + relays small progress dicts. + + Returns ``(output_url_or_path, registry)``. ``registry`` is + returned unchanged when Face ID is disabled, or with ``sdk_id`` + fields refreshed from the worker's gallery-restore results. + """ + registry = registry or {} + session = request.session_hash[:8] + self._sessions.track( + request.session_hash, + "video_process", + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + + fps, width, height, total_frames = _read_video_metadata(input_video) + if total_frames <= 0: + raise gr.Error("Could not read any frames from the video.") + + features = FeatureFlags( + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + + gallery_paths: list[str] = [] + remove_all_users = False + if face_id and registry: + gallery_paths = [entry.path for entry in registry.values()] + elif face_id: + remove_all_users = True + + # Write directly into Gradio's cache so postprocessing skips the + # expensive hash_file + shutil.copy2 that would block the asyncio + # event loop. + gradio_cache = os.path.join(tempfile.gettempdir(), "gradio") + session_out = os.path.join(gradio_cache, f"eve_{request.session_hash}") + os.makedirs(session_out, exist_ok=True) + output_path = os.path.join(session_out, f"output_{uuid.uuid4().hex[:8]}.mp4") + + self._logger.info(f"[{session}] run_eve_inference: waiting for worker...") + # Only emit queue events when we actually have to wait — matches the + # live queue behaviour so dashboards don't show phantom peaks. + will_wait = self._pool.idle_count == 0 + if will_wait: + self._sessions.track(request.session_hash, "offline_queue_enter") + t0 = time.monotonic() + worker = self._pool.acquire( + request.session_hash, + timeout=300.0, + progress=progress, + eta_fn=self._stream.estimated_wait, + ) + if will_wait: + self._sessions.track( + request.session_hash, + "offline_queue_exit", + wait_seconds=round(time.monotonic() - t0, 1), + ) + log_worker_activity( + self._logger, "acquired", "video-processing", self._pool, worker.worker_id + ) + try: + frames_processed, gallery_results = worker.send_process_video( + input_path=input_video, + output_path=output_path, + features=features, + gallery_paths=gallery_paths, + remove_all_users=remove_all_users, + fps=fps, + width=width, + height=height, + total_frames=total_frames, + progress=progress, + max_fps=self._max_fps, + min_fps=self._min_fps, + ) + self._logger.info( + f"[{session}] run_eve_inference: done, {frames_processed} frames processed" + ) + finally: + self._pool.release(worker) + log_worker_activity( + self._logger, "released", "video-processing", self._pool, worker.worker_id + ) + self._sessions.track( + request.session_hash, + "video_process_complete", + duration_seconds=round(time.monotonic() - t0, 1), + ) + + if gallery_results: + for entry, r in zip(registry.values(), gallery_results): + entry.sdk_id = r.user_id if r.success else None + + if self._video_server is not None: + rel_path = os.path.relpath(output_path, gradio_cache) + video_result: str | None = self._video_server.build_url(rel_path, request) + else: + video_result = output_path + + return video_result, registry + + # ----- offline image inference ------------------------------------------- + + def run_eve_image_inference( + self, + input_image: str | None, + face_detection: bool, + person_detection: bool, + face_id: bool, + hand_gesture: bool, + registry: dict[int, FaceEntry] | None, + mod_model: str | None = None, + request: gr.Request | None = None, + progress: gr.Progress = gr.Progress(), + ) -> tuple[np.ndarray | None, dict[int, FaceEntry]]: + """Run a single image through one EVE worker, end-to-end. + + Single-frame variant of :meth:`run_eve_inference`. Loads the image + as BGR, sends one ``InferenceCmd`` to the worker (the same path + that powers live inference), and returns the annotated result as + an RGB ``np.ndarray`` ready for ``gr.Image``. + + Returns ``(annotated_image_rgb_or_None, registry)``. Registry is + returned unchanged — single-image inference does not refresh + Face ID gallery state. + """ + if not input_image: + raise gr.Error("Please upload an image.") + + registry = registry or {} + session = request.session_hash[:8] if request is not None else "?" + if request is not None: + self._sessions.track( + request.session_hash, + "image_process", + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + + frame = cv2.imread(input_image) + if frame is None: + raise gr.Error(f"Could not read image: {input_image}") + + features = FeatureFlags( + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + + self._logger.info(f"[{session}] run_eve_image_inference: waiting for worker...") + will_wait = self._pool.idle_count == 0 + if will_wait and request is not None: + self._sessions.track(request.session_hash, "offline_queue_enter") + t0 = time.monotonic() + worker = self._pool.acquire( + request.session_hash if request is not None else "image", + timeout=300.0, + progress=progress, + eta_fn=self._stream.estimated_wait, + ) + if will_wait and request is not None: + self._sessions.track( + request.session_hash, + "offline_queue_exit", + wait_seconds=round(time.monotonic() - t0, 1), + ) + log_worker_activity( + self._logger, "acquired", "image-processing", self._pool, worker.worker_id + ) + try: + result = worker.send_inference(frame, features) + self._logger.info(f"[{session}] run_eve_image_inference: done") + finally: + self._pool.release(worker) + log_worker_activity( + self._logger, "released", "image-processing", self._pool, worker.worker_id + ) + if request is not None: + self._sessions.track( + request.session_hash, + "image_process_complete", + duration_seconds=round(time.monotonic() - t0, 1), + ) + + # Worker returns BGR (matches live-inference frame format); Gradio's + # gr.Image expects RGB. + result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB) + return result_rgb, registry + + # ----- live inference ---------------------------------------------------- + + def process_live_frame( + self, + frame: np.ndarray, + face_detection: bool, + person_detection: bool, + face_id: bool, + hand_gesture: bool, + registry: dict[int, FaceEntry] | None, + session_hash: str, + mod_model: str | None = None, + ): + # Return type intentionally unannotated: actual return is + # ``np.ndarray | fastrtc.CloseStream | None`` but ``CloseStream`` is + # only imported lazily inside the function (avoids pulling fastrtc + # into module scope), and Gradio/FastRTC may run ``get_type_hints`` + # on bound handlers — a forward-ref string would break that. + """Run a single WebRTC frame through an EVE worker. + + A worker is acquired on the first frame and held for the lifetime of + the stream. Returns the frame with an overlay while waiting for a + worker, or a ``CloseStream`` when the session ends so the UI can + reset to ``Start Inference``. + + ``gr.Request`` is not available inside FastRTC stream handlers, so + the Gradio session hash is passed through ``gr.State``. FastRTC's + per-connection ``webrtc_id`` is still pulled from + ``current_context`` to track worker assignment per peer. + """ + if frame is None: + return None + + from fastrtc.utils import current_context + + connection_id = current_context.get().webrtc_id + worker, reason = self._stream.get_or_acquire(connection_id, session_hash, registry or {}) + if worker is None: + if reason == "waiting": + pos, total = self._stream.waiting_position(connection_id) + eta = self._stream.estimated_wait(pos) + eta_text = "" + if eta is not None: + eta_mins = max(0.5, round(eta / 30) * 0.5) + eta_text = f"\nest. wait ~{eta_mins:g} minutes" + if total > 1: + return draw_overlay(frame, f"In queue {eta_text}") + return draw_overlay(frame, f"Waiting for available worker...{eta_text}") + with self._live_logged_lock: + self._live_logged.discard(connection_id) + from fastrtc import CloseStream + + return CloseStream(reason or "Stream ended") + + bridge = self._stream.get_bridge(connection_id) + if bridge is None or not bridge.is_alive: + self._stream.release(connection_id) + return draw_overlay(frame, "Inference error - retrying...") + + with self._live_logged_lock: + first_frame = connection_id not in self._live_logged + if first_frame: + self._live_logged.add(connection_id) + if first_frame: + self._sessions.track( + session_hash, + "live_start", + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + + features = FeatureFlags( + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + try: + result = bridge.submit_and_get_latest(frame, features) + if result is None: + # First frame — no inference result yet, show camera feed + return frame + + remaining = self._stream.countdown_remaining(connection_id) + if remaining is not None: + secs = int(remaining) + 1 + result = draw_countdown_banner(result, f"Other users waiting - stopping in {secs}s") + + session_left = self._stream.session_remaining(connection_id) + if session_left is not None: + result = draw_session_timer(result, session_left) + + return result + except (BrokenPipeError, EOFError, OSError): + # Worker pipe is gone (shutdown or crash) — release quietly + self._stream.release(connection_id) + return None + except Exception as exc: + self._logger.error(f"Live inference error: {exc}") + self._stream.release(connection_id) + return draw_overlay(frame, "Inference error - retrying...") + + def on_live_feature_change( + self, + face_detection: bool, + person_detection: bool, + face_id: bool, + hand_gesture: bool, + mod_model: str | None = None, + request: gr.Request | None = None, + ) -> None: + """Track changes to the Live tab's feature checkboxes.""" + self._sessions.track( + request.session_hash, + "live_feature_change", + face_detection=face_detection, + person_detection=person_detection, + face_id=face_id, + hand_gesture=hand_gesture, + mod_model=mod_model, + ) + + # ----- session cleanup --------------------------------------------------- + + def cleanup_session(self, request: gr.Request) -> None: + """Gradio ``demo.unload`` handler — close the session + delete cache.""" + self._sessions.on_unload(request) + + gradio_cache = os.path.join(tempfile.gettempdir(), "gradio") + session_out = os.path.join(gradio_cache, f"eve_{request.session_hash}") + if os.path.isdir(session_out): + shutil.rmtree(session_out, ignore_errors=True) + self._logger.debug(f"Session cleanup: removed {session_out}") + + +def _read_video_metadata(path: str) -> tuple[float, int, int, int]: + """Return ``(fps, width, height, total_frames)`` for a video file. + + Webcam-recorded WebM blobs frequently lie in their headers (fps=0, + fps=1000 from ms timestamps, or no frame count) so we fall back to a + full decode pass when the headers look implausible. + """ + cap = cv2.VideoCapture(path) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + if fps <= 0 or fps > 240 or total_frames <= 0 or width <= 0 or height <= 0: + frame_count = 0 + duration_ms = 0.0 + while True: + ret, frame = cap.read() + if not ret: + break + if frame_count == 0: + height, width = frame.shape[:2] + frame_count += 1 + duration_ms = cap.get(cv2.CAP_PROP_POS_MSEC) + total_frames = frame_count + if fps <= 0 or fps > 240: + fps = total_frames / (duration_ms / 1000.0) if duration_ms > 0 else 30.0 + cap.release() + return fps, width, height, total_frames diff --git a/shared/eve_messages.py b/shared/eve_messages.py new file mode 100644 index 0000000000000000000000000000000000000000..5525e0da530129675d748b7ba7b7318b2f6d16cb --- /dev/null +++ b/shared/eve_messages.py @@ -0,0 +1,231 @@ +"""IPC message dataclasses for the Eve worker pool. + +All types are picklable and sent over ``multiprocessing.Pipe`` between the +main Gradio process and Eve SDK worker processes. +""" + +from dataclasses import dataclass + +# --------------------------------------------------------------------------- +# Shared data types +# --------------------------------------------------------------------------- + + +@dataclass +class CalibrationResultMsg: + """Picklable mirror of ``eve_wrapper.CalibrationResult``.""" + + success: bool + user_id: int + message: str + + +@dataclass +class FeatureFlags: + """Feature toggle bundle sent with inference / configure commands.""" + + face_detection: bool = True + person_detection: bool = True + face_id: bool = False + hand_gesture: bool = False + # None = MOD off. Otherwise a model-name string (e.g. "GMOD-80", + # "AMOD-8", "OMOD") that the worker resolves to the EVE SDK MOD model. + # Defaulted so existing demos keep their pickle shape unchanged. + mod_model: str | None = None + + +@dataclass +class SerializedFrame: + """Picklable representation of a numpy ndarray frame.""" + + data: bytes + shape: tuple[int, ...] + dtype: str + + +# --------------------------------------------------------------------------- +# Commands (main process → worker process) +# --------------------------------------------------------------------------- + + +@dataclass +class InferenceCmd: + frame_bytes: bytes + shape: tuple[int, ...] + dtype: str + features: FeatureFlags + + +@dataclass +class ConfigureFeaturesCmd: + features: FeatureFlags + + +@dataclass +class CalibrateNewUserCmd: + frames_data: list[SerializedFrame] + + +@dataclass +class RemoveAllUsersCmd: + pass + + +@dataclass +class RestoreGalleryCmd: + frames_per_user_data: list[list[SerializedFrame]] + + +@dataclass +class EnableFaceIdCmd: + enabled: bool + + +@dataclass +class ProcessVideoCmd: + input_path: str + output_path: str + features: FeatureFlags + gallery_paths: list[str] + remove_all_users: bool + fps: float + width: int + height: int + total_frames: int + max_fps: float | None = None + min_fps: float | None = None + + +@dataclass +class ShutdownCmd: + pass + + +@dataclass +class StartProfilingCmd: + pass + + +@dataclass +class StopProfilingCmd: + pass + + +@dataclass +class GetProfileStatsCmd: + pass + + +@dataclass +class GetTimingStatsCmd: + reset: bool = True + + +WorkerCmd = ( + InferenceCmd + | ConfigureFeaturesCmd + | CalibrateNewUserCmd + | RemoveAllUsersCmd + | RestoreGalleryCmd + | EnableFaceIdCmd + | ProcessVideoCmd + | ShutdownCmd + | StartProfilingCmd + | StopProfilingCmd + | GetProfileStatsCmd + | GetTimingStatsCmd +) + + +# --------------------------------------------------------------------------- +# Responses (worker process → main process) +# --------------------------------------------------------------------------- + + +@dataclass +class ReadyResponse: + pid: int + + +@dataclass +class ErrorResponse: + error: str + + +@dataclass +class HeartbeatResponse: + pass + + +@dataclass +class OkResponse: + pass + + +@dataclass +class InferenceResponse: + frame_bytes: bytes + shape: tuple[int, ...] + dtype: str + + +@dataclass +class CalibrateOkResponse: + result: CalibrationResultMsg + + +@dataclass +class RemoveUsersOkResponse: + result: bool + + +@dataclass +class RestoreGalleryOkResponse: + results: list[CalibrationResultMsg] + + +@dataclass +class GalleryRestoredResponse: + results: list[CalibrationResultMsg] + + +@dataclass +class ProgressResponse: + current: int + total: int + + +@dataclass +class VideoProcessingDoneResponse: + frames_processed: int + gallery_results: list[CalibrationResultMsg] + recycle: bool + + +@dataclass +class ProfileStatsResponse: + stats_data: bytes # marshalled pstats data + + +@dataclass +class TimingStatsResponse: + """Per-SDK-call timing data from EveWrapper.""" + + stats: dict[str, tuple[int, float]] # {name: (call_count, total_seconds)} + + +WorkerResponse = ( + ReadyResponse + | ErrorResponse + | HeartbeatResponse + | OkResponse + | InferenceResponse + | CalibrateOkResponse + | RemoveUsersOkResponse + | RestoreGalleryOkResponse + | GalleryRestoredResponse + | ProgressResponse + | VideoProcessingDoneResponse + | ProfileStatsResponse + | TimingStatsResponse +) diff --git a/shared/eve_python/eve_sdk.py b/shared/eve_python/eve_sdk.py new file mode 100644 index 0000000000000000000000000000000000000000..8cc118a96fc94c95f3e8389cf79cf10a644af8f9 --- /dev/null +++ b/shared/eve_python/eve_sdk.py @@ -0,0 +1,297 @@ +import ctypes +from . import eve_sdk_structs as structs + +EveProcessingCallbackFn = ctypes.CFUNCTYPE(None, ctypes.POINTER(structs.EveProcessingCallbackReturnData)) + +class EveSDK: + def __init__(self, dll_path: str): + self.cdll = ctypes.CDLL(dll_path) + # EveCameraApi.h + self.cdll.EveGetFormats.restype = structs.EveCameraFormats + self.cdll.EveGetFormats.argtypes = [ctypes.c_uint, structs.CCameraFormat] + self.cdll.EveGetCamera.restype = structs.EveCamera + self.cdll.EveGetCamera.argtypes = [ctypes.c_uint] + self.cdll.EveSetCamera.restype = structs.EveError + self.cdll.EveSetCamera.argtypes = [ctypes.c_uint, structs.CCameraFormat] + # EveControlInterface.h + self.cdll.CreateEve.restype = structs.EveError + self.cdll.CreateEve.argtypes = [structs.EveStartupParameters] + self.cdll.EveRegisterDataCallback.restype = structs.EveError + self.cdll.EveRegisterDataCallback.argtypes = [EveProcessingCallbackFn] + self.cdll.StartEve.restype = structs.EveError + self.cdll.StartEve.argtypes = [] + self.cdll.StartEveWithParameters.restype = structs.EveError + self.cdll.StartEveWithParameters.argtypes = [structs.EveProcessingParameters] + self.cdll.EveSendImageForProcessing.restype = structs.EveError + self.cdll.EveSendImageForProcessing.argtypes = [structs.EveInputImage] + self.cdll.EveSendImageForProcessingWithParams.restype = structs.EveError + self.cdll.EveSendImageForProcessingWithParams.argtypes = [structs.EveInputImage, structs.CCameraParameters] + self.cdll.ShutdownEve.restype = structs.EveError + self.cdll.ShutdownEve.argtypes = [] + + # EveKarolinska.h + self.cdll.EveConfigureKarolinska.restype = structs.EveKarolinskaOptions + self.cdll.EveConfigureKarolinska.argtypes = [structs.EveKarolinskaOptions] + + self.cdll.EveGetKarolinskaData.restype = structs.EveKarolinskaData + self.cdll.EveGetKarolinskaData.argtypes = [] + + # EveFaceId.h + self.cdll.EveConfigureFaceId.restype = structs.EveFaceIdOptions + self.cdll.EveConfigureFaceId.argtypes = [structs.EveFaceIdOptions] + self.cdll.EveFaceIdCalibrateCurrent.restype = structs.EveError + self.cdll.EveFaceIdCalibrateCurrent.argtypes = [] + self.cdll.EveFaceIdCalibrateNew.restype = structs.EveError + self.cdll.EveFaceIdCalibrateNew.argtypes = [] + self.cdll.EveFaceIdForceIdentify.restype = structs.EveError + self.cdll.EveFaceIdForceIdentify.argtypes = [] + self.cdll.EveFaceIdRemoveCurrent.restype = structs.EveError + self.cdll.EveFaceIdRemoveCurrent.argtypes = [] + self.cdll.EveFaceIdRemoveAll.restype = structs.EveError + self.cdll.EveFaceIdRemoveAll.argtypes = [] + self.cdll.EveFaceIdReloadGallery.restype = structs.EveError + self.cdll.EveFaceIdReloadGallery.argtypes = [] + self.cdll.EveFaceIdCommandWaiting.restype = ctypes.c_uint + self.cdll.EveFaceIdCommandWaiting.argtypes = [] + self.cdll.EveGetFaceIdData.restype = structs.EveFaceIdData + self.cdll.EveGetFaceIdData.argtypes = [] + self.cdll.EveSendFaceIdCommand.restype = structs.EveFaceIdCommandStruct + self.cdll.EveSendFaceIdCommand.argtypes = [structs.EveFaceIdCommandStruct] + # EveFaceTracker.h + self.cdll.EveConfigureFaceTracker.restype = structs.EveFaceTrackerOptions + self.cdll.EveConfigureFaceTracker.argtypes = [structs.EveFaceTrackerOptions] + self.cdll.EveGetAllFaceData.restype = structs.EveAllFacesData + self.cdll.EveGetAllFaceData.argtypes = [] + # EveFpga.h + self.cdll.EveConfigureFpga.restype = structs.EveFpgaOptions + self.cdll.EveConfigureFpga.argtypes = [structs.EveFpgaOptions] + self.cdll.EveConfigureFpgaDebug.restype = structs.EveFpgaDebugOptions + self.cdll.EveConfigureFpgaDebug.argtypes = [structs.EveFpgaDebugOptions] + self.cdll.QueryFpgaSetting.restype = structs.EveError + self.cdll.QueryFpgaSetting.argtypes = [structs.pipeline_config_t, ctypes.c_bool] + self.cdll.QueryFpgaSettings.restype = structs.EveError + self.cdll.QueryFpgaSettings.argtypes = [ctypes.c_uint16, ctypes.c_uint32, ctypes.c_bool] + self.cdll.SendSetSetting.restype = structs.EveError + self.cdll.SendSetSetting.argtypes = [structs.pipeline_config_t] + self.cdll.PopQueuedSetting.restype = structs.CFpgaGetSetting + self.cdll.PopQueuedSetting.argtypes = [] + self.cdll.EveGetFpgaData.restype = structs.EveFpgaData + self.cdll.EveGetFpgaData.argtypes = [] + self.cdll.FpgaReadJson.restype = structs.EveFpgaJsonMetadata + self.cdll.FpgaReadJson.argtypes = [] + self.cdll.EveSendImageForProcessingWithFpgaData.restype = structs.EveError + self.cdll.EveSendImageForProcessingWithFpgaData.argtypes = [structs.EveInputImage, structs.EveFpgaManualData] + + # EveImageManipulation.h + self.cdll.EveConfigureImageManipulation.restype = structs.EveImageManipulationOptions + self.cdll.EveConfigureImageManipulation.argtypes = [structs.EveImageManipulationOptions] + + # EveImage.h + self.cdll.EveGetProcessedImage.restype = structs.EveProcessedImage + self.cdll.EveGetProcessedImage.argtypes = [] + self.cdll.EveGetProcessedFrameTime.restype = structs.EveProcessedFrameTime + self.cdll.EveGetProcessedFrameTime.argtypes = [] + self.cdll.EveConfigureProcessedImage.restype = structs.EveImageFormatRequest + self.cdll.EveConfigureProcessedImage.argtypes = [structs.EveImageFormatRequest] + + # EveObjectDetection.h + self.cdll.EveConfigureObjectDetection.restype = structs.EveObjectDetectionOptions + self.cdll.EveConfigureObjectDetection.argtypes = [] + self.cdll.EveConfigurePersonDetection.restype = structs.EvePersonDetectionOptions + self.cdll.EveConfigurePersonDetection.argtypes = [] + self.cdll.EveGetObjectDetectionData.restype = structs.EveDetectionData + self.cdll.EveGetObjectDetectionData.argtypes = [] + self.cdll.EveCopyObjectDetectionData.restype = structs.EveDetectionData + self.cdll.EveCopyObjectDetectionData.argtypes = [] + self.cdll.EveGetPersonDetectionData.restype = structs.EveDetectionData + self.cdll.EveGetPersonDetectionData.argtypes = [] + self.cdll.EveCopyPersonDetectionData.restype = structs.EveDetectionData + self.cdll.EveCopyPersonDetectionData.argtypes = [] + self.cdll.DeleteDetectionData.restype = structs.EveError + self.cdll.DeleteDetectionData.argtypes = [structs.EveDetectionData] + # EveROI.h + self.cdll.EveConfigureROIs.restype = structs.EveROIOptions + self.cdll.EveConfigureROIs.argtypes = [structs.EveROIOptions] + self.cdll.EveGetROIScoreData.restype = structs.EveROIScoreData + self.cdll.EveGetROIScoreData.argtypes = [] + # EveHandGesture.h + self.cdll.EveConfigureHandGesture.restype = structs.EveHandGestureOptions + self.cdll.EveConfigureHandGesture.argtypes = [structs.EveHandGestureOptions] + self.cdll.EveGetHandGestureData.restype = structs.EveHandGestureData + self.cdll.EveGetHandGestureData.argtypes = [] + self.cdll.EveCopyHandGestureData.restype = structs.EveHandGestureData + self.cdll.EveCopyHandGestureData.argtypes = [] + self.cdll.EveDeleteHandGestureData.restype = structs.EveError + self.cdll.EveDeleteHandGestureData.argtypes = [structs.EveHandGestureData] + self.cdll.EveGetStaticGestureDetections.restype = structs.EveStaticGestureData + self.cdll.EveGetStaticGestureDetections.argtypes = [] + self.cdll.EveGetDynamicGestureDetections.restype = structs.EveDynamicGestureData + self.cdll.EveGetDynamicGestureDetections.argtypes = [] + + + # EveCamera.h + def EveGetFormats(self, cameraId: ctypes.c_uint, filter: structs.CCameraFormat) -> structs.EveCameraFormats: + return self.cdll.EveGetFormats(cameraId, filter) + + def EveGetCamera(self, cameraId: ctypes.c_uint) -> structs.EveCamera: + return self.cdll.EveGetCamera(cameraId) + + def EveSetCamera(self, cameraId: ctypes.c_uint, filter: structs.CCameraFormat) -> structs.EveError: + return self.cdll.EveSetCamera(cameraId, filter) + + # EveControlInterface.h + def CreateEve(self, options: structs.EveStartupParameters) -> structs.EveError: + return self.cdll.CreateEve(options) + + def EveRegisterDataCallback(self, callback) -> structs.EveError: + return self.cdll.EveRegisterDataCallback(callback) + + def StartEve(self) -> structs.EveError: + return self.cdll.StartEve() + + def StartEveWithParameters(self, parameters: structs.EveProcessingParameters) -> structs.EveError: + return self.cdll.StartEveWithParameters(parameters) + + def EveSendImageForProcessing(self, image: structs.EveInputImage) -> structs.EveError: + return self.cdll.EveSendImageForProcessing(image) + + def EveSendImageForProcessingWithParams(self, image: structs.EveInputImage, params: structs.CCameraParameters) -> structs.EveError: + return self.cdll.EveSendImageForProcessingWithParams(image, params) + + def EveSendFpgaDataManually(self, image: structs.EveFpgaManualData) -> structs.EveError: + return self.cdll.EveSendFpgaDataManually(image) + + def ShutdownEve(self) -> structs.EveError: + return self.cdll.ShutdownEve() + + # EveFaceId.h + def EveConfigureFaceId(self, options: structs.EveFaceIdOptions) -> structs.EveFaceIdOptions: + return self.cdll.EveConfigureFaceId(options) + + def EveFaceIdCalibrateCurrent(self) -> structs.EveError: + return self.cdll.EveFaceIdCalibrateCurrent() + + def EveFaceIdCalibrateNew(self) -> structs.EveError: + return self.cdll.EveFaceIdCalibrateNew() + + def EveFaceIdForceIdentify(self) -> structs.EveError: + return self.cdll.EveFaceIdForceIdentify() + + def EveFaceIdRemoveCurrent(self) -> structs.EveError: + return self.cdll.EveFaceIdRemoveCurrent() + + def EveFaceIdRemoveAll(self) -> structs.EveError: + return self.cdll.EveFaceIdRemoveAll() + + def EveFaceIdReloadGallery(self) -> structs.EveError: + return self.cdll.EveFaceIdReloadGallery() + + def EveFaceIdCommandWaiting(self) -> ctypes.c_uint: + return self.cdll.EveFaceIdCommandWaiting() + + def EveGetFaceIdData(self) -> structs.EveFaceIdData: + return self.cdll.EveGetFaceIdData() + + def EveSendFaceIdCommand(self, command: structs.EveFaceIdCommandStruct) -> structs.EveFaceIdCommandStruct: + return self.cdll.EveSendFaceIdCommand(command) + + # EveFaceTracker.h + def EveConfigureFaceTracker(self, options: structs.EveFaceTrackerOptions) -> structs.EveFaceTrackerOptions: + return self.cdll.EveConfigureFaceTracker(options) + + def EveGetAllFaceData(self) -> structs.EveAllFacesData: + return self.cdll.EveGetAllFaceData() + + # EveFpga.h + def EveGetFpgaData(self) -> structs.EveFpgaData: + return self.cdll.EveGetFpgaData() + + def EveConfigureFpga(self, options: structs.EveFpgaOptions) -> structs.EveFpgaOptions: + return self.cdll.EveConfigureFpga(options) + + def EveConfigureFpgaDebug(self, options: structs.EveFpgaDebugOptions) -> structs.EveFpgaDebugOptions: + return self.cdll.EveConfigureFpgaDebug(options) + + def QueryFpgaSetting(self, command: structs.pipeline_config_t, notify: ctypes.c_bool) -> structs.EveError: + return self.cdll.QueryFpgaSetting(command, notify) + + def QueryFpgaSettings(self, typeMask: ctypes.c_uint16, settingsMask: ctypes.c_uint32, notify: ctypes.c_bool) -> structs.EveError: + return self.cdll.QueryFpgaSettings(typeMask, settingsMask, notify) + + def SendSetSetting(self, command: structs.pipeline_config_t) -> structs.EveError: + return self.cdll.SendSetSetting(command) + + def PopQueuedSetting(self) -> structs.CFpgaGetSetting: + return self.cdll.PopQueuedSetting() + + def FpgaReadJson(self) -> structs.EveFpgaJsonMetadata: + return self.cdll.FpgaReadJson() + + # EveKarolinksa.h + def EveConfigureKarolinska(self, parameters: structs.EveKarolinskaOptions) -> structs.EveKarolinskaOptions: + return self.cdll.EveConfigureKarolinska(parameters) + + def EveGetKarolinskaData(self) -> structs.EveKarolinskaData: + return self.cdll.EveGetKarolinskaData() + + # EveImageManipulation.h + def EveConfigureImageManipulation(self, options: structs.EveImageManipulationOptions) -> structs.EveImageManipulationOptions: + return self.cdll.EveConfigureImageManipulation(options) + + # EveImage.h + def EveGetProcessedImage(self) -> structs.EveProcessedImage: + return self.cdll.EveGetProcessedImage() + + def EveGetProcessedFrameTime(self) -> structs.EveProcessedFrameTime: + return self.cdll.EveGetProcessedFrameTime() + + def EveConfigureProcessedImage(self, fmt: structs.EveImageFormatRequest) -> structs.EveProcessedFrameTime: + return self.cdll.EveConfigureProcessedImage(fmt) + + # EveObjectDetection.h + def EveConfigureObjectDetection(self, enabled: structs.EveObjectDetectionOptions) -> structs.EveObjectDetectionOptions: + return self.cdll.EveConfigureObjectDetection(enabled) + + def EveConfigurePersonDetection(self, enabled: structs.EvePersonDetectionOptions) -> structs.EvePersonDetectionOptions: + return self.cdll.EveConfigurePersonDetection(enabled) + + def EveGetObjectDetectionData(self) -> structs.EveDetectionData: + return self.cdll.EveGetObjectDetectionData() + + def EveCopyObjectDetectionData(self) -> structs.EveDetectionData: + return self.cdll.EveCopyObjectDetectionData() + + def EveGetPersonDetectionData(self) -> structs.EveDetectionData: + return self.cdll.EveGetPersonDetectionData() + + def EveCopyPersonDetectionData(self) -> structs.EveDetectionData: + return self.cdll.EveCopyPersonDetectionData() + + def DeleteDetectionData(self, data: structs.EveDetectionData) -> structs.EveError: + return self.cdll.DeleteDetectionData(data) + + # EveROI.h + def EveConfigureROIs(self, options: structs.EveROIOptions) -> structs.EveError: + return self.cdll.EveConfigureROIs(options) + + def EveGetROIScoreData(self) -> structs.EveROIScoreData: + return self.cdll.EveGetROIScoreData() + + # EveHandGesture.h + def EveConfigureHandGesture(self, options: structs.EveHandGestureOptions) -> structs.EveHandGestureOptions: + return self.cdll.EveConfigureHandGesture(options) + + def EveGetHandGestureData(self) -> structs.EveHandGestureData: + return self.cdll.EveGetHandGestureData() + + def EveCopyHandGestureData(self) -> structs.EveHandGestureData: + return self.cdll.EveGetHandGestureData() + + def EveDeleteHandGestureData(self, data: structs.EveHandGestureData) -> structs.EveError: + return self.cdll.EveGetHandGestureData(data) + + def EveGetStaticGestureDetections(self) -> structs.EveStaticGestureData: + return self.cdll.EveGetStaticGestureDetections() + + def EveGetDynamicGestureDetections(self) -> structs.EveDynamicGestureData: + return self.cdll.EveGetDynamicGestureDetections() \ No newline at end of file diff --git a/shared/eve_python/eve_sdk_structs.py b/shared/eve_python/eve_sdk_structs.py new file mode 100644 index 0000000000000000000000000000000000000000..01d5977656b8b8fdca372a65f67e572c8298ff0f --- /dev/null +++ b/shared/eve_python/eve_sdk_structs.py @@ -0,0 +1,48 @@ +import ctypes +from ctypes_enum import CtypesEnum +#If any of the files below are missing, make sure to run PythonCTypesGenerator.py +from .structs.CAlgorithms import * +from .structs.CBasicStructs import * +from .structs.CCameraStructs import * +from .structs.CDetectionStructs import * +from .structs.CFaceData import * +from .structs.CFaceIdStructs import * +from .structs.CFpgaData import * +from .structs.CHandGesture import * +from .structs.CImageManipulation import * +from .structs.CKarolinska import * +from .structs.CROIStructs import * +from .structs.CScreenLocation import * +from .structs.CVisualSpeechStructs import * +from .structs.EveProcessingStatus import * +from .structs.EveAlgorithm import * +from .structs.EveAlgorithmStructs import * +from .structs.EveCallbackReturnData import * +from .structs.EveCamera import * +from .structs.EveCameraStructs import * +from .structs.EveConfigurationParameters import * +from .structs.EveControlInterface import * +from .structs.EveControlOption import * +from .structs.EveErrors import * +from .structs.EveFaceId import * +from .structs.EveFaceIdStructs import * +from .structs.EveFaceTracker import * +from .structs.EveFaceTrackerStructs import * +from .structs.EveFpga import * +from .structs.EveFpgaStructs import * +from .structs.EveHandGesture import * +from .structs.EveHandGestureStructs import * +from .structs.EveImage import * +from .structs.EveImageManipulation import * +from .structs.EveImageManipulationStructs import * +from .structs.EveImageStructs import * +from .structs.EveKarolinska import * +from .structs.EveKarolinskaStructs import * +from .structs.EveObjectDetection import * +from .structs.EveObjectDetectionStructs import * +from .structs.EveROI import * +from .structs.EveROIStructs import * +from .structs.EveScreenLocation import * +from .structs.EveScreenLocationStructs import * +from .structs.EveTiming import * +from .structs.EveTimingStructs import * diff --git a/shared/eve_python/structs/CAlgorithms.py b/shared/eve_python/structs/CAlgorithms.py new file mode 100644 index 0000000000000000000000000000000000000000..cd9b4bcda53ee7d8aac734340299c51957bcb37c --- /dev/null +++ b/shared/eve_python/structs/CAlgorithms.py @@ -0,0 +1,27 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class EveAlgorithms(CtypesEnum): + EVE_ALGO_NONE = 0 + EVE_ALGO_BACKGROUND_SEGMENTATION = 1 + EVE_ALGO_OBJECT_DETECTION = 2 + EVE_ALGO_PERSON_DETECTION = 3 + EVE_ALGO_HEADPOSE_3D = 4 + EVE_ALGO_HAND_GESTURE = 5 + EVE_ALGO_DEPTH = 6 + EVE_ALGO_EYEWEAR_DETECTION = 7 + EVE_ALGO_KAROLINSKA = 8 + EVE_ALGO_GAZE = 9 + EVE_ALGO_GAZE_SINGLE_OUTPUT = 10 + EVE_ALGO_FACE_ID = 11 + EVE_ALGO_ROI_SELECTION = 12 + EVE_ALGO_FACE_ENHANCEMENT = 13 + EVE_ALGO_VISUAL_SPEECH_DETECTION = 14 + EVE_ALGO_BACKGROUND_BLUR = 15 + EVE_ALGO_BACKGROUND_REPLACEMENT = 16 + EVE_ALGO_USER_HILIGHT = 17 + EVE_ALGO_USER_FRAMING = 18 + EVE_ALGO_MIRROR_IMAGE = 19 + EVE_ALGO_FPGA_DRAWING = 20 + diff --git a/shared/eve_python/structs/CBasicStructs.py b/shared/eve_python/structs/CBasicStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..f6bb776edc75c60512dea6bd6f1c90c684d159a1 --- /dev/null +++ b/shared/eve_python/structs/CBasicStructs.py @@ -0,0 +1,86 @@ +import ctypes +from ctypes_enum import CtypesEnum + +EVE_ENCODING_SIZE = 9 +EVE_LOCATION_SIZE = 2 + +class EveVideoFormat(CtypesEnum): + EVE_NONE = 0 + EVE_BGRA = 1 + EVE_YUY2 = 2 + EVE_NV12 = 3 + EVE_MJPG = 4 + EVE_BGR = 5 + EVE_GRAYSCALE = 6 + EVE_RGBA = 7 + EVE_RGB = 8 + EVE_ENCODING_SIZE = 9 + +class EveImageLocation(CtypesEnum): + EVE_CPU = 0 + EVE_GPU = 1 + EVE_LOCATION_SIZE = 2 + +class CPoint2i(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ] + +class CPoint2f(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_float), + ("y", ctypes.c_float), + ] + +class CPoint3i(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ("z", ctypes.c_int), + ] + +class CPoint3f(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_float), + ("y", ctypes.c_float), + ("z", ctypes.c_float), + ] + +class CAngles3f(ctypes.Structure): + _fields_ = [ + ("pitch", ctypes.c_float), + ("yaw", ctypes.c_float), + ("roll", ctypes.c_float), + ] + +class CRect2i(ctypes.Structure): + _fields_ = [ + ("left", ctypes.c_int), + ("top", ctypes.c_int), + ("right", ctypes.c_int), + ("bottom", ctypes.c_int), + ] + +class CRect2iWH(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ] + +class CRect2fWH(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_float), + ("y", ctypes.c_float), + ("width", ctypes.c_float), + ("height", ctypes.c_float), + ] + +class CResolution(ctypes.Structure): + _fields_ = [ + ("width", ctypes.c_uint), + ("height", ctypes.c_uint), + ] + diff --git a/shared/eve_python/structs/CCameraStructs.py b/shared/eve_python/structs/CCameraStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..d100e5251d96b2c4826cf09fea9195dc040b9d89 --- /dev/null +++ b/shared/eve_python/structs/CCameraStructs.py @@ -0,0 +1,48 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CBasicStructs import * + +CAMERA_PID_VID_SIZE = 8 +CAMERA_NAME_SIZE = 64 + +class EveCompare(CtypesEnum): + EVE_EQUAL = 0 + EVE_AT_MOST = 1 + EVE_AT_LEAST = 2 + +class CCameraFormat(ctypes.Structure): + _fields_ = [ + ("resolution", CResolution), + ("format", ctypes.c_int), + ("fps", ctypes.c_float), + ("compareResolution", ctypes.c_int), + ("compareFps", ctypes.c_int), + ] + +class CCamera(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_int), + ("pid", ctypes.c_byte * CAMERA_PID_VID_SIZE), + ("vid", ctypes.c_byte * CAMERA_PID_VID_SIZE), + ("name", ctypes.c_byte * CAMERA_NAME_SIZE), + ("isHardwareCamera", ctypes.c_uint), + ("isFpgaCamera", ctypes.c_uint), + ("isIrCamera", ctypes.c_uint), + ] + +class CCameraParameters(ctypes.Structure): + _fields_ = [ + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("focalLength", ctypes.c_double), + ("pixelSizeX", ctypes.c_double), + ("pixelSizeY", ctypes.c_double), + ("principalPointX", ctypes.c_double), + ("principalPointY", ctypes.c_double), + ("depthMin", ctypes.c_double), + ("depthMax", ctypes.c_double), + ("screenLocationXinMM", ctypes.c_float), + ("screenLocationYinMM", ctypes.c_float), + ("isInfrared", ctypes.c_uint), + ] + diff --git a/shared/eve_python/structs/CDetectionStructs.py b/shared/eve_python/structs/CDetectionStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..1cc8771bcbe5a32c639e2ce440ccdaf51ca65f4a --- /dev/null +++ b/shared/eve_python/structs/CDetectionStructs.py @@ -0,0 +1,38 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveProcessingStatus import * + +EVE_DETECTIONS_SIZE = 256 +EVE_CLASS_ID_NAME_SIZE = 32 + +class EveActionStatus(CtypesEnum): + EVE_IDLE = 0 + EVE_INTERPOLATED = 1 + EVE_COMPUTED = 2 + EVE_NO_OUTPUT = 3 + +class FrontalStatus(CtypesEnum): + UNKNOWN = 0 + FRONTAL = 1 + NON_FRONTAL = 2 + +class CSingleDetectionData(ctypes.Structure): + _fields_ = [ + ("topLeftX", ctypes.c_int), + ("topLeftY", ctypes.c_int), + ("bottomRightX", ctypes.c_int), + ("bottomRightY", ctypes.c_int), + ("classScore", ctypes.c_float), + ("classId", ctypes.c_int), + ("classIdName", ctypes.c_byte * EVE_CLASS_ID_NAME_SIZE), + ("frontalStatus", ctypes.c_int), + ] + +class CDetectionData(ctypes.Structure): + _fields_ = [ + ("processingStatus", ctypes.c_int), + ("actionStatus", ctypes.c_int), + ("numberOfDetections", ctypes.c_int), + ("detections", CSingleDetectionData * EVE_DETECTIONS_SIZE), + ] + diff --git a/shared/eve_python/structs/CFaceData.py b/shared/eve_python/structs/CFaceData.py new file mode 100644 index 0000000000000000000000000000000000000000..d8e1be7edcea3a28b8e63c90a51564dfaaa30c81 --- /dev/null +++ b/shared/eve_python/structs/CFaceData.py @@ -0,0 +1,55 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CBasicStructs import * +from .CFaceIdStructs import * + +EVE_MAX_FACES = 10 +EVE_EYE_LANDMARK_SIZE = 14 +EVE_PUPIL_LANDMARK_SIZE = 2 + +class EveEyeLandmark(CtypesEnum): + EVE_EYE_RIGHT_CORNER_TEMPORAL = 0 + EVE_EYE_RIGHT_EYELID_UPPER_1 = 1 + EVE_EYE_RIGHT_EYELID_UPPER_2 = 2 + EVE_EYE_RIGHT_CORNER_NASAL = 3 + EVE_EYE_RIGHT_EYELID_LOWER_1 = 4 + EVE_EYE_RIGHT_EYELID_LOWER_2 = 5 + EVE_EYE_LEFT_CORNER_NASAL = 6 + EVE_EYE_LEFT_EYELID_UPPER_2 = 7 + EVE_EYE_LEFT_EYELID_UPPER_1 = 8 + EVE_EYE_LEFT_CORNER_TEMPORAL = 9 + EVE_EYE_LEFT_EYELID_LOWER_2 = 10 + EVE_EYE_LEFT_EYELID_LOWER_1 = 11 + EVE_EYE_RIGHT_PUPIL_CENTER = 12 + EVE_EYE_LEFT_PUPIL_CENTER = 13 + EVE_EYE_LANDMARK_SIZE = 14 + +class EvePupilLandmark(CtypesEnum): + EVE_RIGHT_PUPIL_CENTER = 0 + EVE_LEFT_PUPIL_CENTER = 1 + EVE_PUPIL_LANDMARK_SIZE = 2 + +class CEyeLandmarks(ctypes.Structure): + _fields_ = [ + ("landmarks", CPoint3f * EVE_EYE_LANDMARK_SIZE), + ] + +class CPupilLandmarks(ctypes.Structure): + _fields_ = [ + ("landmarks", CPoint3f * EVE_PUPIL_LANDMARK_SIZE), + ] + +class CFaceData(ctypes.Structure): + _fields_ = [ + ("angles", CAngles3f), + ("faceId", CFaceIdentityData), + ("depth", ctypes.c_float), + ("trackNumber", ctypes.c_int), + ] + +class CAllFaces(ctypes.Structure): + _fields_ = [ + ("detectedFacesCount", ctypes.c_uint), + ("faces", CFaceData * EVE_MAX_FACES), + ] + diff --git a/shared/eve_python/structs/CFaceIdStructs.py b/shared/eve_python/structs/CFaceIdStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..38c3a0fed2c54028341271b715d2e5426cf82a92 --- /dev/null +++ b/shared/eve_python/structs/CFaceIdStructs.py @@ -0,0 +1,68 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveProcessingStatus import * + +EVE_FACE_ID_MAX_MISSING_CALIBRATION_POSES = 5 + +class EveFaceIdActionStatus(CtypesEnum): + EVE_FACE_ID_ACTION_IDLE = 0 + EVE_FACE_ID_ACTION_CALIBRATING = 1 + EVE_FACE_ID_ACTION_CALIBRATED = 2 + EVE_FACE_ID_ACTION_IDENTIFIED = 3 + +class EveFaceIdCalibrationStatus(CtypesEnum): + EVE_FACE_ID_CALIB_NONE = 0 + EVE_FACE_ID_CALIB_RUNNING = 1 + EVE_FACE_ID_CALIB_SUCCESS = 2 + EVE_FACE_ID_CALIB_FAILURE_LACK_POSE_MOTION = 3 + EVE_FACE_ID_CALIB_FAILURE_OTHER = 4 + +class EveFaceIdIdentificationStatus(CtypesEnum): + EVE_FACE_ID_NONE = 0 + EVE_FACE_ID_SUCCESS = 1 + EVE_FACE_ID_FAILURE_VERIFICATION = 2 + EVE_FACE_ID_FAILURE_ANGLE_PITCH = 3 + EVE_FACE_ID_FAILURE_ANGLE_YAW = 4 + EVE_FACE_ID_FAILURE_ANGLE_ROLL = 5 + EVE_FACE_ID_FAILURE_ANGLE_BOTH = 6 + EVE_FACE_ID_FAILURE_NO_GALLERY = 7 + EVE_FACE_ID_FAILURE_EXP_SMILE = 8 + EVE_FACE_ID_FAILURE_EXP_SQUINT = 9 + EVE_FACE_ID_FAILURE_EXP_EYES_CLOSED = 10 + EVE_FACE_ID_FAILURE_DEPTH = 11 + EVE_FACE_ID_FAILURE_OTHER = 12 + +class EveFaceIdPose(CtypesEnum): + EVE_FACE_ID_POSE_FRONTAL = 0 + EVE_FACE_ID_POSE_LEFT = 1 + EVE_FACE_ID_POSE_RIGHT = 2 + EVE_FACE_ID_POSE_UP = 3 + EVE_FACE_ID_POSE_DOWN = 4 + +class EveFaceIdCommand(CtypesEnum): + EVE_FACE_ID_COMMAND_NONE = 0 + EVE_FACE_ID_COMMAND_ADD_NEW_USER = 1 + EVE_FACE_ID_COMMAND_CALIBRATE_CURRENT_USER = 2 + EVE_FACE_ID_COMMAND_FORCE_ID = 3 + EVE_FACE_ID_COMMAND_REMOVE_CURRENT_USER = 4 + EVE_FACE_ID_COMMAND_REMOVE_ALL_USERS = 5 + EVE_FACE_ID_COMMAND_RELOAD_GALLERY = 6 + +class CFaceIdentity(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_longlong), + ("confidence", ctypes.c_float), + ("similarity", ctypes.c_float), + ] + +class CFaceIdentityData(ctypes.Structure): + _fields_ = [ + ("processingStatus", ctypes.c_int), + ("actionStatus", ctypes.c_int), + ("calibrationStatus", ctypes.c_int), + ("identificationStatus", ctypes.c_int), + ("faceIdentity", CFaceIdentity), + ("missingCalibrationPosesCount", ctypes.c_uint), + ("missingCalibrationPoses", ctypes.c_int * EVE_FACE_ID_MAX_MISSING_CALIBRATION_POSES), + ] + diff --git a/shared/eve_python/structs/CFpgaData.py b/shared/eve_python/structs/CFpgaData.py new file mode 100644 index 0000000000000000000000000000000000000000..ea12acf45b52a81fe935f584ad58d34787d47c10 --- /dev/null +++ b/shared/eve_python/structs/CFpgaData.py @@ -0,0 +1,350 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CBasicStructs import * + +EVE_FPGA_MAX_USERS = 10 +EVE_FPGA_MAX_PERSONS = 5 +EVE_FPGA_MAX_HAND_LANDMARKS = 11 +EVE_FPGA_HAND_LANDMARKS = 10 +EVE_FPGA_MAX_OBJECT_DETECTION = 50 +PT_SIZE = 6 +MT_SIZE = 4 +RT_SIZE = 4 + +class EveFpgaConnectionType(CtypesEnum): + EVE_FPGA_AUTO_SELECT = 0 + EVE_FPGA_UART = 1 + EVE_FPGA_I2C = 2 + EVE_FPGA_HUB = 3 + EVE_FPGA_MANUAL = 4 + +class EveFpgaConnectionRequest(CtypesEnum): + EVE_FPGA_STOP = 0 + EVE_FPGA_CONTINUE = 1 + +class pipeline_config_type_t(CtypesEnum): + PT_FD = 0 + PT_LM_FV = 1 + PT_FID = 2 + PT_PD = 3 + PT_HD = 4 + PT_HLMV = 5 + PT_SIZE = 6 + +class setting_type_t(CtypesEnum): + CS_ENABLED = 0x00 + CS_IPS = 0x01 + CS_RESERVED_2_7 = 0x02 + CS_COMMAND = 0x08 + CS_CUSTOM = 0x10 + CS_MAX = 0x11 + +class message_type_t(CtypesEnum): + MT_NONE = 0 + MT_SET = 1 + MT_GET = 2 + MT_GET_BATCH = 3 + MT_SIZE = 4 + +class response_type_t(CtypesEnum): + RT_NONE = 0 + RT_DATA = 1 + RT_GET = 2 + RT_ACK = 3 + RT_SIZE = 4 + +class EveFpgaSerialStatus(CtypesEnum): + EVE_FPGA_SUCCESS = 0 + EVE_FPGA_NO_DATA = 1 + EVE_FPGA_READ_START_MARKER_FAILED = 2 + EVE_FPGA_FIND_START_MARKER_FAILED = 3 + EVE_FPGA_READ_DATA_LENGTH_FAILED = 4 + EVE_FPGA_READ_DATA_FAILED = 5 + EVE_FPGA_CORRUPTED_DATA = 6 + EVE_FPGA_UNEXPECTED_RESPONSE_TYPE = 7 + EVE_FPGA_API_ERROR_START = 8 + EVE_FPGA_NO_CALLBACK = 9 + EVE_FPGA_DATA_ACCESSED_OUTSIDE_CALLBACK = 10 + EVE_FPGA_INIT_FAILED = 11 + EVE_FPGA_NOT_INIT = 12 + EVE_FPGA_NOT_IMPLEMENTED = 13 + EVE_FPGA_API_ERROR_END = 14 + +class EveWakeupDetectionType(CtypesEnum): + EVE_USER_DETECTION = 0 + EVE_STRANGER_DETECTION = 1 + +class EveFpgaPipelineType(CtypesEnum): + EVE_UNKNOWN_PIPELINE = 0 + EVE_HEAD_POSE_PIPELINE = 1 + EVE_FACE_ID_PIPELINE = 2 + EVE_HAND_GESTURE_PIPELINE = 3 + EVE_COMPACT_HEAD_POSE_PIPELINE = 4 + EVE_HMI_PIPELINE = 5 + EVE_STANDALONE_HAND_GESTURE_PIPELINE = 6 + +class EvePersonBodyPose(CtypesEnum): + EVE_FRONT = 0 + EVE_NOT_FRONT = 1 + +class EveDistanceFromCamera(CtypesEnum): + EVE_DISTANCE_CLOSE = 0 + EVE_DISTANCE_MID = 1 + EVE_DISTANCE_FAR = 2 + +class EvePersonRegistrationStatus(CtypesEnum): + EVE_REGISTERED = 0 + EVE_UNREGISTERED = 1 + EVE_UNKNOWN = 2 + EVE_REQUIREMENTS_UNMET = 3 + EVE_DISABLED = 4 + EVE_NO_GALLERY = 5 + +class EveFpgaHandGesture(CtypesEnum): + EVE_FPGA_HAND_GESTURE_NO_GESTURE = 0 + EVE_FPGA_HAND_GESTURE_CLOSE = 1 + EVE_FPGA_HAND_GESTURE_OPEN = 2 + EVE_FPGA_HAND_GESTURE_OPEN_LEFT = 3 + EVE_FPGA_HAND_GESTURE_OPEN_RIGHT = 4 + EVE_FPGA_HAND_GESTURE_INDEX_UP = 5 + EVE_FPGA_HAND_GESTURE_INDEX_DOWN = 6 + EVE_FPGA_HAND_GESTURE_TIP_LEFT = 7 + EVE_FPGA_HAND_GESTURE_TIP_RIGHT = 8 + EVE_FPGA_HAND_GESTURE_UNKNOWN = 9 + +class EveFpgaObjectClass(CtypesEnum): + EVE_FPGA_OBJECT_CLASS_PERSON = 0 + EVE_FPGA_OBJECT_CLASS_BICYCLE = 1 + EVE_FPGA_OBJECT_CLASS_CAR = 2 + EVE_FPGA_OBJECT_CLASS_MOTORCYCLE = 3 + EVE_FPGA_OBJECT_CLASS_BUS = 4 + EVE_FPGA_OBJECT_CLASS_TRUCK = 5 + EVE_FPGA_OBJECT_CLASS_TRAFFIC_LIGHT = 6 + EVE_FPGA_OBJECT_CLASS_STOP_SIGN = 7 + +class pipeline_setting_t(ctypes.Structure): + _fields_ = [ + ("settingType", ctypes.c_int), + ("value", ctypes.c_uint32), + ] + +class pipeline_config_t(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_int), + ("setting", pipeline_setting_t), + ] + +class CFpgaIdealPersonData(ctypes.Structure): + _fields_ = [ + ("valid", ctypes.c_uint), + ("index", ctypes.c_uint), + ("status", ctypes.c_int), + ("faceAngles", CAngles3f), + ("faceLandmarksConfidence", ctypes.c_float), + ("isFaceLandmarksConfidenceValid", ctypes.c_bool), + ] + +class CFpgaImageDimensions(ctypes.Structure): + _fields_ = [ + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("cropArea", CRect2i), + ("reserved1", ctypes.c_int), + ("reserved2", ctypes.c_int), + ] + +class CFpgaDataContent(ctypes.Structure): + _fields_ = [ + ("numberOfUsers", ctypes.c_int16), + ("idealUserIndex", ctypes.c_int16), + ("numberOfDetectedFaces", ctypes.c_int16), + ("numberOfFacesConfidence", ctypes.c_float), + ("numberOfDetectedPersons", ctypes.c_int16), + ("numberOfPersonsConfidence", ctypes.c_float), + ("isIdealUserDataAvailable", ctypes.c_bool), + ("idealUserDetected", ctypes.c_bool), + ("isIdealUserIndexValid", ctypes.c_bool), + ("isNumberOfDetectedFacesAvailable", ctypes.c_bool), + ("isNumberOfFacesConfidenceAvailable", ctypes.c_bool), + ("isNumberOfDetectedPersonsAvailable", ctypes.c_bool), + ("isNumberOfPersonsConfidenceAvailable", ctypes.c_bool), + ("isUsersDataAvilable", ctypes.c_bool), + ("isFaceIdDataAvailable", ctypes.c_bool), + ("isObjectDetectionAvailable", ctypes.c_bool), + ("isCameraStreaming", ctypes.c_bool), + ("isHandGestureDataAvailable", ctypes.c_bool), + ("isDefectDetectionAvailable", ctypes.c_bool), + ] + +class CFpgaHandData(ctypes.Structure): + _fields_ = [ + ("validationScore", ctypes.c_float), + ("handBox", CRect2i), + ("landmarks", CPoint3f * EVE_FPGA_MAX_HAND_LANDMARKS), + ] + +class CFpgaHandsData(ctypes.Structure): + _fields_ = [ + ("numberOfHandLandmarkPoints", ctypes.c_int16), + ("handData", CFpgaHandData), + ("gesture", ctypes.c_int), + ("isHandBoxAvailable", ctypes.c_bool), + ("isHandLandmark3D", ctypes.c_bool), + ] + +class CFpgaDefectData(ctypes.Structure): + _fields_ = [ + ("defectBox", CRect2i), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("similarity", ctypes.c_float), + ("isDefective", ctypes.c_bool), + ] + +class CFpgaFaceData(ctypes.Structure): + _fields_ = [ + ("faceConfidence", ctypes.c_float), + ("faceDistance", ctypes.c_int16), + ("faceCenter", CPoint3i), + ("anglesICS", CAngles3f), + ("anglesCCS", CAngles3f), + ("faceLandmarksConfidence", ctypes.c_float), + ("faceBox", CRect2i), + ("faceIDStatus", ctypes.c_int), + ("faceID", ctypes.c_int16), + ("isFaceConfidenceAvailable", ctypes.c_bool), + ("isFaceDistanceAvailable", ctypes.c_bool), + ("isFacePositionAvailable", ctypes.c_bool), + ("isEulerAnglesIcsAvailable", ctypes.c_bool), + ("isEulerAnglesCcsAvailable", ctypes.c_bool), + ("isFaceLandmark3D", ctypes.c_bool), + ("isFaceLandmarksConfidenceAvailable", ctypes.c_bool), + ("isFaceGeometricBoxAvailable", ctypes.c_bool), + ("isStatusAvailable", ctypes.c_bool), + ] + +class CFpgaPersonData(ctypes.Structure): + _fields_ = [ + ("personConfidence", ctypes.c_float), + ("personDistance", ctypes.c_int), + ("personPosture", ctypes.c_int), + ("personFrontalPostureConfidence", ctypes.c_float), + ("personNotFrontalPostureConfidence", ctypes.c_float), + ("position", CPoint3i), + ("personBox", CRect2i), + ("isPersonDataAvailable", ctypes.c_bool), + ] + +class CFpgaObjectDetection(ctypes.Structure): + _fields_ = [ + ("objectClass", ctypes.c_int), + ("objectConfidence", ctypes.c_float), + ("objectBox", CRect2i), + ] + +class CFpgaObjectData(ctypes.Structure): + _fields_ = [ + ("numberOfObjects", ctypes.c_int16), + ("objects", CFpgaObjectDetection * EVE_FPGA_MAX_OBJECT_DETECTION), + ] + +class CFpgaUserData(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_int16), + ("status", ctypes.c_int), + ("scale", ctypes.c_float), + ("faceData", CFpgaFaceData), + ("personData", CFpgaPersonData), + ("isIdealUser", ctypes.c_bool), + ("isIdValid", ctypes.c_bool), + ("isStatusAvailable", ctypes.c_bool), + ("isScaleAvailable", ctypes.c_bool), + ] + +class CFpgaFaceIdData(ctypes.Structure): + _fields_ = [ + ("command", ctypes.c_int16), + ("userId", ctypes.c_int16), + ("freeEntry", ctypes.c_int16), + ("statusCode", ctypes.c_int16), + ("faceId", ctypes.c_int16), + ("lastRegisteredFaceID", ctypes.c_int16), + ("usersInGallery", ctypes.c_int16), + ("gallerySize", ctypes.c_int16), + ] + +class CFpgaPipelineData(ctypes.Structure): + _fields_ = [ + ("pipelineType", ctypes.c_int), + ("imageDimensions", CFpgaImageDimensions), + ("dataContent", CFpgaDataContent), + ("userData", CFpgaUserData * EVE_FPGA_MAX_USERS), + ("objectData", CFpgaObjectData), + ("faceId", CFpgaFaceIdData), + ("handsData", CFpgaHandsData), + ("defectData", CFpgaDefectData), + ] + +class CFpgaMessage(ctypes.Structure): + _fields_ = [ + ("responseType", ctypes.c_int), + ("responseVersion", ctypes.c_uint8), + ("serialStatus", ctypes.c_int), + ("serialReadTimeNano", ctypes.c_longlong), + ] + +class CFpgaData(ctypes.Structure): + _fields_ = [ + ("message", CFpgaMessage), + ("pipelineData", CFpgaPipelineData), + ] + +class CFpgaGetSetting(ctypes.Structure): + _fields_ = [ + ("message", CFpgaMessage), + ("type", ctypes.c_int), + ("setting", ctypes.c_int), + ("value", ctypes.c_uint32), + ] + +class CFpgaParameters(ctypes.Structure): + _fields_ = [ + ("comport", ctypes.c_uint), + ("socWakeupDelay", ctypes.c_uint), + ("wakeupType", ctypes.c_int), + ("forceCameraOn", ctypes.c_ubyte), + ("registerNewFace", ctypes.c_ubyte), + ("clearCurrentFace", ctypes.c_ubyte), + ("enableFaceId", ctypes.c_ubyte), + ("allPipelinesSupported", ctypes.c_ubyte), + ("pipelineVersion", ctypes.c_uint), + ("connection", ctypes.c_int), + ("i2cAdapterNumber", ctypes.c_uint), + ("i2cDeviceNumber", ctypes.c_uint), + ("i2cIRQPin", ctypes.c_uint), + ] + +class CFpgaCallbackControl(ctypes.Structure): + _fields_ = [ + ("request", ctypes.c_int), + ] + +class EveFpgaMetadata(ctypes.Structure): + _fields_ = [ + ("data", ctypes.POINTER(CFpgaData)), + ("errorCode", ctypes.c_int), + ] + +class EveFpgaManualData(ctypes.Structure): + _fields_ = [ + ("data", ctypes.POINTER(ctypes.c_ubyte)), + ("size", ctypes.c_int), + ] + +class EveFpgaJsonMetadata(ctypes.Structure): + _fields_ = [ + ("textStart", ctypes.POINTER(ctypes.c_byte)), + ("textSize", ctypes.c_uint), + ("errorCode", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/CHandGesture.py b/shared/eve_python/structs/CHandGesture.py new file mode 100644 index 0000000000000000000000000000000000000000..c4b589d7b3c35e06122ce6bc0b282d6f04ae71bd --- /dev/null +++ b/shared/eve_python/structs/CHandGesture.py @@ -0,0 +1,144 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CBasicStructs import * +from .EveProcessingStatus import * + +EVE_MAX_HAND_DETECTIONS = 8 +EVE_MAX_DYNAMIC_GESTURE_SEQUENCE = 8 +EVE_MAX_CUSTOM_STATIC_GESTURES = 20 +EVE_MAX_STATIC_GESTURES = 40 +EVE_HAND_LANDMARKS_SIZE = 2 +EVE_HAND_LANDMARK_SIZE = 11 +EVE_STATIC_GESTURE_SIZE = 22 +EVE_DYNAMIC_GESTURE_SIZE = 12 + +class EveRunHandLandmarks(CtypesEnum): + EVE_HAND_LANDMARKS_ALL_HANDS = 0 + EVE_HAND_LANDMARKS_MAIN_HAND_ONLY = 1 + EVE_HAND_LANDMARKS_SIZE = 2 + +class EveHandLandmark(CtypesEnum): + EVE_WRIST = 0 + EVE_THUMB_IP = 1 + EVE_THUMB_TIP = 2 + EVE_INDEX_MCP = 3 + EVE_INDEX_TIP = 4 + EVE_MIDDLE_MCP = 5 + EVE_MIDDLE_TIP = 6 + EVE_RING_MCP = 7 + EVE_RING_TIP = 8 + EVE_PINKY_MCP = 9 + EVE_PINKY_TIP = 10 + EVE_HAND_LANDMARK_SIZE = 11 + +class EveGestureQuality(CtypesEnum): + EVE_POOR_QUALITY_LOW_CONFIDENCE = 0 + EVE_POOR_QUALITY_HAND_OVER_FACE = 1 + EVE_GOOD_QUALITY = 2 + +class EveStaticGestureType(CtypesEnum): + EVE_STATIC_GESTURE_NONE = 0 + EVE_OPEN_HAND = 1 + EVE_OPEN_HAND_LEFT = 2 + EVE_OPEN_HAND_RIGHT = 3 + EVE_CLOSED_HAND = 4 + EVE_THUMBS_LEFT = 5 + EVE_THUMBS_RIGHT = 6 + EVE_RESERVED_STATIC_GESTURE_1 = 7 + EVE_RESERVED_STATIC_GESTURE_2 = 8 + EVE_RESERVED_STATIC_GESTURE_3 = 9 + EVE_RESERVED_STATIC_GESTURE_4 = 10 + EVE_RESERVED_STATIC_GESTURE_5 = 11 + EVE_CUSTOM_STATIC_GESTURE_1 = 12 + EVE_CUSTOM_STATIC_GESTURE_2 = 13 + EVE_CUSTOM_STATIC_GESTURE_3 = 14 + EVE_CUSTOM_STATIC_GESTURE_4 = 15 + EVE_CUSTOM_STATIC_GESTURE_5 = 16 + EVE_CUSTOM_STATIC_GESTURE_6 = 17 + EVE_CUSTOM_STATIC_GESTURE_7 = 18 + EVE_CUSTOM_STATIC_GESTURE_8 = 19 + EVE_CUSTOM_STATIC_GESTURE_9 = 20 + EVE_CUSTOM_STATIC_GESTURE_10 = 21 + EVE_STATIC_GESTURE_SIZE = 22 + +class EveDynamicGestureType(CtypesEnum): + EVE_DYNAMIC_GESTURE_NONE = 0 + EVE_GRAB = 1 + EVE_RESERVED_DYNAMIC_GESTURE_1 = 2 + EVE_RESERVED_DYNAMIC_GESTURE_2 = 3 + EVE_RESERVED_DYNAMIC_GESTURE_3 = 4 + EVE_RESERVED_DYNAMIC_GESTURE_4 = 5 + EVE_RESERVED_DYNAMIC_GESTURE_5 = 6 + EVE_CUSTOM_DYNAMIC_GESTURE_1 = 7 + EVE_CUSTOM_DYNAMIC_GESTURE_2 = 8 + EVE_CUSTOM_DYNAMIC_GESTURE_3 = 9 + EVE_CUSTOM_DYNAMIC_GESTURE_4 = 10 + EVE_CUSTOM_DYNAMIC_GESTURE_5 = 11 + EVE_DYNAMIC_GESTURE_SIZE = 12 + +class EveStaticGesture(ctypes.Structure): + _fields_ = [ + ("handId", ctypes.c_int), + ("isMainUserHand", ctypes.c_int), + ("type", ctypes.c_int), + ("confidence", ctypes.c_float), + ("quality", ctypes.c_int), + ] + +class EveStaticGestures(ctypes.Structure): + _fields_ = [ + ("count", ctypes.c_uint), + ("gestures", EveStaticGesture * EVE_MAX_HAND_DETECTIONS), + ] + +class EveStaticGestureDefinition(ctypes.Structure): + _fields_ = [ + ("gestureType", ctypes.c_int), + ("id", ctypes.c_uint), + ("landmarksMap", CPoint2f * EVE_HAND_LANDMARK_SIZE), + ] + +class EveDynamicGesture(ctypes.Structure): + _fields_ = [ + ("handId", ctypes.c_int), + ("isMainUserHand", ctypes.c_int), + ("type", ctypes.c_int), + ("quality", ctypes.c_int), + ] + +class EveDynamicGestures(ctypes.Structure): + _fields_ = [ + ("count", ctypes.c_uint), + ("gestures", EveDynamicGesture * EVE_MAX_HAND_DETECTIONS), + ] + +class EveDynamicGestureDefinition(ctypes.Structure): + _fields_ = [ + ("gestureType", ctypes.c_int), + ("sequenceCount", ctypes.c_uint), + ("gestureSequence", ctypes.c_int * EVE_MAX_DYNAMIC_GESTURE_SEQUENCE), + ] + +class EveSingleHandDetection(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_int), + ("isHandValid", ctypes.c_int), + ("boundingBox", CRect2iWH), + ("boundingBoxScore", ctypes.c_float), + ("landmarksICS", CPoint2f * EVE_HAND_LANDMARK_SIZE), + ("validationScore", ctypes.c_float), + ("inPlaneAngle", ctypes.c_float), + ("depth", ctypes.c_float), + ("isMainUserHand", ctypes.c_int), + ("isInCurrentFrame", ctypes.c_int), + ] + +class EveHandDetections(ctypes.Structure): + _fields_ = [ + ("status", ctypes.c_int), + ("hasFaceROI", ctypes.c_int), + ("faceROI", CRect2fWH), + ("detectedHandCount", ctypes.c_uint), + ("hands", EveSingleHandDetection * EVE_MAX_HAND_DETECTIONS), + ] + diff --git a/shared/eve_python/structs/CImageManipulation.py b/shared/eve_python/structs/CImageManipulation.py new file mode 100644 index 0000000000000000000000000000000000000000..89ebfe21ee394e86c3a5a62126fe27bd68db3010 --- /dev/null +++ b/shared/eve_python/structs/CImageManipulation.py @@ -0,0 +1,12 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class CImageManipulationSettings(ctypes.Structure): + _fields_ = [ + ("mirrorImage", ctypes.c_uint), + ("reserved1", ctypes.c_uint), + ("reserved2", ctypes.c_uint), + ("reserved3", ctypes.c_uint), + ] + diff --git a/shared/eve_python/structs/CKarolinska.py b/shared/eve_python/structs/CKarolinska.py new file mode 100644 index 0000000000000000000000000000000000000000..e19807667ad7f80bf3086928200e5b44a5a3e55e --- /dev/null +++ b/shared/eve_python/structs/CKarolinska.py @@ -0,0 +1,57 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class EveKarolinskaSleepiness(CtypesEnum): + EVE_KAROLINSKA_DISABLED = 0 + EVE_KAROLINSKA_1 = 1 + EVE_KAROLINSKA_2 = 2 + EVE_KAROLINSKA_3 = 3 + EVE_KAROLINSKA_4 = 4 + EVE_KAROLINSKA_5 = 5 + EVE_KAROLINSKA_6 = 6 + EVE_KAROLINSKA_7 = 7 + EVE_KAROLINSKA_8 = 8 + EVE_KAROLINSKA_9 = 9 + EVE_KAROLINSKA_MAX = 10 + +class EveEyeClosureState(CtypesEnum): + EVE_EYE_STATE_UNKNOWN = 0 + EVE_EYE_OPEN = 1 + EVE_EYE_CLOSED = 2 + +class EveKarolinskaStatus(CtypesEnum): + EVE_KAROLINSKA_OFF = 0 + EVE_KAROLINSKA_NO_FACE = 1 + EVE_KAROLINSKA_BLINKS_ONLY = 2 + EVE_KAROLINSKA_ON = 3 + +class CEyeState(ctypes.Structure): + _fields_ = [ + ("state", ctypes.c_int), + ("closure", ctypes.c_float), + ("confidence", ctypes.c_float), + ("eyelidDistanceMM", ctypes.c_float), + ] + +class CEyeStates(ctypes.Structure): + _fields_ = [ + ("left", CEyeState), + ("right", CEyeState), + ("fused", CEyeState), + ("blinkCount", ctypes.c_uint), + ] + +class CKarolinskaData(ctypes.Structure): + _fields_ = [ + ("status", ctypes.c_int), + ("scale", ctypes.c_int), + ("headPitchScale", ctypes.c_int), + ("yawnScale", ctypes.c_int), + ("blinkDurationScale", ctypes.c_int), + ("yawn", ctypes.c_float), + ("yawnConfidence", ctypes.c_float), + ("yawnCount", ctypes.c_uint), + ("eyes", CEyeStates), + ] + diff --git a/shared/eve_python/structs/CLandmarkMaps.py b/shared/eve_python/structs/CLandmarkMaps.py new file mode 100644 index 0000000000000000000000000000000000000000..75adb9a72bc3426e5b80f54a0fba7092ee353198 --- /dev/null +++ b/shared/eve_python/structs/CLandmarkMaps.py @@ -0,0 +1,40 @@ +import ctypes +from ctypes_enum import CtypesEnum + +EVE_HMI_P_L_SIZE = 23 +EVE_HMI_S_L_SIZE = 5 + +class EveHmiPrimaryLandmarks(CtypesEnum): + EVE_HMI_P_L_RIGHT_TEMPLE = 0 + EVE_HMI_P_L_RIGHT_JAW = 1 + EVE_HMI_P_L_CENTER_JAW = 2 + EVE_HMI_P_L_LEFT_JAW = 3 + EVE_HMI_P_L_LEFT_TEMPLE = 4 + EVE_HMI_P_L_RIGHT_EYEBROW = 5 + EVE_HMI_P_L_LEFT_EYEBROW = 6 + EVE_HMI_P_L_NOSE_BRIDGE = 7 + EVE_HMI_P_L_NOSE_TIP_HIGH = 8 + EVE_HMI_P_L_NOSE_TIP = 9 + EVE_HMI_P_L_NOSE_TIP_LOW = 10 + EVE_HMI_P_L_RIGHT_EYE_1 = 11 + EVE_HMI_P_L_RIGHT_EYE_2 = 12 + EVE_HMI_P_L_LEFT_EYE_1 = 13 + EVE_HMI_P_L_LEFT_EYE_2 = 14 + EVE_HMI_P_L_MOUTH_RIGHT_CORNER = 15 + EVE_HMI_P_L_MOUTH_UPPER_LIP = 16 + EVE_HMI_P_L_MOUTH_LEFT_CORNER = 17 + EVE_HMI_P_L_MOUTH_LOWER_LIP = 18 + EVE_HMI_P_L_MOUTH_OPEN_TOP = 19 + EVE_HMI_P_L_MOUTH_OPEN_BOTTOM = 20 + EVE_HMI_P_L_RIGHT_PUPIL = 21 + EVE_HMI_P_L_LEFT_PUPIL = 22 + EVE_HMI_P_L_SIZE = 23 + +class EveHmiSecondaryLandmarks(CtypesEnum): + EVE_HMI_S_L_RIGHT_PUPIL = 0 + EVE_HMI_S_L_LEFT_PUPIL = 1 + EVE_HMI_S_L_NOSE_TIP_LOW = 2 + EVE_HMI_S_L_RIGHT_MOUTH_CORNER = 3 + EVE_HMI_S_L_LEFT_MOUTH_CORNER = 4 + EVE_HMI_S_L_SIZE = 5 + diff --git a/shared/eve_python/structs/CROIStructs.py b/shared/eve_python/structs/CROIStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..b5d7d214c7e7aa51c5e10532c53da98be2e42a8d --- /dev/null +++ b/shared/eve_python/structs/CROIStructs.py @@ -0,0 +1,29 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveProcessingStatus import * + +EVE_ROI_MAX_SCORE_COUNT = 20 + +class EveROIState(CtypesEnum): + EVE_ROI_STATE_INACTIVE = 0 + EVE_ROI_STATE_ENTERING = 1 + EVE_ROI_STATE_LEAVING = 2 + EVE_ROI_STATE_SELECTED = 3 + +class CROIScore(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint), + ("intersectionScore", ctypes.c_double), + ("filteredScore", ctypes.c_double), + ("state", ctypes.c_int), + ] + +class CROIScoreData(ctypes.Structure): + _fields_ = [ + ("processingStatus", ctypes.c_int), + ("fusedRoiScoresCount", ctypes.c_uint), + ("fusedRoiScores", CROIScore * EVE_ROI_MAX_SCORE_COUNT), + ("faceRoiScoresCount", ctypes.c_uint), + ("faceRoiScores", CROIScore * EVE_ROI_MAX_SCORE_COUNT), + ] + diff --git a/shared/eve_python/structs/CScreenLocation.py b/shared/eve_python/structs/CScreenLocation.py new file mode 100644 index 0000000000000000000000000000000000000000..f17be354d233539cdb9e95145c95eb409a54335a --- /dev/null +++ b/shared/eve_python/structs/CScreenLocation.py @@ -0,0 +1,10 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class CScreenLocation(ctypes.Structure): + _fields_ = [ + ("topLeftXInMM", ctypes.c_float), + ("topLeftYInMM", ctypes.c_float), + ] + diff --git a/shared/eve_python/structs/CVisualSpeechStructs.py b/shared/eve_python/structs/CVisualSpeechStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..6696e6c823217ab47a1ba642cbabaa28e163b1f1 --- /dev/null +++ b/shared/eve_python/structs/CVisualSpeechStructs.py @@ -0,0 +1,18 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveProcessingStatus import * + + +class EveVisualSpeechState(CtypesEnum): + EVE_NOT_SET = 0 + EVE_NOT_SPEAKING = 1 + EVE_SPEAKING = 2 + +class CVisualSpeechData(ctypes.Structure): + _fields_ = [ + ("processingStatus", ctypes.c_int), + ("speechState", ctypes.c_int), + ("notSpeakingProbability", ctypes.c_double), + ("speakingProbability", ctypes.c_double), + ] + diff --git a/shared/eve_python/structs/EveAlgorithm.py b/shared/eve_python/structs/EveAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..48b251b546bd62fee87cb03f16e7ced724869aab --- /dev/null +++ b/shared/eve_python/structs/EveAlgorithm.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveAlgorithmStructs import * + + diff --git a/shared/eve_python/structs/EveAlgorithmStructs.py b/shared/eve_python/structs/EveAlgorithmStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..79f7337f8c1699f7c0c18a069be4631224537853 --- /dev/null +++ b/shared/eve_python/structs/EveAlgorithmStructs.py @@ -0,0 +1,14 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CAlgorithms import * +from .EveErrors import * + +EVE_ALGORITHMS_SIZE = 20 + +class EveSupportedAlgorithms(ctypes.Structure): + _fields_ = [ + ("errorCode", ctypes.c_int), + ("count", ctypes.c_uint), + ("algorithms", ctypes.c_int * EVE_ALGORITHMS_SIZE), + ] + diff --git a/shared/eve_python/structs/EveCallbackReturnData.py b/shared/eve_python/structs/EveCallbackReturnData.py new file mode 100644 index 0000000000000000000000000000000000000000..7e05c11672da36c1a4ada3f4d7e4a2dff8b4c32d --- /dev/null +++ b/shared/eve_python/structs/EveCallbackReturnData.py @@ -0,0 +1,13 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class EveRequestedProcessingState(CtypesEnum): + EVE_REQUESTED_PROCESSING_STATE_CONTINUE = 0 + EVE_REQUESTED_PROCESSING_STATE_STOP = 1 + +class EveProcessingCallbackReturnData(ctypes.Structure): + _fields_ = [ + ("requestedState", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveCamera.py b/shared/eve_python/structs/EveCamera.py new file mode 100644 index 0000000000000000000000000000000000000000..49775c99e9b9388610997b20a10dd9c15681ba75 --- /dev/null +++ b/shared/eve_python/structs/EveCamera.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveCameraStructs import * + + diff --git a/shared/eve_python/structs/EveCameraStructs.py b/shared/eve_python/structs/EveCameraStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..31f14735f4cf0c64707920be0c26ec922ad41b9d --- /dev/null +++ b/shared/eve_python/structs/EveCameraStructs.py @@ -0,0 +1,27 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CCameraStructs import * +from .EveErrors import * + +EVE_CAMERA_FORMATS_SIZE = 20 + +class EveCameraFormats(ctypes.Structure): + _fields_ = [ + ("formats", CCameraFormat * EVE_CAMERA_FORMATS_SIZE), + ("formatsCount", ctypes.c_uint), + ("hadMoreFormats", ctypes.c_uint), + ("error", ctypes.c_int), + ] + +class EveCamera(ctypes.Structure): + _fields_ = [ + ("data", CCamera), + ("error", ctypes.c_int), + ] + +class EveNumberOfCameras(ctypes.Structure): + _fields_ = [ + ("count", ctypes.c_uint), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveConfigurationParameters.py b/shared/eve_python/structs/EveConfigurationParameters.py new file mode 100644 index 0000000000000000000000000000000000000000..28b28be60335a27a703be2a87e5fb3103c6ea37b --- /dev/null +++ b/shared/eve_python/structs/EveConfigurationParameters.py @@ -0,0 +1,36 @@ +import ctypes +from ctypes_enum import CtypesEnum + +EVE_PIPELINE_TYPE_SIZE = 2 + +class EveImageProvider(CtypesEnum): + EVE_CAMERA = 0 + EVE_CLIENT_PROVIDED = 1 + +class EveGpuPreference(CtypesEnum): + EVE_GPU_LOW_POWER = 0 + EVE_GPU_HIGH_PERFORMANCE = 1 + EVE_NO_GPU = 2 + +class EveStartupType(CtypesEnum): + EVE_SYNC = 0 + EVE_ASYNC = 1 + +class EveProcessingPipelineType(CtypesEnum): + EVE_FULL = 0 + EVE_HMI = 1 + EVE_PIPELINE_TYPE_SIZE = 2 + +class EveStartupParameters(ctypes.Structure): + _fields_ = [ + ("gpuPreference", ctypes.c_int), + ("imageProvider", ctypes.c_int), + ("startupType", ctypes.c_int), + ("pathOverride", ctypes.c_byte * 512), + ] + +class EveProcessingParameters(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveControlInterface.py b/shared/eve_python/structs/EveControlInterface.py new file mode 100644 index 0000000000000000000000000000000000000000..7bf10108a1e3e3d1ff5f13559acdaa2271e16323 --- /dev/null +++ b/shared/eve_python/structs/EveControlInterface.py @@ -0,0 +1,9 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CCameraStructs import * +from .EveCallbackReturnData import * +from .EveConfigurationParameters import * +from .EveErrors import * +from .EveImageStructs import * + + diff --git a/shared/eve_python/structs/EveControlOption.py b/shared/eve_python/structs/EveControlOption.py new file mode 100644 index 0000000000000000000000000000000000000000..33566e29d3f3700761107a9985aaed7887894a32 --- /dev/null +++ b/shared/eve_python/structs/EveControlOption.py @@ -0,0 +1,8 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class EveOptionEnabled(CtypesEnum): + EVE_OPTION_DISABLED = 0 + EVE_OPTION_ENABLED = 1 + diff --git a/shared/eve_python/structs/EveErrors.py b/shared/eve_python/structs/EveErrors.py new file mode 100644 index 0000000000000000000000000000000000000000..2f0ce4f3af7bd4e71ceccab3af7aa01a37783f60 --- /dev/null +++ b/shared/eve_python/structs/EveErrors.py @@ -0,0 +1,22 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class EveError(CtypesEnum): + EVE_ERROR_NO_ERROR = 0 + EVE_ERROR_NOT_CREATED = 1 + EVE_ERROR_NOT_STARTED = 2 + EVE_ERROR_PIPELINE_NOT_FOUND = 3 + EVE_ERROR_CAMERA_MANAGER_NOT_FOUND = 4 + EVE_ERROR_NO_CALLBACK = 5 + EVE_ERROR_NOT_ACCESSED_FROM_CALLBACK = 6 + EVE_INVALID_IMAGE_ENCODING = 7 + EVE_CALLBACK_WITHOUT_CONNECTING_TO_CAMERA = 8 + EVE_CAMERA_INTERACTION_WITHOUT_CAMERA = 9 + EVE_NO_MORE_DATA = 10 + EVE_INVALID_CAMERA_ID = 11 + EVE_FACE_ID_INVALID_THRESHOLD = 12 + EVE_ERROR_NO_CAMERA_INTERACTION_WITH_CAMERA = 13 + EVE_ERROR_NOT_IMPLEMENTED = 14 + EVE_ERROR_UNSUPPORTED_FORMAT = 15 + diff --git a/shared/eve_python/structs/EveFaceId.py b/shared/eve_python/structs/EveFaceId.py new file mode 100644 index 0000000000000000000000000000000000000000..41e0a0c46161be056c89e3ec50edb1ff968658d9 --- /dev/null +++ b/shared/eve_python/structs/EveFaceId.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveFaceIdStructs import * + + diff --git a/shared/eve_python/structs/EveFaceIdStructs.py b/shared/eve_python/structs/EveFaceIdStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..d7cc0802aa9a7821966d236165fe2afc86c9e70a --- /dev/null +++ b/shared/eve_python/structs/EveFaceIdStructs.py @@ -0,0 +1,31 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CFaceIdStructs import * +from .EveControlOption import * +from .EveErrors import * + + +class EveFaceIdCalibrationPoseMode(CtypesEnum): + EVE_FACEID_CALIBRATION_FRONTAL_ONLY = 1 + +class EveFaceIdCommandStruct(ctypes.Structure): + _fields_ = [ + ("command", ctypes.c_int), + ("errorCode", ctypes.c_int), + ] + +class EveFaceIdOptions(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_int), + ("calibrationPoses", ctypes.c_int), + ("galleryPath", ctypes.c_byte * 256), + ("threshold", ctypes.c_float), + ("error", ctypes.c_int), + ] + +class EveFaceIdData(ctypes.Structure): + _fields_ = [ + ("data", CFaceIdentityData), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveFaceTracker.py b/shared/eve_python/structs/EveFaceTracker.py new file mode 100644 index 0000000000000000000000000000000000000000..9e44ace41a23a2bf1436064f216ea8bb2599f1e8 --- /dev/null +++ b/shared/eve_python/structs/EveFaceTracker.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveFaceTrackerStructs import * + + diff --git a/shared/eve_python/structs/EveFaceTrackerStructs.py b/shared/eve_python/structs/EveFaceTrackerStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..945706c8a086979972abbf655689796f893287ab --- /dev/null +++ b/shared/eve_python/structs/EveFaceTrackerStructs.py @@ -0,0 +1,40 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CFaceData import * +from .EveErrors import * + + +class EveFaceTrackerMinimumMode(CtypesEnum): + EVE_FACETRACKER_MINIMUM_MODE_OFF = 0 + EVE_FACETRACKER_MINIMUM_MODE_MINIMAL = 1 + EVE_FACETRACKER_MINIMUM_MODE_AVERAGE = 2 + EVE_FACETRACKER_MINIMUM_MODE_MAXIMAL = 3 + +class EveFaceTrackerOptions(ctypes.Structure): + _fields_ = [ + ("faceTrackerMode", ctypes.c_int), + ("enableEyeLandmarks", ctypes.c_uint), + ("enable3DFaceTracking", ctypes.c_uint), + ("enablePersonDetection", ctypes.c_uint), + ("fitSecondaryUsers", ctypes.c_uint), + ("error", ctypes.c_int), + ] + +class EveEyes(ctypes.Structure): + _fields_ = [ + ("data", CEyeLandmarks), + ("errorCode", ctypes.c_int), + ] + +class EvePupils(ctypes.Structure): + _fields_ = [ + ("data", CPupilLandmarks), + ("errorCode", ctypes.c_int), + ] + +class EveAllFacesData(ctypes.Structure): + _fields_ = [ + ("faceData", ctypes.POINTER(CAllFaces)), + ("errorCode", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveFpga.py b/shared/eve_python/structs/EveFpga.py new file mode 100644 index 0000000000000000000000000000000000000000..2a60a0e27970002f9569df0690b34267f7c5c1bc --- /dev/null +++ b/shared/eve_python/structs/EveFpga.py @@ -0,0 +1,7 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CFpgaData import * +from .EveFpgaStructs import * +from .EveImageStructs import * + + diff --git a/shared/eve_python/structs/EveFpgaStructs.py b/shared/eve_python/structs/EveFpgaStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..cf9f898a3e4389dc592b85fba74389ec21aa8ae1 --- /dev/null +++ b/shared/eve_python/structs/EveFpgaStructs.py @@ -0,0 +1,24 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CFpgaData import * +from .EveErrors import * + + +class EveFpgaOptions(ctypes.Structure): + _fields_ = [ + ("parameters", CFpgaParameters), + ("error", ctypes.c_int), + ] + +class EveFpgaDebugOptions(ctypes.Structure): + _fields_ = [ + ("enableDrawingOnImage", ctypes.c_uint), + ("error", ctypes.c_int), + ] + +class EveFpgaData(ctypes.Structure): + _fields_ = [ + ("data", ctypes.POINTER(CFpgaData)), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveHandGesture.py b/shared/eve_python/structs/EveHandGesture.py new file mode 100644 index 0000000000000000000000000000000000000000..f6a4adf6725096c11c7dbcef0c47303f0d45556e --- /dev/null +++ b/shared/eve_python/structs/EveHandGesture.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveHandGestureStructs import * + + diff --git a/shared/eve_python/structs/EveHandGestureStructs.py b/shared/eve_python/structs/EveHandGestureStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..825fe46f65c4eabc91250787f2397af33cd87237 --- /dev/null +++ b/shared/eve_python/structs/EveHandGestureStructs.py @@ -0,0 +1,48 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CHandGesture import * +from .EveControlOption import * +from .EveErrors import * + + +class EveHandGestureOptions(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_int), + ("redetectionDelay", ctypes.c_int), + ("async", ctypes.c_int), + ("run", ctypes.c_int), + ("errorCode", ctypes.c_int), + ] + +class EveHandGestureData(ctypes.Structure): + _fields_ = [ + ("hands", ctypes.POINTER(EveHandDetections)), + ("errorCode", ctypes.c_int), + ] + +class EveStaticGestureData(ctypes.Structure): + _fields_ = [ + ("gestures", EveStaticGestures), + ("errorCode", ctypes.c_int), + ] + +class EveDynamicGestureData(ctypes.Structure): + _fields_ = [ + ("gestures", EveDynamicGestures), + ("errorCode", ctypes.c_int), + ] + +class EveStaticGestureDefinitions(ctypes.Structure): + _fields_ = [ + ("count", ctypes.c_uint), + ("errorCode", ctypes.c_int), + ("definitions", EveStaticGestureDefinition * EVE_MAX_STATIC_GESTURES), + ] + +class EveDynamicGestureDefinitions(ctypes.Structure): + _fields_ = [ + ("count", ctypes.c_uint), + ("errorCode", ctypes.c_int), + ("definitions", EveDynamicGestureDefinition * EVE_DYNAMIC_GESTURE_SIZE), + ] + diff --git a/shared/eve_python/structs/EveImage.py b/shared/eve_python/structs/EveImage.py new file mode 100644 index 0000000000000000000000000000000000000000..3071fb0c5b29058ae26e4130bfa0d51220b84388 --- /dev/null +++ b/shared/eve_python/structs/EveImage.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveImageStructs import * + + diff --git a/shared/eve_python/structs/EveImageManipulation.py b/shared/eve_python/structs/EveImageManipulation.py new file mode 100644 index 0000000000000000000000000000000000000000..1bb76a79a9a8040c206b8e21a973b926f9757004 --- /dev/null +++ b/shared/eve_python/structs/EveImageManipulation.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveImageManipulationStructs import * + + diff --git a/shared/eve_python/structs/EveImageManipulationStructs.py b/shared/eve_python/structs/EveImageManipulationStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..1139a637db637598d3882037a756db705ae0e155 --- /dev/null +++ b/shared/eve_python/structs/EveImageManipulationStructs.py @@ -0,0 +1,12 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CImageManipulation import * +from .EveErrors import * + + +class EveImageManipulationOptions(ctypes.Structure): + _fields_ = [ + ("settings", CImageManipulationSettings), + ("errorCode", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveImageStructs.py b/shared/eve_python/structs/EveImageStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..d80ed90f4d66fcd8e25d74b6bcacc3c07c36a7b6 --- /dev/null +++ b/shared/eve_python/structs/EveImageStructs.py @@ -0,0 +1,38 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CBasicStructs import * +from .EveErrors import * + + +class EveInputImage(ctypes.Structure): + _fields_ = [ + ("data", ctypes.POINTER(ctypes.c_ubyte)), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("encoding", ctypes.c_int), + ] + +class EveProcessedImage(ctypes.Structure): + _fields_ = [ + ("data", ctypes.POINTER(ctypes.c_ubyte)), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("channels", ctypes.c_int), + ("timestamp", ctypes.c_longlong), + ("error", ctypes.c_int), + ("location", ctypes.c_int), + ] + +class EveProcessedFrameTime(ctypes.Structure): + _fields_ = [ + ("frameTime", ctypes.c_double), + ("error", ctypes.c_int), + ] + +class EveImageFormatRequest(ctypes.Structure): + _fields_ = [ + ("format", ctypes.c_int), + ("location", ctypes.c_int), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveKarolinska.py b/shared/eve_python/structs/EveKarolinska.py new file mode 100644 index 0000000000000000000000000000000000000000..8c03da5d42b45addcc836dd33a1c38c8faeb7da6 --- /dev/null +++ b/shared/eve_python/structs/EveKarolinska.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveKarolinskaStructs import * + + diff --git a/shared/eve_python/structs/EveKarolinskaStructs.py b/shared/eve_python/structs/EveKarolinskaStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..c36c40bab0b3d099536c793465d9ac87c6f579e6 --- /dev/null +++ b/shared/eve_python/structs/EveKarolinskaStructs.py @@ -0,0 +1,19 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CKarolinska import * +from .EveControlOption import * +from .EveErrors import * + + +class EveKarolinskaOptions(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_int), + ("error", ctypes.c_int), + ] + +class EveKarolinskaData(ctypes.Structure): + _fields_ = [ + ("data", CKarolinskaData), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveObjectDetection.py b/shared/eve_python/structs/EveObjectDetection.py new file mode 100644 index 0000000000000000000000000000000000000000..6c2a7c8fe5e98e1f960ccd3ead229675bf52efab --- /dev/null +++ b/shared/eve_python/structs/EveObjectDetection.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveObjectDetectionStructs import * + + diff --git a/shared/eve_python/structs/EveObjectDetectionStructs.py b/shared/eve_python/structs/EveObjectDetectionStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..900cf2cd68b96063f87e6cb4d601fe7c2e1c9945 --- /dev/null +++ b/shared/eve_python/structs/EveObjectDetectionStructs.py @@ -0,0 +1,25 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CDetectionStructs import * +from .EveControlOption import * +from .EveErrors import * + + +class EveObjectDetectionOptions(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_int), + ("error", ctypes.c_int), + ] + +class EvePersonDetectionOptions(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_int), + ("error", ctypes.c_int), + ] + +class EveDetectionData(ctypes.Structure): + _fields_ = [ + ("data", ctypes.POINTER(CDetectionData)), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveProcessingStatus.py b/shared/eve_python/structs/EveProcessingStatus.py new file mode 100644 index 0000000000000000000000000000000000000000..7d9fdf8a0dadf4c38d227051893e049ac28b7e92 --- /dev/null +++ b/shared/eve_python/structs/EveProcessingStatus.py @@ -0,0 +1,10 @@ +import ctypes +from ctypes_enum import CtypesEnum + + +class EveProcessingStatus(CtypesEnum): + EVE_PROCESSING_DISABLED = 0 + EVE_PROCESSING_ENABLED_FAILURE = 1 + EVE_PROCESSING_ENABLED_SUCCESS = 2 + EVE_SOURCED_FROM_FPGA = 3 + diff --git a/shared/eve_python/structs/EveROI.py b/shared/eve_python/structs/EveROI.py new file mode 100644 index 0000000000000000000000000000000000000000..d577ee75757cb31b6c9f75095307f418d64f6ca3 --- /dev/null +++ b/shared/eve_python/structs/EveROI.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveROIStructs import * + + diff --git a/shared/eve_python/structs/EveROIStructs.py b/shared/eve_python/structs/EveROIStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..5b162b80ff69a26dcae31de8c24411f1c8c3175f --- /dev/null +++ b/shared/eve_python/structs/EveROIStructs.py @@ -0,0 +1,34 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CROIStructs import * +from .EveControlOption import * +from .EveErrors import * + +EVE_ROI_MAX_COUNT = 20 + +class EveROI(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint), + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("scoreThresholdForInactive", ctypes.c_double), + ] + +class EveROIOptions(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_int), + ("roiSelectionResponseTime", ctypes.c_double), + ("roiCount", ctypes.c_uint), + ("rois", EveROI * EVE_ROI_MAX_COUNT), + ("error", ctypes.c_int), + ("headVectorOnly", ctypes.c_uint), + ] + +class EveROIScoreData(ctypes.Structure): + _fields_ = [ + ("data", CROIScoreData), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveScreenLocation.py b/shared/eve_python/structs/EveScreenLocation.py new file mode 100644 index 0000000000000000000000000000000000000000..e6de027cf7da7c871606ecb10c932387bdbfdbcf --- /dev/null +++ b/shared/eve_python/structs/EveScreenLocation.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveScreenLocationStructs import * + + diff --git a/shared/eve_python/structs/EveScreenLocationStructs.py b/shared/eve_python/structs/EveScreenLocationStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..37f342fb134ee6d99739ce53a6062413bf78dc41 --- /dev/null +++ b/shared/eve_python/structs/EveScreenLocationStructs.py @@ -0,0 +1,18 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .CScreenLocation import * +from .EveErrors import * + + +class EveScreenLocationOptions(ctypes.Structure): + _fields_ = [ + ("topLeftX", ctypes.c_float), + ("topLeftY", ctypes.c_float), + ] + +class EveScreenLocationData(ctypes.Structure): + _fields_ = [ + ("data", CScreenLocation), + ("error", ctypes.c_int), + ] + diff --git a/shared/eve_python/structs/EveTiming.py b/shared/eve_python/structs/EveTiming.py new file mode 100644 index 0000000000000000000000000000000000000000..7185b439663dec5a7eb3cf4bf782c96632e6beba --- /dev/null +++ b/shared/eve_python/structs/EveTiming.py @@ -0,0 +1,5 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveTimingStructs import * + + diff --git a/shared/eve_python/structs/EveTimingStructs.py b/shared/eve_python/structs/EveTimingStructs.py new file mode 100644 index 0000000000000000000000000000000000000000..b70c1a85193935851802a998c26bcc46df8fd063 --- /dev/null +++ b/shared/eve_python/structs/EveTimingStructs.py @@ -0,0 +1,28 @@ +import ctypes +from ctypes_enum import CtypesEnum +from .EveErrors import * + +TIME_SOURCE_SIZE = 16 + + +class EveTiming(ctypes.Structure): + _fields_ = [ + ("errorCode", ctypes.c_int), + ("eveCameraImageAcquisitionTimepoint", ctypes.c_longlong), + ("eveAlgorithmDuration", ctypes.c_longlong), + ("time1", ctypes.c_longlong), + ("time1Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ("time2", ctypes.c_longlong), + ("time2Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ("time3", ctypes.c_longlong), + ("time3Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ("time4", ctypes.c_longlong), + ("time4Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ("time5", ctypes.c_longlong), + ("time5Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ("time6", ctypes.c_longlong), + ("time6Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ("time7", ctypes.c_longlong), + ("time7Source", ctypes.c_byte * TIME_SOURCE_SIZE), + ] + diff --git a/shared/eve_python/structs/README.txt b/shared/eve_python/structs/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f0e9f132c045a80a27667eab0b6e1438dec8361 --- /dev/null +++ b/shared/eve_python/structs/README.txt @@ -0,0 +1,6 @@ +Run PythonCTypeGenerator.py to generate CType structs from EveCStructs/include/ and EveSDK/include directories. + +The Visual Studio solution generated by CMake will run this command automatically during the install step. +We allow these files to be committed to Git to make life easier for python devs, it is preferrable you do not +modify these files manually, since they will just get overridden by the next time someone changes the C includes +and runs the install step again. diff --git a/shared/eve_worker_pool.py b/shared/eve_worker_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..bd1c42bcc8492e0240736660bf0cf0287a15208a --- /dev/null +++ b/shared/eve_worker_pool.py @@ -0,0 +1,1115 @@ +"""Multi-process Eve SDK worker pool. + +Spawns N child processes, each hosting an independent ``EveWrapper`` instance +with its own ``ctypes.CDLL`` handle. The main Gradio process communicates with +workers via ``multiprocessing.Pipe`` (one bidirectional pipe per worker). + +Frame data is serialised as raw bytes (``ndarray.tobytes()``) rather than +pickled ``ndarray`` objects for speed (~1-2 ms for a 1280x720x3 frame). + +Usage:: + + pool = EveWorkerPool(max_workers=4, ram_headroom_gb=2.0) + worker = pool.acquire(session_hash="abc123") + try: + result = worker.send_inference(frame, features) + finally: + pool.release(worker) +""" + +import atexit +import logging +import multiprocessing as mp +import os +import threading +import time +from collections.abc import Callable +from dataclasses import dataclass +from multiprocessing.connection import Connection + +import numpy as np +from eve_messages import ( + CalibrateNewUserCmd, + CalibrateOkResponse, + CalibrationResultMsg, + EnableFaceIdCmd, + ErrorResponse, + FeatureFlags, + GalleryRestoredResponse, + GetProfileStatsCmd, + GetTimingStatsCmd, + HeartbeatResponse, + InferenceCmd, + InferenceResponse, + OkResponse, + ProcessVideoCmd, + ProfileStatsResponse, + ProgressResponse, + ReadyResponse, + RemoveAllUsersCmd, + RemoveUsersOkResponse, + RestoreGalleryCmd, + RestoreGalleryOkResponse, + SerializedFrame, + ShutdownCmd, + StartProfilingCmd, + StopProfilingCmd, + TimingStatsResponse, + VideoProcessingDoneResponse, + WorkerCmd, + WorkerResponse, +) +from log_utils import setup_logger + +logger = setup_logger("EveWorkerPool") + +# Cached per-process so the probe runs once per worker, not per video. +_h264_encoder_cache: tuple[str, dict[str, str]] | None = None + + +def _get_h264_encoder() -> tuple[str, dict[str, str]]: + """Return (codec_name, codec_options) for H.264 encoding. + + Tries NVENC (GPU) first, falls back to libx264 (CPU). + """ + global _h264_encoder_cache + if _h264_encoder_cache is not None: + return _h264_encoder_cache + + import logging + + import av + + log = logging.getLogger("EveWorkerPool") + try: + ctx = av.CodecContext.create("h264_nvenc", "w") + ctx.width = 64 + ctx.height = 64 + ctx.pix_fmt = "yuv420p" + ctx.open() + ctx.close() + log.info("Using GPU encoder: h264_nvenc") + _h264_encoder_cache = ( + "h264_nvenc", + {"profile": "baseline", "preset": "p1", "delay": "0"}, + ) + except Exception: + log.info("GPU encoder unavailable, falling back to libx264") + _h264_encoder_cache = ( + "libx264", + {"profile": "baseline", "level": "3.1", "preset": "ultrafast", "threads": "1"}, + ) + return _h264_encoder_cache + + +def log_worker_activity( + log: logging.Logger, + action: str, + feature: str, + pool: "EveWorkerPool", + worker_id: int | None = None, + note: str = "", +) -> None: + """Single-line worker activity log for tracking acquire/release/queue events.""" + busy = pool.worker_count - pool.idle_count + w = f" w{worker_id}" if worker_id is not None else "" + n = f" ({note})" if note else "" + log.info( + f"[worker] {action}{w} for {feature}{n} " + f"[busy={busy} idle={pool.idle_count} wait={pool.waiting_count} " + f"sessions={pool.session_count}]" + ) + + +def compute_fps_sleep( + elapsed: float, + max_fps: float | None, + min_fps: float | None, + load: float, +) -> float: + """Compute how long to sleep after one frame to stay within the FPS cap. + + Linearly interpolates the target cycle time between ``1/max_fps`` + (at *load* 0) and ``1/min_fps`` (at *load* 1), then subtracts the + time already spent. + + Used by both the live-inference bridge and the video-processing loop + so that the throttle logic lives in one place. + """ + if max_fps is None: + return 0.0 + effective_min = min_fps if min_fps is not None else max_fps + max_period = 1.0 / max_fps + min_period = 1.0 / effective_min + target_period = max_period + load * (min_period - max_period) + return max(0.0, target_period - elapsed) + + +# Use 'spawn' on all platforms so each child gets a fresh interpreter +# (required on Windows; avoids CDLL sharing on Linux/fork). +_mp_ctx = mp.get_context("spawn") + +DO_PROBE_WORKER = ( + False # Set to False to skip the RAM probe and just use an estimate of the RAM used +) + + +# --------------------------------------------------------------------------- +# Worker process entry point (runs in the child) +# --------------------------------------------------------------------------- + + +def _eve_worker_main( + conn: Connection, + eve_bin_path: str, + eve_lib_path: str, + worker_id: int, + max_jobs_per_worker: int | None = None, + shared_load: "mp.Value | None" = None, +) -> None: + """Top-level function executed inside each worker ``Process``. + + Communicates with the main process exclusively through *conn*. + *shared_load* is a cross-process float (0.0–1.0) updated by the pool + on acquire/release, used by the video processing loop to throttle. + """ + import ctypes + import gc + import platform + import signal + import sys + from pathlib import Path + + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # CPU affinity (for stress testing) + affinity_env = os.environ.get(f"WORKER{worker_id}_AFFINITY") + if affinity_env: + cpu_id = int(affinity_env.removeprefix("CPU")) + import psutil + + psutil.Process().cpu_affinity([cpu_id]) + logger.info(f"Worker {worker_id} pinned to CPU {cpu_id}") + + # Ensure shared package is importable inside the child + shared_dir = str(Path(__file__).resolve().parent) + if shared_dir not in sys.path: + sys.path.insert(0, shared_dir) + + from eve_wrapper import EveWrapper # noqa: E402 (deferred import) + from frame_utils import load_media_frames_raw # noqa: E402 + + eve: EveWrapper | None = None + try: + eve = EveWrapper(eve_bin_path, eve_lib_path) + conn.send(ReadyResponse(pid=os.getpid())) + except Exception as exc: + conn.send(ErrorResponse(error=str(exc))) + return + + # Optional cProfile (enabled via ENABLE_PROFILER env var) + profiler: "cProfile.Profile | None" = None + profiling_enabled = os.environ.get("ENABLE_PROFILER", "").strip() not in ("", "0", "false") + if profiling_enabled: + import cProfile + + # Use process_time so I/O wait (pipe polling) doesn't dominate the profile + profiler = cProfile.Profile(timer=time.process_time) + + job_count = 0 + while True: + try: + if not conn.poll(timeout=2.0): + # No command received — send heartbeat + try: + conn.send(HeartbeatResponse()) + except (BrokenPipeError, OSError): + break + continue + + cmd: WorkerCmd = conn.recv() + except (EOFError, OSError): + break + + if isinstance(cmd, ShutdownCmd): + conn.send(OkResponse()) + break + + try: + if isinstance(cmd, InferenceCmd): + frame = np.frombuffer(cmd.frame_bytes, dtype=cmd.dtype).reshape(cmd.shape) + features = cmd.features + del cmd # free recv'd bytes early; numpy holds its own ref + + eve.enable_face_and_person_detection( + faceEnabled=features.face_detection, + personEnabled=features.person_detection, + ) + eve.enable_face_id(enabled=features.face_id) + eve.enable_hand_gesture(enabled=features.hand_gesture) + eve.enable_object_detection(model_name=features.mod_model) + eve.enable_mirror(True) + result = eve.inference(frame) + del frame + + conn.send( + InferenceResponse( + frame_bytes=result.tobytes(), + shape=result.shape, + dtype=str(result.dtype), + ) + ) + del result + + elif isinstance(cmd, CalibrateNewUserCmd): + frames = _deserialize_frames(cmd.frames_data) + result = eve.calibrate_new_user(frames) + conn.send( + CalibrateOkResponse( + result=CalibrationResultMsg(result.success, result.user_id, result.message), + ) + ) + + elif isinstance(cmd, RemoveAllUsersCmd): + ok = eve.remove_all_users() + conn.send(RemoveUsersOkResponse(result=ok)) + + elif isinstance(cmd, RestoreGalleryCmd): + frames_per_user = [_deserialize_frames(fd) for fd in cmd.frames_per_user_data] + eve.remove_all_users() + results = eve.restore_gallery(frames_per_user) + conn.send( + RestoreGalleryOkResponse( + results=[ + CalibrationResultMsg(r.success, r.user_id, r.message) for r in results + ], + ) + ) + + elif isinstance(cmd, EnableFaceIdCmd): + eve.enable_face_id(enabled=cmd.enabled) + conn.send(OkResponse()) + + elif isinstance(cmd, ProcessVideoCmd): + import av + import cv2 + from fractions import Fraction + + features = cmd.features + + # Gallery restore (if requested) — must happen before + # applying user feature flags because restore_gallery() + # internally enables face/person detection for calibration. + gallery_results: list[CalibrationResultMsg] = [] + if cmd.remove_all_users: + eve.remove_all_users() + if cmd.gallery_paths: + frames_per_user = [load_media_frames_raw(p) for p in cmd.gallery_paths] + raw_results = eve.restore_gallery(frames_per_user) + del frames_per_user + gallery_results = [ + CalibrationResultMsg(r.success, r.user_id, r.message) for r in raw_results + ] + del raw_results + conn.send(GalleryRestoredResponse(results=gallery_results)) + + # Apply user's feature flags after gallery restore so they + # aren't overridden by restore_gallery's internal SDK calls. + eve.enable_face_and_person_detection( + faceEnabled=features.face_detection, + personEnabled=features.person_detection, + ) + eve.enable_face_id(enabled=features.face_id) + eve.enable_hand_gesture(enabled=features.hand_gesture) + eve.enable_object_detection(model_name=features.mod_model) + eve.enable_mirror(False) + + # Video processing loop + cap = cv2.VideoCapture(cmd.input_path) + codec_name, codec_opts = _get_h264_encoder() + container = av.open(cmd.output_path, mode="w", options={"movflags": "faststart"}) + stream = container.add_stream(codec_name, rate=round(cmd.fps)) + stream.time_base = Fraction(1, round(cmd.fps)) + stream.width = cmd.width + stream.height = cmd.height + stream.pix_fmt = "yuv420p" + stream.codec_context.options = codec_opts + + frame_count = 0 + v_max_fps = cmd.max_fps + v_min_fps = cmd.min_fps if cmd.min_fps is not None else v_max_fps + try: + while True: + ret, frame = cap.read() + if not ret: + break + t0 = time.monotonic() + result = eve.inference(frame) + vf = av.VideoFrame.from_ndarray(result, format="bgr24") + vf.pts = frame_count + for pkt in stream.encode(vf): + container.mux(pkt) + del pkt + del result, vf, frame + frame_count += 1 + if frame_count % 10 == 0: + conn.send( + ProgressResponse( + current=frame_count, + total=cmd.total_frames, + ) + ) + load = shared_load.value if shared_load is not None else 0.0 + # FPS throttle (only if there's some load already) + if load > 0.0: + sleep = compute_fps_sleep( + time.monotonic() - t0, v_max_fps, v_min_fps, load + ) + if sleep > 0: + time.sleep(sleep) + # Flush encoder + for pkt in stream.encode(): + container.mux(pkt) + del pkt + finally: + cap.release() + container.close() + del ( + cap, + container, + stream, + ) + + job_count += 1 + conn.send( + VideoProcessingDoneResponse( + frames_processed=frame_count, + gallery_results=gallery_results, + recycle=max_jobs_per_worker is not None + and job_count >= max_jobs_per_worker, + ) + ) + + del cmd + gc.collect() + if platform.system() == "Linux": + try: + ctypes.CDLL("libc.so.6").malloc_trim(0) + except Exception: + pass + + if max_jobs_per_worker is not None and job_count >= max_jobs_per_worker: + logger.info(f"Worker {worker_id} recycling after {job_count} jobs") + break + + elif isinstance(cmd, StartProfilingCmd): + if profiler is not None: + profiler.enable() + conn.send(OkResponse()) + + elif isinstance(cmd, StopProfilingCmd): + if profiler is not None: + profiler.disable() + conn.send(OkResponse()) + + elif isinstance(cmd, GetProfileStatsCmd): + if profiler is not None: + import io + import marshal + import pstats + + profiler.disable() + stream = io.StringIO() + ps = pstats.Stats(profiler, stream=stream) + conn.send(ProfileStatsResponse(stats_data=marshal.dumps(ps.stats))) + else: + conn.send(ProfileStatsResponse(stats_data=b"")) + + elif isinstance(cmd, GetTimingStatsCmd): + conn.send(TimingStatsResponse(stats=eve.get_timing_stats(reset=cmd.reset))) + + else: + conn.send(ErrorResponse(error=f"Unknown command: {cmd}")) + + except Exception as exc: + logger.error(f"Worker {worker_id} error handling '{type(cmd).__name__}': {exc}") + try: + conn.send(ErrorResponse(error=str(exc))) + except (BrokenPipeError, OSError): + break + + # Clean shutdown + if eve is not None: + eve.shutdown() + + +def _serialize_frames(frames: list[np.ndarray]) -> list[SerializedFrame]: + """Convert a list of ndarrays to a picklable representation.""" + return [SerializedFrame(data=f.tobytes(), shape=f.shape, dtype=str(f.dtype)) for f in frames] + + +def _deserialize_frames(data: list[SerializedFrame]) -> list[np.ndarray]: + """Reconstruct ndarrays from their serialized form.""" + return [np.frombuffer(d.data, dtype=d.dtype).reshape(d.shape) for d in data] + + +# --------------------------------------------------------------------------- +# EveWorker — main-process handle for one child process +# --------------------------------------------------------------------------- + + +class EveWorker: + """Main-process proxy for a single worker process. + + Not thread-safe by itself — the ``EveWorkerPool`` ensures only one thread + accesses a worker at a time. + """ + + def __init__( + self, + process: mp.Process, + conn: Connection, + worker_id: int, + ): + self.process = process + self._conn = conn + self.worker_id = worker_id + self.status: str = "idle" # idle | busy | dead + self.session_hash: str | None = None + self.busy_since: float | None = None + self.last_heartbeat: float = time.monotonic() + self.is_live_stream: bool = False + self.pending_recycle: bool = False + + # -- command helpers --------------------------------------------------- + + def _recv_response(self) -> WorkerResponse: + """Receive the next non-heartbeat response from the worker.""" + while True: + try: + resp: WorkerResponse = self._conn.recv() + except (EOFError, OSError): + raise RuntimeError(f"Worker {self.worker_id} pipe closed") + except Exception as exc: + self.pending_recycle = True + raise RuntimeError( + f"Worker {self.worker_id} pipe corrupt: {exc}" + ) from exc + if isinstance(resp, HeartbeatResponse): + self.last_heartbeat = time.monotonic() + continue + return resp + + def _expect_ok(self, context: str) -> WorkerResponse: + """Receive a response and raise on error.""" + resp = self._recv_response() + if isinstance(resp, ErrorResponse): + raise RuntimeError(f"Worker {self.worker_id} {context} error: {resp.error}") + return resp + + def send_inference(self, frame: np.ndarray, features: FeatureFlags) -> np.ndarray: + """Send a frame for inference and return the annotated result.""" + self._conn.send( + InferenceCmd( + frame_bytes=frame.tobytes(), + shape=frame.shape, + dtype=str(frame.dtype), + features=features, + ) + ) + resp = self._expect_ok("inference") + if not isinstance(resp, InferenceResponse): + raise RuntimeError(f"Worker {self.worker_id} inference: unexpected {resp}") + return np.frombuffer(resp.frame_bytes, dtype=resp.dtype).reshape(resp.shape) + + + def send_calibrate_new_user(self, frames: list[np.ndarray]) -> CalibrationResultMsg: + self._conn.send(CalibrateNewUserCmd(frames_data=_serialize_frames(frames))) + resp = self._expect_ok("calibrate") + if not isinstance(resp, CalibrateOkResponse): + raise RuntimeError(f"Worker {self.worker_id} calibrate: unexpected {resp}") + return resp.result + + def send_remove_all_users(self) -> bool: + self._conn.send(RemoveAllUsersCmd()) + resp = self._expect_ok("remove") + if not isinstance(resp, RemoveUsersOkResponse): + raise RuntimeError(f"Worker {self.worker_id} remove: unexpected {resp}") + return resp.result + + def send_restore_gallery( + self, frames_per_user: list[list[np.ndarray]] + ) -> list[CalibrationResultMsg]: + self._conn.send( + RestoreGalleryCmd( + frames_per_user_data=[_serialize_frames(fu) for fu in frames_per_user], + ) + ) + resp = self._expect_ok("restore") + if not isinstance(resp, RestoreGalleryOkResponse): + raise RuntimeError(f"Worker {self.worker_id} restore: unexpected {resp}") + return resp.results + + def send_enable_face_id(self, enabled: bool) -> None: + self._conn.send(EnableFaceIdCmd(enabled=enabled)) + self._expect_ok("enable_face_id") + + def send_process_video( + self, + input_path: str, + output_path: str, + features: FeatureFlags, + gallery_paths: list[str], + remove_all_users: bool, + fps: float, + width: int, + height: int, + total_frames: int, + progress: Callable[..., None] | None = None, + max_fps: float | None = None, + min_fps: float | None = None, + ) -> tuple[int, list[CalibrationResultMsg]]: + """Run the full video processing loop inside the worker process. + + The worker reads the input video, runs Eve inference on each frame, + encodes the result with PyAV, and writes the output video. Only + small progress messages travel over the pipe — no frame data. + + Args: + input_path: Path to the input video file. + output_path: Path where the output video will be written. + features: Feature toggles for the Eve SDK. + gallery_paths: Media paths for Face ID gallery restore (may be empty). + remove_all_users: Whether to clear the Face ID gallery first. + fps: Output video frame rate. + width: Output video width in pixels. + height: Output video height in pixels. + total_frames: Total frame count (for progress reporting). + progress: Optional ``gr.Progress``-compatible callback. + max_fps: FPS cap when idle (``None`` = unlimited). + min_fps: FPS cap under full load (defaults to ``max_fps``). + + Returns: + Tuple of (frames_processed, gallery_results). + + Raises: + RuntimeError: If the worker reports an error. + """ + self._conn.send( + ProcessVideoCmd( + input_path=input_path, + output_path=output_path, + features=features, + gallery_paths=gallery_paths, + remove_all_users=remove_all_users, + fps=fps, + width=width, + height=height, + total_frames=total_frames, + max_fps=max_fps, + min_fps=min_fps, + ) + ) + + gallery_results: list[CalibrationResultMsg] = [] + + while True: + resp = self._recv_response() + + if isinstance(resp, GalleryRestoredResponse): + gallery_results = resp.results + + elif isinstance(resp, ProgressResponse): + if progress is not None: + progress( + (resp.current, resp.total), + desc=f"Processing frame {resp.current}/{resp.total}", + ) + + elif isinstance(resp, VideoProcessingDoneResponse): + if resp.recycle: + self.pending_recycle = True + return resp.frames_processed, gallery_results + + elif isinstance(resp, ErrorResponse): + raise RuntimeError(f"Worker {self.worker_id} process_video error: {resp.error}") + + else: + raise RuntimeError(f"Worker {self.worker_id} unexpected response: {resp}") + + def send_start_profiling(self) -> None: + self._conn.send(StartProfilingCmd()) + self._expect_ok("start_profiling") + + def send_stop_profiling(self) -> None: + self._conn.send(StopProfilingCmd()) + self._expect_ok("stop_profiling") + + def send_get_profile_stats(self) -> bytes: + """Request profiling stats from the worker. Returns marshalled pstats data.""" + self._conn.send(GetProfileStatsCmd()) + resp = self._recv_response() + if isinstance(resp, ErrorResponse): + raise RuntimeError(f"Worker {self.worker_id} get_profile_stats error: {resp.error}") + if not isinstance(resp, ProfileStatsResponse): + raise RuntimeError(f"Worker {self.worker_id} get_profile_stats: unexpected {resp}") + return resp.stats_data + + def send_get_timing_stats(self, reset: bool = True) -> dict[str, tuple[int, float]]: + """Request per-SDK-call timing stats from the worker.""" + self._conn.send(GetTimingStatsCmd(reset=reset)) + resp = self._recv_response() + if isinstance(resp, ErrorResponse): + raise RuntimeError(f"Worker {self.worker_id} get_timing_stats error: {resp.error}") + if not isinstance(resp, TimingStatsResponse): + raise RuntimeError(f"Worker {self.worker_id} get_timing_stats: unexpected {resp}") + return resp.stats + + def send_shutdown(self) -> None: + try: + self._conn.send(ShutdownCmd()) + self._conn.recv() + except (EOFError, OSError, BrokenPipeError): + pass + + def drain_heartbeats(self) -> None: + """Consume any pending heartbeat messages on the pipe.""" + while self._conn.poll(timeout=0): + try: + msg = self._conn.recv() + if isinstance(msg, HeartbeatResponse): + self.last_heartbeat = time.monotonic() + except (EOFError, OSError): + break + except Exception: + # Pipe has non-pickle data (e.g. native library output) — the + # framing is now unreliable so schedule this worker for recycling. + logger.warning( + "Worker %d: corrupt data on pipe, scheduling recycle", + self.worker_id, + ) + self.pending_recycle = True + break + + +# --------------------------------------------------------------------------- +# EveWorkerPool — manages all workers +# --------------------------------------------------------------------------- + + +@dataclass +class _PoolConfig: + max_workers: int = 8 + max_ram_gb: float = 32.0 + ram_headroom_gb: float = 2.0 + rss_safety_factor: float = 2.5 + stuck_timeout_s: float = 300.0 # 5 min for video processing + health_check_interval_s: float = 5.0 + max_jobs_per_worker: int | None = 50 + + +class EveWorkerPool: + """Pool of Eve SDK worker processes. + + Measures available RAM at startup, spawns as many workers as fit + (with safety margins), and provides acquire / release semantics. + + Args: + max_workers: Upper bound on the number of workers. + max_ram_gb: Cap on available RAM (GB) considered for worker sizing. + Useful on shared machines where reported available RAM is + much larger than what the demo should consume. + ram_headroom_gb: Free RAM (GB) to reserve for main process / OS. + rss_safety_factor: Multiplier applied to the measured init RSS to + estimate peak runtime memory per worker. + eve_bin_path: Forwarded to ``EveWrapper``. + eve_lib_path: Forwarded to ``EveWrapper``. + """ + + def __init__( + self, + max_workers: int = 8, + max_ram_gb: float = 32.0, + ram_headroom_gb: float = 2.0, + rss_safety_factor: float = 2.5, + eve_bin_path: str = "", + eve_lib_path: str = "", + max_jobs_per_worker: int | None = 50, + ): + self._cfg = _PoolConfig( + max_workers=max_workers, + max_ram_gb=max_ram_gb, + ram_headroom_gb=ram_headroom_gb, + rss_safety_factor=rss_safety_factor, + max_jobs_per_worker=max_jobs_per_worker, + ) + self._eve_bin_path = eve_bin_path + self._eve_lib_path = eve_lib_path + self._lock = threading.Condition(threading.Lock()) + self._workers: list[EveWorker] = [] + self._shutting_down = False + self._next_id = 0 + self._waiting_count = 0 + self.session_count = 0 + # Shared load signal readable by worker processes (0.0–1.0). + # lock=False is safe: single writer (main process), readers get + # a slightly stale value at worst. + self._shared_load: mp.Value = _mp_ctx.Value("d", 0.0, lock=False) + + # Spawn workers based on RAM capacity + count = self._compute_worker_count() + self._spawn_workers_parallel(count) + + logger.info(f"EveWorkerPool started with {len(self._workers)} workers") + + # Start health-check daemon + self._health_thread = threading.Thread(target=self._health_monitor, daemon=True) + self._health_thread.start() + + # Hourly memory report + from memory_monitor import start_memory_reporter + + self._mem_thread = start_memory_reporter( + get_workers=lambda: list(self._workers), + lock=self._lock, + shutdown_flag=lambda: self._shutting_down, + max_ram_gb=self._cfg.max_ram_gb, + ) + + atexit.register(self.shutdown_all) + + # -- public API -------------------------------------------------------- + + @property + def worker_count(self) -> int: + """Number of live workers (for setting ``concurrency_limit``).""" + with self._lock: + return sum(1 for w in self._workers if w.status != "dead") + + @property + def idle_count(self) -> int: + """Number of idle workers.""" + with self._lock: + return sum(1 for w in self._workers if w.status == "idle") + + @property + def waiting_count(self) -> int: + """Number of callers blocked in ``acquire()`` waiting for a worker.""" + with self._lock: + return self._waiting_count + + def try_acquire(self, session_hash: str) -> EveWorker | None: + """Non-blocking acquire — returns an idle worker or ``None``. + + Unlike :meth:`acquire`, this never blocks or increments the + waiting count. Used by :class:`LiveStreamManager` so that + WebRTC frame handlers can return immediately when no worker + is available. + """ + with self._lock: + return self._try_acquire(session_hash) + + def _try_acquire(self, session_hash: str) -> EveWorker | None: + """Try to grab an idle worker (must hold ``self._lock``). + + Returns the worker if successful, ``None`` otherwise. + """ + # Prefer session-affiliated idle worker (Face ID gallery affinity) + for w in self._workers: + if w.status == "idle" and not w.pending_recycle and w.session_hash == session_hash: + w.status = "busy" + w.busy_since = time.monotonic() + w.drain_heartbeats() + self._update_shared_load() + return w + # Any idle worker + for w in self._workers: + if w.status == "idle" and not w.pending_recycle: + w.status = "busy" + w.session_hash = session_hash + w.busy_since = time.monotonic() + w.drain_heartbeats() + self._update_shared_load() + return w + return None + + def acquire( + self, + session_hash: str, + timeout: float = 300.0, + progress: Callable[..., None] | None = None, + eta_fn: Callable[[int], float | None] | None = None, + ) -> EveWorker: + """Reserve a worker for *session_hash*, blocking until one is free. + + Prefers a worker already affiliated with the session (Face ID + gallery affinity). Falls back to any idle worker. Blocks up to + *timeout* seconds if all workers are busy. + + Args: + session_hash: Gradio session hash for worker affinity. + timeout: Max seconds to wait for a free worker. + progress: Optional ``gr.Progress``-compatible callback invoked + while waiting so the UI can show queue position. + eta_fn: Optional callable accepting a 1-based queue position + and returning estimated seconds until a worker frees up. + The return value is shown in the progress description. + + Raises: + RuntimeError: If no worker becomes available within *timeout*. + """ + deadline = time.monotonic() + timeout + + # Fast path — try without incrementing waiting count + with self._lock: + worker = self._try_acquire(session_hash) + if worker is not None: + return worker + self._waiting_count += 1 + + # Slow path — block until a worker is released + try: + while True: + # Send progress update OUTSIDE the lock to avoid blocking release() + if progress is not None: + with self._lock: + pos = self._waiting_count + eta = eta_fn(pos) if eta_fn is not None else None + if eta is not None: + eta_int = int(eta) + eta_mins = max(0.5, round(eta / 30) * 0.5) + progress( + (0, eta_int), + desc=f"⏳ In queue ~{eta_mins:g} minutes", + ) + else: + progress((0, 1), desc=f"⏳ In queue") + + with self._lock: + worker = self._try_acquire(session_hash) + if worker is not None: + return worker + + remaining = deadline - time.monotonic() + if remaining <= 0: + raise RuntimeError( + "No idle Eve workers available — timed out after " + f"{timeout:.0f}s. Try again shortly." + ) + self._lock.wait(timeout=min(remaining, 1.0)) + finally: + with self._lock: + self._waiting_count -= 1 + + def release(self, worker: EveWorker) -> None: + """Return a worker to the idle pool and wake any waiting acquirers.""" + with self._lock: + worker.status = "idle" + worker.busy_since = None + worker.is_live_stream = False + self._update_shared_load() + self._lock.notify_all() + + def get_live_workers(self) -> list[EveWorker]: + """Return a snapshot of non-dead workers (thread-safe).""" + with self._lock: + return [w for w in self._workers if w.status != "dead"] + + def shutdown_all(self) -> None: + """Gracefully shut down all workers.""" + self._shutting_down = True + with self._lock: + for w in self._workers: + if w.status != "dead": + try: + w.send_shutdown() + except Exception: + pass + w.process.join(timeout=5) + if w.process.is_alive(): + w.process.kill() + w.process.join(timeout=2) + w.status = "dead" + + def _update_shared_load(self) -> None: + """Refresh the shared load signal (must hold ``self._lock``).""" + total = sum(1 for w in self._workers if w.status != "dead") + if total <= 1: + self._shared_load.value = 0.0 + else: + idle = sum(1 for w in self._workers if w.status == "idle") + self._shared_load.value = 1.0 - idle / total + + # -- internals --------------------------------------------------------- + + def _compute_worker_count(self) -> int: + """Determine how many workers fit in available RAM.""" + try: + import psutil + except ImportError: + logger.warning("psutil not installed — defaulting to 1 worker") + return 1 + + raw_available_mb = psutil.virtual_memory().available / (1024 * 1024) + cap_mb = self._cfg.max_ram_gb * 1024 + available_mb = min(raw_available_mb, cap_mb) + headroom_mb = self._cfg.ram_headroom_gb * 1024 + + # Spawn a probe worker to measure init RSS + + if DO_PROBE_WORKER: + probe = self._spawn_worker(probe=True) + try: + import psutil as _ps + + probe_rss_mb = _ps.Process(probe.process.pid).memory_info().rss / (1024 * 1024) + except Exception: + logger.warning("Could not measure worker RSS — defaulting to 1 worker") + return 1 + else: + # Estimated 500MB RSS for an idle worker without running inference (based on observed values) + # It's around 200MB idle, and there's the video being loaded in memory, depends on the size of the video. + probe_rss_mb = 250 + + estimated_peak_mb = probe_rss_mb * self._cfg.rss_safety_factor + usable_mb = available_mb - headroom_mb + # +1 because the probe worker already consumed memory + max_by_ram = max(1, int(usable_mb / estimated_peak_mb) + 1) + if DO_PROBE_WORKER: + actual = min(self._cfg.max_workers - 1, max_by_ram) + else: + actual = min(self._cfg.max_workers, max_by_ram) + + logger.info( + f"RAM estimation: init_rss={probe_rss_mb:.0f}MB, " + f"estimated_peak={estimated_peak_mb:.0f}MB, " + f"raw_available={raw_available_mb:.0f}MB, " + f"available={available_mb:.0f}MB (cap={cap_mb:.0f}MB), " + f"headroom={headroom_mb:.0f}MB, " + f"max_by_ram={max_by_ram}, max_workers={self._cfg.max_workers}, " + f"actual={actual}" + ) + return actual + + def _launch_worker(self) -> tuple[int, mp.Process, Connection]: + """Start a worker process without waiting for readiness. + + Returns: + Tuple of (worker_id, process, parent_conn). + """ + wid = self._next_id + self._next_id += 1 + + parent_conn, child_conn = _mp_ctx.Pipe() + proc = _mp_ctx.Process( + target=_eve_worker_main, + args=( + child_conn, + self._eve_bin_path, + self._eve_lib_path, + wid, + self._cfg.max_jobs_per_worker, + self._shared_load, + ), + daemon=True, + ) + proc.start() + return wid, proc, parent_conn + + def _wait_for_ready(self, wid: int, proc: mp.Process, parent_conn: Connection) -> EveWorker: + """Block until a launched worker sends its "ready" message. + + Raises: + RuntimeError: If the worker doesn't become ready within 120 s. + """ + if not parent_conn.poll(timeout=120): + proc.kill() + proc.join(timeout=5) + raise RuntimeError(f"Worker {wid} did not start within 120 s") + + msg: WorkerResponse = parent_conn.recv() + if not isinstance(msg, ReadyResponse): + proc.kill() + proc.join(timeout=5) + error = msg.error if isinstance(msg, ErrorResponse) else str(msg) + raise RuntimeError(f"Worker {wid} failed to initialise: {error}") + + worker = EveWorker(proc, parent_conn, wid) + with self._lock: + self._workers.append(worker) + self._lock.notify_all() + logger.info(f"Worker {wid} (pid={proc.pid}) ready") + return worker + + def _spawn_workers_parallel(self, count: int) -> None: + """Launch *count* workers in parallel and wait for all to be ready.""" + pending = [self._launch_worker() for _ in range(count)] + for wid, proc, conn in pending: + try: + self._wait_for_ready(wid, proc, conn) + except RuntimeError: + logger.error(f"Worker {wid} failed to start — skipping") + + def _spawn_worker(self, probe: bool = False) -> EveWorker: + """Spawn a single worker and wait for it to become ready. + + Used for the RAM probe and for respawning crashed workers. + """ + wid, proc, conn = self._launch_worker() + return self._wait_for_ready(wid, proc, conn) + + def _respawn_worker(self, dead_worker: EveWorker) -> None: + """Replace a dead worker with a fresh one.""" + if self._shutting_down: + return + logger.warning( + f"Respawning worker {dead_worker.worker_id} " f"(was pid={dead_worker.process.pid})" + ) + dead_worker.process.join(timeout=0) + with self._lock: + self._workers.remove(dead_worker) + try: + self._spawn_worker() + except RuntimeError as exc: + logger.error(f"Failed to respawn worker: {exc}") + + def _health_monitor(self) -> None: + """Background daemon that detects dead / stuck workers.""" + while not self._shutting_down: + time.sleep(self._cfg.health_check_interval_s) + if self._shutting_down: + break + + with self._lock: + workers_snapshot = list(self._workers) + + for w in workers_snapshot: + if w.status == "dead": + continue + + # Detect crashed / zombie processes + if not w.process.is_alive(): + if w.pending_recycle: + logger.info(f"Worker {w.worker_id} (pid={w.process.pid}) recycled cleanly") + else: + logger.error( + f"Worker {w.worker_id} (pid={w.process.pid}) died unexpectedly" + ) + w.status = "dead" + self._respawn_worker(w) + continue + + # Detect stuck workers (skip live streams — they're long by design) + if w.status == "busy" and not w.is_live_stream and w.busy_since is not None: + elapsed = time.monotonic() - w.busy_since + if elapsed > self._cfg.stuck_timeout_s * 2: + logger.error(f"Worker {w.worker_id} stuck for {elapsed:.0f}s — killing") + w.process.kill() + w.process.join(timeout=5) + w.status = "dead" + self._respawn_worker(w) + elif elapsed > self._cfg.stuck_timeout_s: + logger.warning( + f"Worker {w.worker_id} busy for {elapsed:.0f}s " + f"(threshold={self._cfg.stuck_timeout_s}s)" + ) diff --git a/shared/eve_wrapper.py b/shared/eve_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..2e56f3efc652e0bc55ef520bbd1fb43bfa64c2e1 --- /dev/null +++ b/shared/eve_wrapper.py @@ -0,0 +1,588 @@ +import ctypes +import glob +import os +import platform +import sys +import time +from dataclasses import dataclass + +import cv2 +import numpy as np +from eve_python import eve_sdk as sdk +from eve_python.structs.CFaceIdStructs import ( + EveFaceIdCommand, + EveFaceIdIdentificationStatus, +) +from log_utils import setup_logger + +logger = setup_logger("EveWrapper") + +_is_windows = platform.system() == "Windows" + +DO_FAKE_MIRROR = True + +# Gate timing instrumentation behind the profiler env var +_TIMING_ENABLED = os.environ.get("ENABLE_PROFILER", "").strip() not in ("", "0", "false") + + +@dataclass +class CalibrationResult: + """Result of a face ID calibration attempt.""" + + success: bool + user_id: int + message: str + + +class EveWrapper: + def __init__(self, eve_bin_path="", eve_lib_path=""): + self._ensure_config_dir() + eve_bin_path, eve_lib_path = self._resolve_eve_paths(eve_bin_path, eve_lib_path) + + self._mirror = False + self._inference_frame_count = 0 + # Per-call timing: {name: [call_count, total_seconds]} + self._timings: dict[str, list[float]] = {} + self._timing_enabled = _TIMING_ENABLED + + image_provider = sdk.structs.EveImageProvider.EVE_CLIENT_PROVIDED + self.eve_sdk = self._load_and_create_eve(eve_bin_path, eve_lib_path, image_provider) + + if image_provider == sdk.structs.EveImageProvider.EVE_CAMERA: + self._set_camera() + + image_request = sdk.structs.EveImageFormatRequest( + location=sdk.structs.EveImageLocation.EVE_CPU, + format=sdk.structs.EveVideoFormat.EVE_BGRA, + ) + self.eve_sdk.EveConfigureProcessedImage(image_request) + + err = self.eve_sdk.StartEveWithParameters( + sdk.structs.EveProcessingParameters(type=sdk.structs.EveProcessingPipelineType.EVE_HMI) + ) + if err != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"StartEveWithParameters error code: {err}") + sys.exit(err) + + os.chdir(self._backup_cwd) + logger.info("EVE initialized") + + def _record_timing(self, name: str, elapsed: float) -> None: + entry = self._timings.get(name) + if entry is None: + self._timings[name] = [1, elapsed] + else: + entry[0] += 1 + entry[1] += elapsed + + def get_timing_stats(self, reset: bool = False) -> dict[str, tuple[int, float]]: + """Return accumulated per-call timings as {name: (count, total_seconds)}. + + Args: + reset: If True, clear the accumulators after reading. + """ + result = {k: (int(v[0]), v[1]) for k, v in self._timings.items()} + if reset: + self._timings.clear() + return result + + def inference(self, image: np.ndarray) -> np.ndarray: + t = self._timing_enabled + + if self._mirror and DO_FAKE_MIRROR: + if t: + _t0 = time.perf_counter() + image = cv2.flip(image, 1) + if t: + self._record_timing("flip", time.perf_counter() - _t0) + + if t: + _t0 = time.perf_counter() + if not self._send_frame(image): + return image + if t: + self._record_timing("EveSendImageForProcessing", time.perf_counter() - _t0) + + if t: + _t0 = time.perf_counter() + processed_image = self.eve_sdk.EveGetProcessedImage() + if t: + self._record_timing("EveGetProcessedImage", time.perf_counter() - _t0) + if processed_image.error != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"EveGetProcessedImage() error code: {processed_image.error}") + sys.exit(processed_image.error) + + if t: + _t0 = time.perf_counter() + img = np.ctypeslib.as_array( + processed_image.data, + shape=(processed_image.height, processed_image.width, processed_image.channels), + ).copy() + del processed_image + if t: + self._record_timing("as_array+copy", time.perf_counter() - _t0) + + if img.shape[2] == 2: + if t: + _t0 = time.perf_counter() + img = cv2.cvtColor(img, cv2.COLOR_YUV2BGR_YUYV) + if t: + self._record_timing("cvtColor", time.perf_counter() - _t0) + elif img.shape[2] == 4: + if t: + _t0 = time.perf_counter() + img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR) + if t: + self._record_timing("cvtColor", time.perf_counter() - _t0) + + # SDK requires all outputs to be consumed before the next frame + if t: + _t0 = time.perf_counter() + person_detection_data = self.eve_sdk.EveGetPersonDetectionData() + if t: + self._record_timing("EveGetPersonDetectionData", time.perf_counter() - _t0) + if person_detection_data.error != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"EveGetPersonDetectionData() error code: {person_detection_data.error}") + sys.exit(person_detection_data.error) + del person_detection_data + + if t: + _t0 = time.perf_counter() + all_faces = self.eve_sdk.EveGetAllFaceData() + if t: + self._record_timing("EveGetAllFaceData", time.perf_counter() - _t0) + if all_faces.errorCode == sdk.structs.EveError.EVE_ERROR_NO_ERROR: + face_count = all_faces.faceData.contents.detectedFacesCount + if face_count > 0: + self._inference_frame_count += 1 + # Log face ID data periodically to avoid flooding + if self._inference_frame_count % self._INFERENCE_LOG_INTERVAL == 1: + self._log_all_faces("inference", self._inference_frame_count, all_faces) + del all_faces + + if img.shape[2] in (1, 3): + return img + + print( + f"WRONG FORMAT: {img.shape} " + "(Consider converting it above here, or making EveImageFormatRequest work)" + ) + return None + + def shutdown(self) -> None: + """Cleanly shut down the Eve SDK instance.""" + try: + self.eve_sdk.ShutdownEve() + logger.info("EVE shut down") + except Exception as exc: + logger.warning(f"EVE shutdown error: {exc}") + + def enable_mirror(self, enabled: bool = True) -> None: + """Enable or disable image mirroring.""" + if DO_FAKE_MIRROR: + self._mirror = enabled + return + options = sdk.structs.EveImageManipulationOptions() + options.settings.mirrorImage = 1 if enabled else 0 + result = self.eve_sdk.EveConfigureImageManipulation(options) + if result.errorCode != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"EveConfigureImageManipulation() error code: {result.errorCode}") + sys.exit(result.errorCode) + + def enable_object_detection(self, model_name: str | None = "GMOD-80"): + """Enable EVE's object detection (MOD) feature. + + Pass ``None`` / ``""`` / ``False`` to disable. Any truthy ``model_name`` + enables MOD; the name is recorded for future model-switching support + but is **not** acted on yet — the EVE SDK currently ships a single + hardcoded MOD model (GMOD-80, see ``EveEthosNpu/Models.h``). + + TODO: When the EVE C SDK gains a ``EveLoadObjectDetectionModel(path)`` + (or similar) entry point, hand-add the binding in + ``eve_python/eve_sdk.py`` and call it here before + ``EveConfigureObjectDetection`` to actually switch models. + """ + enabled = bool(model_name) + if enabled and model_name not in (None, "GMOD-80"): + logger.info( + f"enable_object_detection: requested {model_name!r} but SDK only " + f"ships GMOD-80; falling back to GMOD-80 weights." + ) + result = self.eve_sdk.EveConfigureObjectDetection( + sdk.structs.EveObjectDetectionOptions( + enabled=( + sdk.structs.EveOptionEnabled.EVE_OPTION_ENABLED + if enabled + else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED + ) + ) + ) + if result.error != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"eveConfigureObjectDetection() error code: {result.error}") + sys.exit(result.error) + + def enable_face_and_person_detection( + self, faceEnabled: bool = True, personEnabled: bool = True + ): + mode = sdk.structs.EveFaceTrackerMinimumMode.EVE_FACETRACKER_MINIMUM_MODE_OFF + + params = sdk.structs.EveFaceTrackerOptions( + faceTrackerMode=mode, + enable3DFaceTracking=( + sdk.structs.EveOptionEnabled.EVE_OPTION_ENABLED + if faceEnabled + else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED + ), + fitSecondaryUsers=( + sdk.structs.EveOptionEnabled.EVE_OPTION_ENABLED + if faceEnabled + else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED + ), + enablePersonDetection=( + sdk.structs.EveOptionEnabled.EVE_OPTION_ENABLED + if personEnabled + else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED + ), + enableEyeLandmarks=sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED, + ) + result = self.eve_sdk.EveConfigureFaceTracker(params) + if result.error != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"EveConfigureFaceTracker() error code: {result.error}") + sys.exit(result.error) + + def enable_face_id(self, enabled: bool = True, threshold: float = 0.7) -> None: + eve_enabled = ( + sdk.structs.EveOptionEnabled.EVE_OPTION_ENABLED + if enabled + else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED + ) + options = sdk.structs.EveFaceIdOptions(enabled=eve_enabled, threshold=threshold) + if enabled: + options.calibrationPoses = ( + sdk.structs.EveFaceIdCalibrationPoseMode.EVE_FACEID_CALIBRATION_FRONTAL_ONLY + ) + result = self.eve_sdk.EveConfigureFaceId(options) + if result.error != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"EveConfigureFaceId() error code: {result.error}") + sys.exit(result.error) + + def enable_hand_gesture(self, enabled=True, redetection_delay_ms=0): + eve_enabled = ( + sdk.structs.EveOptionEnabled.EVE_OPTION_ENABLED + if enabled + else sdk.structs.EveOptionEnabled.EVE_OPTION_DISABLED + ) + result = self.eve_sdk.EveConfigureHandGesture( + sdk.structs.EveHandGestureOptions( + enabled=eve_enabled, redetectionDelay=redetection_delay_ms + ) + ) + if result.errorCode != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"EveConfigureHandGesture() error code: {result.errorCode}") + sys.exit(result.errorCode) + + def _send_face_id_command( + self, command: EveFaceIdCommand + ) -> sdk.structs.EveFaceIdCommandStruct: + cmd = sdk.structs.EveFaceIdCommandStruct(command=command) + return self.eve_sdk.EveSendFaceIdCommand(cmd) + + def _log_all_faces(self, context: str, frame_idx: int, all_faces) -> None: + """Log face ID details for every detected face in one frame.""" + face_count = all_faces.faceData.contents.detectedFacesCount + if face_count == 0: + logger.debug(f"[{context}] frame {frame_idx}: no faces detected") + return + for j in range(face_count): + f = all_faces.faceData.contents.faces[j] + fid = f.faceId.faceIdentity + logger.debug( + f"[{context}] frame {frame_idx}: face {j}: " + f"id={fid.id}, confidence={fid.confidence:.4f}, " + f"similarity={fid.similarity:.4f}, " + f"identificationStatus={f.faceId.identificationStatus}, " + f"calibrationStatus={f.faceId.calibrationStatus}" + ) + + def _send_frame(self, frame: np.ndarray) -> None: + """Send a single frame through the SDK pipeline (send + consume output).""" + eve_image = self._create_eve_input_image(frame, frame.shape[1], frame.shape[0], "BGR") + send_err = self.eve_sdk.EveSendImageForProcessing(eve_image) + if send_err != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.warning(f"EveSendImageForProcessing failed: {send_err}") + return False + return True + + _FLUSH_FRAME_COUNT = 6 + _CALIBRATION_MAX_ATTEMPTS = 5 + _INFERENCE_LOG_INTERVAL = 30 # log face ID data every N frames during inference + + def calibrate_new_user(self, frames: list[np.ndarray]) -> CalibrationResult: + """Register a new face by sending frames through the SDK calibration pipeline. + + Retries up to ``_CALIBRATION_MAX_ATTEMPTS`` times because the SDK can + intermittently reject borderline-frontal faces. + + Args: + frames: BGR images to use for calibration. Each frame is sent exactly once + per attempt; returns at the first successful attempt. + + Returns: + CalibrationResult with success flag, SDK-assigned user ID, and message. + """ + last_result: CalibrationResult | None = None + for attempt in range(self._CALIBRATION_MAX_ATTEMPTS): + last_result = self._calibrate_new_user_once(frames) + if last_result.success: + return last_result + logger.info( + "calibrate_new_user: attempt %d/%d failed: %s", + attempt + 1, + self._CALIBRATION_MAX_ATTEMPTS, + last_result.message, + ) + return last_result # type: ignore[return-value] + + def _calibrate_new_user_once(self, frames: list[np.ndarray]) -> CalibrationResult: + """Single calibration attempt — flush, send ADD_NEW_USER + frames, check result.""" + self.enable_face_and_person_detection(faceEnabled=True) + self.enable_face_id(enabled=True) + + h, w = frames[0].shape[:2] + blank = np.zeros((h, w, 3), dtype=np.uint8) + for _ in range(self._FLUSH_FRAME_COUNT): + self._send_frame(blank) + + success_frame = -1 + for i, frame in enumerate(frames): + result = self._send_face_id_command(EveFaceIdCommand.EVE_FACE_ID_COMMAND_ADD_NEW_USER) + if result.errorCode != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + return CalibrationResult( + False, 0, f"ADD_NEW_USER command failed: {result.errorCode}" + ) + + self._send_frame(frame) + + all_faces = self.eve_sdk.EveGetAllFaceData() + if all_faces.errorCode != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + return CalibrationResult( + False, 0, f"EveGetAllFaceData failed: {all_faces.errorCode}" + ) + + face_count = all_faces.faceData.contents.detectedFacesCount + self._log_all_faces("calibrate frame", i, all_faces) + + matched_face = None + for fi in range(face_count): + face = all_faces.faceData.contents.faces[fi] + if ( + face.faceId.faceIdentity.id >= 0 + and face.faceId.identificationStatus + == EveFaceIdIdentificationStatus.EVE_FACE_ID_SUCCESS + ): + matched_face = face + break + if matched_face is not None: + success_frame = i + break + + if success_frame > -1: + user_id = matched_face.faceId.faceIdentity.id + confidence = matched_face.faceId.faceIdentity.confidence + similarity = matched_face.faceId.faceIdentity.similarity + + logger.debug( + f"calibrate_new_user: SUCCESS on frame {success_frame}, " + f"id={user_id}, confidence={confidence:.4f}, similarity={similarity:.4f}" + ) + if len(frames) == 1: + return CalibrationResult(True, user_id, f"Registered as user {user_id}") + else: + return CalibrationResult( + True, + user_id, + f"Calibration succeeded on frame {success_frame}. " + f"Registered as user {user_id}.", + ) + else: + return CalibrationResult( + False, + 0, + "Calibration did not succeed. " + "Ensure a clear, frontal face is visible in the upload.", + ) + + def remove_all_users(self) -> bool: + """Remove all users from the SDK face ID gallery. + + Returns: + True if removal succeeded. + """ + result = self._send_face_id_command(EveFaceIdCommand.EVE_FACE_ID_COMMAND_REMOVE_ALL_USERS) + if result.errorCode != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.warning(f"REMOVE_ALL_USERS command error: {result.errorCode}") + return False + return True + + def restore_gallery(self, frames_per_user: list[list[np.ndarray]]) -> list[CalibrationResult]: + """Wipe the SDK gallery and re-register users from stored frames. + + Used before video processing to sync the SDK gallery with a session's + registered users. Sends a frame after removing to flush the command + through the SDK pipeline before re-registering. + + Args: + frames_per_user: List of frame lists, one per user to re-register. + + Returns: + List of CalibrationResult, one per user. + """ + if not frames_per_user: + return [] + + self.enable_face_and_person_detection(faceEnabled=True) + self.enable_face_id(enabled=True) + + self.remove_all_users() + # SDK commands are async — flush the remove by sending a frame + self._send_frame(frames_per_user[0][0]) + + results = [] + for idx, frames in enumerate(frames_per_user): + r = self.calibrate_new_user(frames) + logger.info( + f"restore_gallery: user {idx}: success={r.success}, " + f"sdk_id={r.user_id}, message='{r.message}'" + ) + results.append(r) + return results + + @staticmethod + def _ensure_config_dir(): + from pathlib import Path + + try: + new_dir_path = Path.home() / ".config" + new_dir_path.mkdir(exist_ok=True) + except Exception as e: + print(f"Error creating directory: {e}") + + @staticmethod + def _resolve_eve_paths(eve_bin_path="", eve_lib_path=""): + if eve_bin_path and eve_lib_path: + return eve_bin_path, eve_lib_path + + if _is_windows: + eve_bin_path = r"C:\TLT_SRC_DIR\EdgeVisionEngine\x64\Release\\" + eve_lib_path = eve_bin_path + else: + eve_dir_default_paths = glob.glob("/opt/EVE-*-Source", recursive=False) + eve_bin_path = eve_bin_path or ( + os.path.join(eve_dir_default_paths[0], "bin") if eve_dir_default_paths else "" + ) + eve_lib_path = eve_lib_path or ( + os.path.join(eve_dir_default_paths[0], "lib") if eve_dir_default_paths else "" + ) + return eve_bin_path, eve_lib_path + + def _load_and_create_eve(self, eve_bin_path, eve_lib_path, image_provider): + from pathlib import Path + + self._backup_cwd = os.getcwd() + os.chdir(eve_bin_path) + + if _is_windows: + eve_sdk_path = os.path.join(eve_bin_path, "EveSDK.dll") + root = Path(os.path.abspath(__file__)).parent + if not os.path.isfile(eve_sdk_path): + eve_sdk_path = os.path.join(root.parent.parent, eve_bin_path, "EveSDK.dll") + else: + eve_sdk_path = os.path.join(eve_bin_path, "libEveSDK.so") + if not os.path.isfile(eve_sdk_path): + eve_sdk_path = os.path.join(eve_lib_path, "libEveSDK.so") + + eve_sdk_instance = sdk.EveSDK(eve_sdk_path) + + ByteArray512 = ctypes.c_byte * 512 + encoded = os.path.dirname(eve_bin_path + os.sep).encode("utf-8") # EVE needs os.sep + pathOverride = ByteArray512(*encoded, *([0] * (512 - len(encoded)))) # zero-pad to 512 + + startup_options = sdk.structs.EveStartupParameters( + pathOverride=pathOverride, + gpuPreference=sdk.structs.EveGpuPreference.EVE_NO_GPU, + imageProvider=image_provider, + startupType=sdk.structs.EveStartupType.EVE_SYNC, + ) + err = eve_sdk_instance.CreateEve(startup_options) + if err != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + logger.info(f"CreateEve error code: {err}") + sys.exit(err) + + return eve_sdk_instance + + def _set_camera(self): + i = 0 + self._metaDataFpgaCameraId = -1 + self._fpgaCameraId = -1 + while True: + cameraInfo = self.eve_sdk.EveGetCamera(i) + if ( + cameraInfo.error == sdk.structs.EveError.EVE_INVALID_CAMERA_ID + or cameraInfo.error == sdk.structs.EveError.EVE_NO_MORE_DATA + ): + break + + pid = ctypes.cast(cameraInfo.data.pid, ctypes.c_char_p).value + vid = ctypes.cast(cameraInfo.data.vid, ctypes.c_char_p).value + if cameraInfo.data.isFpgaCamera == 1: + if self._metaDataFpgaCameraId == -1 and vid == b"META" and pid == b"DATA": + self._metaDataFpgaCameraId = i + elif self._fpgaCameraId == -1: + self._fpgaCameraId = i + print(i, self._fpgaCameraId, self._metaDataFpgaCameraId, cameraInfo.error, pid, vid) + if self._fpgaCameraId >= 0 and self._metaDataFpgaCameraId >= 0: + break + i += 1 + + if self._fpgaCameraId == -1 and self._metaDataFpgaCameraId == -1: + raise RuntimeError("No FPGA camera found") + print( + f" \n\t\t *** FPGA camera found: {self._fpgaCameraId}, metadata {self._metaDataFpgaCameraId}\n" + ) + + useMetadataCamera = False + if useMetadataCamera: + self._usedCameraId = self._metaDataFpgaCameraId + else: + self._usedCameraId = self._fpgaCameraId + + cameraFormat = sdk.structs.CCameraFormat() + cameraFormat.resolution.width = 640 + cameraFormat.resolution.height = 480 + cameraFormat.compareResolution = sdk.structs.EveCompare.EVE_AT_MOST + cameraFormat.compareFps = sdk.structs.EveCompare.EVE_AT_LEAST + formats = self.eve_sdk.EveGetFormats(self._usedCameraId, cameraFormat) + f = formats.formats[0] + + print( + f"camera selected: ID#{self._usedCameraId}: {f.resolution.width}x{f.resolution.height}, " + f"Format: {f.format} @ {f.fps}FPS" + ) + + errorCode = self.eve_sdk.EveSetCamera(self._usedCameraId, f) + if errorCode != sdk.structs.EveError.EVE_ERROR_NO_ERROR: + raise RuntimeError(f"Could't set camera {errorCode}") + + @staticmethod + def _create_eve_input_image(image_bin, width, height, encoding): + image = sdk.structs.EveInputImage() + image.data = image_bin.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) + image.width = width + image.height = height + if encoding == "YUY2": + image.encoding = sdk.structs.EveVideoFormat.EVE_YUY2 + elif encoding == "NV12": + image.encoding = sdk.structs.EveVideoFormat.EVE_NV12 + elif encoding == "BGR": + image.encoding = sdk.structs.EveVideoFormat.EVE_BGR + return image diff --git a/shared/face_id_tab.py b/shared/face_id_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..65a056bd4b0408c04d1c651ef5181efc1c70708d --- /dev/null +++ b/shared/face_id_tab.py @@ -0,0 +1,963 @@ +"""Face ID Registration Gradio tab for Eve SDK applications. + +Provides a self-contained Gradio tab for registering and unregistering faces +via the Eve SDK. Registration media is stored in per-session tmp directories +(``tmp/