From a4c9afed5ee44a093911a298197481c9f002f29c Mon Sep 17 00:00:00 2001 From: "Lacewell, Chaunte W" Date: Tue, 17 Mar 2026 22:14:29 -0700 Subject: [PATCH 01/20] Add Fastapi for processing Signed-off-by: Lacewell, Chaunte W --- deployment/docker-swarm/build.sh | 11 +- deployment/docker-swarm/udf.m4 | 8 +- deployment/docker-swarm/video.m4 | 79 +- fastapi/CMakeLists.txt | 2 + fastapi/Dockerfile | 71 ++ fastapi/build.sh | 11 + fastapi/include/utils.py | 1554 +++++++++++++++++++++++ fastapi/main.py | 1132 +++++++++++++++++ fastapi/nginx.conf | 36 + fastapi/requirements.GPU.txt | 6 + fastapi/requirements.txt | 7 + fastapi/shell.sh | 6 + frontend/Dockerfile | 11 +- frontend/requirements.txt | 24 +- video/Dockerfile | 62 +- video/Dockerfile.base | 34 + video/manage.sh | 3 +- video/nginx.conf | 36 +- video/process_stream.py | 988 -------------- video/requirements.CPU.txt | 6 +- video/requirements.txt | 67 +- video/resources/models/download_yolo.py | 70 +- video/segment.py | 9 +- video/thumbnail.py | 9 +- video/watch_and_send2vdms.py | 149 ++- 25 files changed, 3255 insertions(+), 1136 deletions(-) create mode 100644 fastapi/CMakeLists.txt create mode 100644 fastapi/Dockerfile create mode 100755 fastapi/build.sh create mode 100644 fastapi/include/utils.py create mode 100644 fastapi/main.py create mode 100644 fastapi/nginx.conf create mode 100644 fastapi/requirements.GPU.txt create mode 100644 fastapi/requirements.txt create mode 100755 fastapi/shell.sh create mode 100644 video/Dockerfile.base delete mode 100644 video/process_stream.py diff --git a/deployment/docker-swarm/build.sh b/deployment/docker-swarm/build.sh index ef6a80a..19abc0b 100755 --- a/deployment/docker-swarm/build.sh +++ b/deployment/docker-swarm/build.sh @@ -16,11 +16,20 @@ RESIZE_FLAG="${12}" MODEL_NAME="${13}" CUSTOM_MODEL_FLAG="${14}" OMIT_DETECTIONS_FLAG="${15}" +HOSTIP=$(ip route get 8.8.8.8 | awk '/ src /{split(substr($0,index($0," src ")),f);print f[2];exit}') echo "Generating templates with PLATFORM=${PLATFORM},NCURATIONS=${NCURATIONS},NSTREAMS=${NSTREAMS},INGESTION=${INGESTION},DEVICE=${DEVICE},IN_SOURCE=${IN_SOURCE},NCPU=${NCPU},HOSTIP=${HOSTIP},DEBUG=${DEBUG},DOCKER_TAR=${DOCKER_TAR},DOCKER_TAR_DIR=${DOCKER_TAR_DIR},RESIZE_FLAG=${RESIZE_FLAG},OMIT_DETECTIONS_FLAG=${OMIT_DETECTIONS_FLAG}" +BDIR=$(dirname $(dirname $DIR)) +# echo "docker build --build-arg no_proxy --network host --file=${BDIR}/video/Dockerfile.base -t lcc_base_video_image:latest ${DIR}/video $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') --build-arg DEVICE=${DEVICE}" +# if video or fastapi in DIR; then +# if [[ "$DIR" == *video* || "$DIR" == *fastapi* ]]; then +# echo "docker build --network host --file=${DIR}/../video/Dockerfile.base $@ -t lcc_base_video_image:latest ${DIR}/../video $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') --build-arg DEVICE=${DEVICE}" +docker build --build-arg DEVICE=${DEVICE} --network host --file="${BDIR}/video/Dockerfile.base" -t "lcc_base_video_image:latest" "${BDIR}/video" $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') +# fi + if test -f "${DIR}/docker-compose.yml.m4"; then echo "Generating docker-compose.yml" - m4 -D${DEVICE} -Din_${IN_SOURCE} -DREGISTRY_PREFIX=$REGISTRY -DINGESTION="$INGESTION" -DDEVICE="$DEVICE" -DDEBUG="$DEBUG" -DNCURATIONS="${NCURATIONS}" -DNSTREAMS="${NSTREAMS}" -DIN_SOURCE="${IN_SOURCE}" -DNCPU="${NCPU}" -DRESIZE_FLAG="${RESIZE_FLAG}" -DMODEL_NAME="${MODEL_NAME}" -DCUSTOM_MODEL_FLAG="${CUSTOM_MODEL_FLAG}" -DOMIT_DETECTIONS_FLAG="${OMIT_DETECTIONS_FLAG}" -I "${DIR}" "${DIR}/docker-compose.yml.m4" > "${DIR}/docker-compose.yml" + m4 -D${DEVICE} -Din_${IN_SOURCE} -DREGISTRY_PREFIX=$REGISTRY -DINGESTION="$INGESTION" -DDEVICE="$DEVICE" -DDEBUG="$DEBUG" -DNCURATIONS="${NCURATIONS}" -DHOSTIP="${HOSTIP}" -DNSTREAMS="${NSTREAMS}" -DIN_SOURCE="${IN_SOURCE}" -DNCPU="${NCPU}" -DRESIZE_FLAG="${RESIZE_FLAG}" -DMODEL_NAME="${MODEL_NAME}" -DCUSTOM_MODEL_FLAG="${CUSTOM_MODEL_FLAG}" -DOMIT_DETECTIONS_FLAG="${OMIT_DETECTIONS_FLAG}" -I "${DIR}" "${DIR}/docker-compose.yml.m4" > "${DIR}/docker-compose.yml" fi diff --git a/deployment/docker-swarm/udf.m4 b/deployment/docker-swarm/udf.m4 index 674609e..d31f471 100644 --- a/deployment/docker-swarm/udf.m4 +++ b/deployment/docker-swarm/udf.m4 @@ -17,8 +17,8 @@ HTTP_PROXY: "${HTTP_PROXY}" https_proxy: "${https_proxy}" HTTPS_PROXY: "${HTTPS_PROXY}" - no_proxy: "video-service,vdms-service,${no_proxy}" - NO_PROXY: "video-service,vdms-service,${NO_PROXY}" + no_proxy: "fastapi-service,video-service,vdms-service,${no_proxy}" + NO_PROXY: "fastapi-service,video-service,vdms-service,${NO_PROXY}" volumes: - /etc/localtime:/etc/localtime:ro - app-content:/var/www:ro @@ -48,8 +48,8 @@ HTTP_PROXY: "${HTTP_PROXY}" https_proxy: "${https_proxy}" HTTPS_PROXY: "${HTTPS_PROXY}" - no_proxy: "video-service,vdms-service,${no_proxy}" - NO_PROXY: "video-service,vdms-service,${NO_PROXY}" + no_proxy: "fastapi-service,video-service,vdms-service,${no_proxy}" + NO_PROXY: "fastapi-service,video-service,vdms-service,${NO_PROXY}" volumes: - /etc/localtime:/etc/localtime:ro - app-content:/var/www:ro diff --git a/deployment/docker-swarm/video.m4 b/deployment/docker-swarm/video.m4 index c0e63a0..a2c819a 100644 --- a/deployment/docker-swarm/video.m4 +++ b/deployment/docker-swarm/video.m4 @@ -1,10 +1,5 @@ -define(`PROFILE_DEFAULT', `depends_on: - - udf-service - - vdms-service') -define(`PROFILE_GPU', `depends_on: - - udf-service - - vdms-service - runtime: nvidia +define(`PROFILE_DEFAULT', `') +define(`PROFILE_GPU', `runtime: nvidia deploy: resources: reservations: @@ -14,6 +9,7 @@ define(`PROFILE_GPU', `depends_on: video-service: image: defn(`REGISTRY_PREFIX')lcc_video:stream environment: + YOLO_CONFIG_DIR: "/tmp" RETENTION_MINS: "60" CLEANUP_INTERVAL: "10m" DBHOST: "vdms-service" @@ -32,14 +28,79 @@ define(`PROFILE_GPU', `depends_on: HTTP_PROXY: "${HTTP_PROXY}" https_proxy: "${https_proxy}" HTTPS_PROXY: "${HTTPS_PROXY}" - no_proxy: "vdms-service,udf-service,${no_proxy}" - NO_PROXY: "vdms-service,udf-service,${NO_PROXY}" + no_proxy: "fastapi-service,localhost,127.0.0.1,vdms-service,udf-service,${no_proxy}" + NO_PROXY: "fastapi-service,localhost,127.0.0.1,vdms-service,udf-service,${NO_PROXY}" + secrets: + - source: self_crt + target: /var/run/secrets/self.crt + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0444 + - source: self_key + target: /var/run/secrets/self.key + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0440 volumes: - /etc/localtime:/etc/localtime:ro - app-content:/var/www + - ../../inputs:/watch_dir:ro - ../../inputs/camera_config.yaml:/home/camera_config.yaml:ro + networks: + - appnet + restart: always + depends_on: + - fastapi-service + - udf-service + - vdms-service + + fastapi-service: + shm_size: '2gb' # Give it plenty of space for video frames + image: defn(`REGISTRY_PREFIX')lcc_fastapi:stream + environment: + YOLO_CONFIG_DIR: "/tmp" + DBHOST: "vdms-service" + UDF_HOST: "udf-service" + `MODEL_NAME': "defn(`MODEL_NAME')" + `CUSTOM_MODEL_FLAG': "defn(`CUSTOM_MODEL_FLAG')" + `RESIZE_FLAG': "defn(`RESIZE_FLAG')" + `OMIT_DETECTIONS_FLAG': "defn(`OMIT_DETECTIONS_FLAG')" + CPU_BATCH_SIZE: 1 + GPU_BATCH_SIZE: 1 + `DEBUG': "defn(`DEBUG')" + `DEVICE': "defn(`DEVICE')" + `INGESTION': "defn(`INGESTION')" + WATCH_DIR: "/watch_dir" + http_proxy: "${http_proxy}" + HTTP_PROXY: "${HTTP_PROXY}" + https_proxy: "${https_proxy}" + HTTPS_PROXY: "${HTTPS_PROXY}" + no_proxy: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${no_proxy}" + NO_PROXY: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${NO_PROXY}" + ports: + - target: 80 + published: 30077 + protocol: tcp + mode: host + secrets: + - source: self_crt + target: /var/run/secrets/self.crt + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0444 + - source: self_key + target: /var/run/secrets/self.key + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0440 + volumes: + - /etc/localtime:/etc/localtime:ro + - app-content:/var/www - ../../inputs:/watch_dir:ro networks: - appnet restart: always + depends_on: + - udf-service + - vdms-service ifdef(`GPU', PROFILE_GPU, PROFILE_DEFAULT) diff --git a/fastapi/CMakeLists.txt b/fastapi/CMakeLists.txt new file mode 100644 index 0000000..55d5d41 --- /dev/null +++ b/fastapi/CMakeLists.txt @@ -0,0 +1,2 @@ +set(service "lcc_fastapi") +include("${CMAKE_SOURCE_DIR}/script/service.cmake") diff --git a/fastapi/Dockerfile b/fastapi/Dockerfile new file mode 100644 index 0000000..af2a974 --- /dev/null +++ b/fastapi/Dockerfile @@ -0,0 +1,71 @@ + +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a as build + +ARG DEBIAN_FRONTEND=noninteractive +ENV VIRTUAL_ENV=/opt/venv + +ARG DEBUG="0" +ENV DEBUG="${DEBUG}" + +RUN apt-get update +RUN apt-get install --only-upgrade libc-bin libc6 && \ + apt-get install -y -q --no-install-recommends python3 python3-venv curl libgl1-mesa-glx && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get clean + +RUN python3 -m venv ${VIRTUAL_ENV} +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +# make sure all messages always reach console +ENV PYTHONUNBUFFERED=1 + +COPY --from=lcc_base_video_image:latest ${VIRTUAL_ENV} ${VIRTUAL_ENV} +COPY --from=lcc_base_video_image:latest /home /home + +# activate virtual environment +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +ARG DEVICE="CPU" +ENV DEVICE="${DEVICE}" + +RUN if [ "${DEVICE}" = "CPU" ]; then \ + pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ + else \ + pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ + fi; + +# Set the working directory in the container +WORKDIR /home + +COPY resources /home/resources +COPY requirements.* /home/ +RUN python -m pip install pip --upgrade --no-cache-dir && \ + python -m pip install --no-cache-dir -r requirements.txt && \ + python -m pip install --no-cache-dir -r requirements.GPU.txt + +COPY *.py /home/ +COPY include /home/include +# COPY templates /home/templates +COPY nginx.conf /etc/nginx/nginx.conf + +# CMD /usr/local/sbin/nginx && uvicorn main:app --host 127.0.0.1 --port 8000 +# EXPOSE 80 8000 8080 +# CMD /usr/local/sbin/nginx -g 'daemon on;' && python3 /home/resources/models/download_yolo.py && python3 main.py +CMD ["/bin/bash","-c","/usr/local/sbin/nginx -g 'daemon on;' && python /home/resources/models/download_yolo.py && python3 main.py"] +EXPOSE 80 8000 8080 + +#### +ARG USER +ARG GROUP +ARG UID +ARG GID +## must use ; here to ignore user exist status code +## --no-log-init option can prevent the creation of large, sparse files in /var/log/lastlog and /var/log/faillog +RUN if [ ${GID} -gt 0 ]; then groupadd -f -g ${GID} ${GROUP}; fi; \ + if [ ${UID} -gt 0 ]; then useradd --no-log-init -d /home/${USER} -g ${GID} -K UID_MAX=${UID} -K UID_MIN=${UID} ${USER}; fi; \ + touch /var/run/nginx.pid && \ + mkdir -p /var/log/nginx /var/lib/nginx /var/www/cache /var/www/gen /var/www/mp4 /var/www/logwatch && \ + chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home +VOLUME ["/var/www"] +USER ${UID} +#### diff --git a/fastapi/build.sh b/fastapi/build.sh new file mode 100755 index 0000000..6428e1b --- /dev/null +++ b/fastapi/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +IMAGE="lcc_fastapi" +DIR=$(dirname $(readlink -f "$0")) + +# Make sure user provided models are available to FastAPI +cp -rp "$DIR/../video/resources" $DIR/ + +. "$DIR/../script/build.sh" + +rm -rf "$DIR/resources" diff --git a/fastapi/include/utils.py b/fastapi/include/utils.py new file mode 100644 index 0000000..84668d5 --- /dev/null +++ b/fastapi/include/utils.py @@ -0,0 +1,1554 @@ +# Copyright (C) 2025 Intel Corporation + +import os +import shlex +import subprocess +import time +import traceback +from dataclasses import dataclass +from datetime import datetime +from math import ceil +from pathlib import Path + +import cv2 +import numpy as np + +# import streamlit as st +from ultralytics import YOLO + +import vdms + +""" +GENERAL DEFINITIONS/FUNCTIONS +""" + + +def safely_join_path(base_dir, add_path): + safe_base = os.path.abspath(base_dir) + candidate_path = os.path.abspath(os.path.join(safe_base, add_path)) + if not candidate_path.startswith(safe_base + os.sep): + raise ValueError(f"Invalid path: {candidate_path}") + return candidate_path + + +def str2bool(in_val): + if isinstance(in_val, bool): + return in_val + + if not isinstance(in_val, str): + raise ValueError(f"{in_val} is not a bool or string") + + if in_val.title() == "True": + return True + else: + return False + + +PROJECT_PATH = Path(__file__).parent.parent +CONDITION_OPTIONS = ["", ">", ">=", "<", "<=", "==", "!="] + +AVAILABLE_MODELS = [ # Default model listed first + "Ultralytics-yolo11n-ov-FP16", + "Ultralytics-yolo11n-pt-FP16", +] + +YOLO_BATCH_SIZE = 1 +DBPORT = 55555 +DETECTION_THRESHOLD = 0.25 +DEVICE_OV = "AUTO" +DYNAMIC_FLAG = True +FILETYPES = ["mp4", "avi"] +HALF_FLAG = True +IOU_THRESHOLD = 0.7 +MODEL_PRECISION = "FP16" +MODEL_W, MODEL_H = (640, 640) +NUM_USUABLE_CPUS = 2 +TARGET_FPS = 15 +UDF_PORT = 5011 +WRITER_FOURCC = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v + +CODE_DIR = os.getenv("CODE_DIR", "/home") +CUSTOM_MODEL_FLAG = str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)) +DBHOST = os.getenv("DBHOST", "vdms-service") +DEBUG = os.getenv("DEBUG", "0") +DEBUG_FLAG = True if DEBUG == "1" else False +# DEVICE = os.environ.get("DEVICE", "CPU") +DEVICE = os.getenv("DEVICE", "CPU") +device_input = DEVICE.lower() if DEVICE == "CPU" else "cuda" +INGESTION = os.getenv("INGESTION", "object,face") +MODEL_NAME = os.getenv("MODEL_NAME", "yolo11n") +OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) +RESIZE_FLAG = str2bool(os.getenv("RESIZE_FLAG", False)) +RESIZE_FLAG = str2bool(os.getenv("RESIZE_FLAG", False)) +SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") +Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) +TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) +TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") +UDF_HOST = os.getenv("UDF_HOST", "fastapi-service") + +if DEVICE == "GPU": + EXPORT_BATCH_SIZE = int(os.environ.get("GPU_BATCH_SIZE", 1)) + os.environ["CUDA_VISIBLE_DEVICES"] = "0" + print("[!] USING GPU") +else: + EXPORT_BATCH_SIZE = int(os.environ.get("CPU_BATCH_SIZE", 1)) # 8 + print("[!] USING CPU") + +if not TEST_MODE: + db = vdms.vdms() + db.connect(DBHOST, DBPORT) + +LOCKTIMEOUT_RETRIES = 5 +ERR_KEYWORDS = [ + "timeout", + "null search iterator", + "outoftransactions", + "internal server", +] + + +def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): + global db + for ridx in range(num_retries + 1): + response, _ = db.query(query, [[]]) + if "FailedCommand" in response[0] and any( + k in response[0]["info"].lower() for k in ERR_KEYWORDS + ): + err = response[0]["info"] + if DEBUG == "1": + query_type = list(query[0].keys())[0] + print( + f"DEBUG [process_stream Attempt #{ridx}] Received '{err}' for {query_type} query", + flush=True, + ) + if sleep_timer > 0: + time.sleep(sleep_timer) + else: + if DEBUG == "1": + print( + f"[DEBUG process_stream] Successful query response: {response}", + flush=True, + ) + break # Continue + return response + + +# # This code is based on https://github.com/streamlit/demo-self-driving/blob/230245391f2dda0cb464008195a470751c01770b/streamlit_app.py#L48 # noqa: E501 +# def download_file(url, download_to: Path, expected_size=None): +# # Don't download the file twice. +# # (If possible, verify the download using the file length.) +# if download_to.exists(): +# if expected_size: +# if download_to.stat().st_size == expected_size: +# return +# else: +# st.info(f"{url} is already downloaded.") +# if not st.button("Download again?"): +# return + +# download_to.parent.mkdir(parents=True, exist_ok=True) + +# # These are handles to two visual elements to animate. +# weights_warning, progress_bar = None, None +# try: +# weights_warning = st.warning("Downloading %s..." % url) +# progress_bar = st.progress(0) +# with open(download_to, "wb") as output_file: +# with urllib.request.urlopen(url) as response: +# length = int(response.info()["Content-Length"]) +# counter = 0.0 +# MEGABYTES = 2.0**20.0 +# while True: +# data = response.read(8192) +# if not data: +# break +# counter += len(data) +# output_file.write(data) + +# # We perform animation by overwriting the elements. +# weights_warning.warning( +# "Downloading %s... (%6.2f/%6.2f MB)" +# % (url, counter / MEGABYTES, length / MEGABYTES) +# ) +# progress_bar.progress(min(counter / length, 1.0)) +# # Finally, we remove these visual elements by calling .empty(). +# finally: +# if weights_warning is not None: +# weights_warning.empty() +# if progress_bar is not None: +# progress_bar.empty() + + +def format_df_value(value): + if value is None: + return value + if value.isdigit(): + if "." in value: + return float(value) + else: + return int(value) + return value + + +def get_model( + MODEL_NAME, + model_dir, + run_platform, + device_input, + batch=1, + half_flag=True, + dynamic_flag=True, +): + final_model_path = f"{model_dir}/{MODEL_NAME}.pt" + pt_detection_model = YOLO(final_model_path, verbose=False, task="detect") + if run_platform == "ov": + final_model_path = f"{model_dir}/{MODEL_NAME}_openvino_model/" + if not Path(final_model_path).exists(): + pt_detection_model.export( + format="openvino", + half=half_flag, + dynamic=dynamic_flag, + device=device_input, + batch=batch, + ) + + object_detection_model = YOLO( + final_model_path, + verbose=False, + task="detect", + ) + + # det_ov_model = core.read_model(final_model_path+"yolo11n.xml") + # ov_config = {hints.performance_mode: hints.PerformanceMode.LATENCY} + # if device == "GPU": + # ov_config["GPU_DISABLE_WINOGRAD_CONVOLUTION"] = "YES" + # compiled_model = core.compile_model(det_ov_model, device, ov_config) + # object_detection_model.predictor.model.ov_compiled_model = compiled_model + + elif run_platform == "engine": + final_model_path = f"{model_dir}/{MODEL_NAME}.engine" + if not Path(final_model_path).exists(): + pt_detection_model.export( + format="engine", + half=half_flag, + imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) + dynamic=dynamic_flag, + device=device_input, + simplify=True, + batch=batch, + ) + + object_detection_model = YOLO( + final_model_path, + verbose=False, + task="detect", + ) + + elif run_platform == "onnx": + from torch import cuda + from ultralytics.utils.checks import check_requirements + + check_requirements( + "onnxruntime-gpu" + if cuda.is_available() and device_input != "cpu" + else "onnxruntime" + ) + + final_model_path = f"{model_dir}/{MODEL_NAME}.onnx" + if not Path(final_model_path).exists(): + pt_detection_model.export( + format="onnx", + half=half_flag, + dynamic=dynamic_flag, + device=device_input, + simplify=True, + batch=batch, + ) + + object_detection_model = YOLO(final_model_path, verbose=False, task="detect") + + elif run_platform == "pt": + object_detection_model = pt_detection_model + if device_input == "cuda": + object_detection_model.to("cuda") + else: + object_detection_model.to(device_input) + + else: + raise ValueError(f"[!] Model for {run_platform} is not implemented.") + + return ( + object_detection_model, + final_model_path, + list(object_detection_model.names.values()), + ) + + +def get_models(model_tag: str, model_dir=PROJECT_PATH / "models"): # , _st_sidebar): + # FW-Model Name-TYPE + fw, model_name, model_fw, model_precision = model_tag.split("-") + + if fw == "Ultralytics": + model, model_path, labels = get_model( + model_name, + model_dir / f"ultralytics/{model_precision}", + model_fw, + device_input, + batch=EXPORT_BATCH_SIZE, + half_flag=HALF_FLAG, + dynamic_flag=DYNAMIC_FLAG, + ) + else: + raise ValueError(f"Model ({model_tag}) not implemented") + + return model, model_path, labels + + +# Manual FPS calculation if OpenCV reports 0 +def manual_fps_calculation(src, num_frames=10): + vid_obj = cv2.VideoCapture(src) + + frame_count = 0 + start_t = time.time() + + while frame_count < num_frames: + grabbed, frame = vid_obj.read() + + if not grabbed: + break + + frame_count += 1 + + end_t = time.time() + vid_obj.release() + + elapsed_t = end_t - start_t + + if elapsed_t > 0: + return frame_count / elapsed_t + else: + return 0 + + +# def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): +# global db +# for ridx in range(num_retries + 1): +# response, _ = db.query(query, [[]]) +# if "FailedCommand" in response[0] and any( +# k in response[0]["info"].lower() for k in ERR_KEYWORDS +# ): +# err = response[0]["info"] +# if DEBUG == "1": +# query_type = list(query[0].keys())[0] +# print( +# f"DEBUG [process_stream Attempt #{ridx}] Received '{err}' for {query_type} query", +# flush=True, +# ) +# if sleep_timer > 0: +# time.sleep(sleep_timer) +# else: +# if DEBUG == "1": +# print( +# f"[DEBUG process_stream] Successful query response: {response}", +# flush=True, +# ) +# break # Continue +# return response + + +# Extract metadata from object model results +def extract_metadata_from_results( + stream_name, frameNum, results, img_size, fps=TARGET_FPS +): + fW, fH = img_size + metadata = dict() + try: + for _, result in enumerate(results): + # GET METADATA FOR CLIP + boxes = result.boxes.cpu() + oidx = 0 + for box in boxes: + confidence = float(box.conf.item()) + if confidence > DETECTION_THRESHOLD: + class_id = int(box.cls.item()) + class_name = str(result.names[class_id]) + + if not OMIT_DETECTIONS_FLAG: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + print( + # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", + f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", + flush=True, + ) + x1, y1, x2, y2 = box.xyxy.tolist()[0] + height = min(y2, fH) - max(0, y1) + width = min(x2, fW) - max(0, x1) + object_res = [ + x1, + y1, + height, + width, + result.names[class_id], + confidence, + fH, + fW, + ] + + framenum_str = f"{frameNum:04d}_{oidx:04d}" + if DEBUG_FLAG: + meta_str = ",".join( + [str(o) for o in object_res + [framenum_str]] + ) + print(f"[{stream_name} METADATA],{meta_str}", flush=True) + + metadata[framenum_str] = { + "frameId": frameNum, + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(fH), + "frameW": int(fW), + }, + }, + } + oidx += 1 + + except Exception: + e = traceback.format_exc() + print(f"Error in {stream_name} extract_metadata_from_results: {e}", flush=True) + + return metadata + + +# Release Video Writer object and re-encode video to seek via ffmpeg later +def release_clip_and_reencode(clip_key, _out_vid, clip_filename, tmp_file, target_fps): + if DEBUG == "1": + print( + f"[TIMING],start_release_clip,{clip_key},{time.time()}", + flush=True, + ) + _out_vid.release() + if DEBUG == "1": + print( + f"[TIMING],end_release_clip,{clip_key},{time.time()}", + flush=True, + ) + _out_vid = None + + # Re-encode video in order to seek via ffmpeg later + GENERAL_OPTS = "-flags -global_header -hide_banner -loglevel error -nostats -tune zerolatency -flush_packets 0" # -filter:v fps={target_fps} + CONVERSION = f"-c:v libx264 -preset ultrafast -filter:v fps=fps={target_fps}" # "-c:v libx264 -preset medium" + reencode_cmd = f"ffmpeg -y -i {tmp_file} {GENERAL_OPTS} {CONVERSION} -crf 23 -c:a copy {clip_filename}" + cmd_list = shlex.split(reencode_cmd) + if DEBUG == "1": + print( + f"[TIMING],start_reencode,{clip_key},{time.time()}", + flush=True, + ) + subprocess.run(cmd_list, check=True) + end_time = time.time() + # filename = str(Path(clip_filename).name) + if DEBUG == "1": + print( + f"[TIMING],end_reencode,{clip_key},{end_time}", + flush=True, + ) + print(f"[TIMING],Save clip,{clip_key},{end_time}", flush=True) + os.remove(tmp_file) + return _out_vid + + +# """ +# VDMS RELATED FUNCTIONS +# """ + + +# def vdms_connection_status(dbhost: str = "localhost", dbport: int = 55555): +# db = vdms.vdms() +# availability_status = False +# try: +# availability_status = db.connect(dbhost, int(dbport)) +# except Exception: +# pass +# return availability_status + + +# def initialize_vdms_df(): +# st.session_state.vdms_instance_df = pd.DataFrame( +# columns=["Container Name", "Hostname", "Port", "Status"] +# ) + + +# def search_for_vdms_instances(): +# if st.button("Search for VDMS demo instances"): +# with st.spinner("Processing..."): +# info_str = get_vdms_instances() + +# st.info(info_str) + + +# def get_docker_status(dbhost="localhost", dbport=55555): +# try: +# get_containers_cmd = 'docker ps --filter name=vdms_log_pipeline --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"' +# output = subprocess.check_output(get_containers_cmd, shell=True) +# condition = "vdms_log_pipeline" in output.decode("utf-8") +# except Exception: +# db = vdms.vdms() +# condition = db.connect(dbhost, dbport) +# if condition: +# return "Deployed" +# else: +# return "Not Deployed" + + +# def get_vdms_instances(): +# get_containers_cmd = 'docker ps --filter name=vdms_.*_demo_test --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"' +# output = subprocess.check_output(get_containers_cmd, shell=True) + +# initialize_vdms_df() + +# lines = [line for line in output.decode("utf-8").split("\n")[1:] if line] +# line_names = ["ID", "Container Name", "Ports"] +# new_instances = 0 +# for line in lines: +# line_split = line.split() +# line_data = line_split[:2] + ["".join(line_split[2:])] + +# name = line_data[line_names.index("Container Name")] +# hostname = "localhost" +# port_str = line_data[line_names.index("Ports")] +# end_idx = port_str.find("->55555/tcp") +# port = port_str[port_str.find(":") + 1 : end_idx] +# availability_status = vdms_connection_status(hostname, int(port)) + +# new_data = pd.DataFrame( +# { +# "Container Name": [name], +# "Hostname": [hostname], +# "Port": [port], +# "Status": ["Connected" if availability_status else "Not Available"], +# } +# ) +# st.session_state.vdms_instance_df = pd.concat( +# [st.session_state.vdms_instance_df, new_data], ignore_index=True +# ) +# new_instances += 1 + +# info_str = f"{new_instances} VDMS instances found" + +# st.session_state.vdms_instance_df.sort_values( +# by=["Container Name"], inplace=True, ignore_index=True +# ) +# st.session_state.vdms_instance_df.drop_duplicates(inplace=True, ignore_index=True) + +# return info_str + + +# def add_vdms_instance_buttons(vdms_details): +# dbhost = vdms_details[1] +# dbport = vdms_details[2] + +# left_column, right_column = st.columns([0.5, 0.5]) + +# right_column.checkbox("Kill & Restart if DB exists", key="Kill_restart") + +# if left_column.form_submit_button("Add", use_container_width=True): +# if all([arg != "" for arg in [dbhost, dbport]]): +# matching_idx = st.session_state.vdms_instance_df.loc[ +# (st.session_state.vdms_instance_df["Hostname"] == dbhost) +# & (st.session_state.vdms_instance_df["Port"] == dbport) +# ].index.tolist() + +# if not st.session_state.Kill_restart and len(matching_idx) != 0: +# vdms_method = "Use existing DB" +# else: +# vdms_method = "Fresh DB" + +# if len(matching_idx) > 0: +# instance_num = matching_idx[0] +# container_name = st.session_state.vdms_instance_df.at[ +# instance_num, "Container Name" +# ] + +# # Start instance locally +# _ = start_vdms_docker( +# container_name=container_name, +# project_path=PROJECT_PATH, +# dbport=int(dbport), +# vdms_method=vdms_method, +# ) +# info_str = ( +# f"Instance #{instance_num} ({container_name}) already running" +# ) +# if vdms_method == "Fresh DB": +# info_str += " but redeploying" +# st.info(info_str) + +# else: +# instance_num = st.session_state.vdms_instance_df.shape[0] + 1 +# container_name = f"vdms_{instance_num}_demo_test" + +# # Start instance locally +# _ = start_vdms_docker( +# container_name=container_name, +# project_path=PROJECT_PATH, +# dbport=int(dbport), +# vdms_method=vdms_method, +# ) +# vdms_details[0] = container_name + +# # Check is available for connection +# availability_status = vdms_connection_status(dbhost, int(dbport)) +# vdms_details[-1] = ( +# "Connected" if availability_status else "Not Available" +# ) + +# if vdms_details[-1] == "Connected": +# st.session_state.vdms_instance_df.loc[instance_num] = vdms_details +# info_str = f"Instance #{instance_num} ({container_name}) added" +# st.info(info_str) +# else: +# st.error("Cannot connect to instance; Check server") + +# else: +# st.error("Must provide VDMS Port") + +# st.session_state.vdms_instance_df.sort_values( +# by=["Container Name"], inplace=True, ignore_index=True +# ) +# st.session_state.vdms_instance_df.drop_duplicates(inplace=True, ignore_index=True) + + +# def add_vdms_instance(): +# col_count = st.session_state.vdms_instance_df.shape[1] + +# with st.form(key="add form", clear_on_submit=True): +# cols = st.columns(1) +# vdms_details = [] + +# col_idx = 0 +# for i in range(col_count): +# value = "" +# if st.session_state.vdms_instance_df.columns[i] in ["Hostname", "Port"]: +# if st.session_state.vdms_instance_df.columns[i] == "Hostname": +# # Only local deployment supported for demo +# value = "localhost" + +# if st.session_state.vdms_instance_df.columns[i] == "Port": +# value = str( +# cols[col_idx].text_input( +# st.session_state.vdms_instance_df.columns[i] +# ) +# ) + +# vdms_details.append(value) + +# add_vdms_instance_buttons(vdms_details) + + +# def populate_vdms_instances(): +# if "vdms_instance_df" not in st.session_state: +# initialize_vdms_df() + +# st.markdown("1. If instances already deployed, search for them below.") +# search_for_vdms_instances() +# st.markdown("\n\n") + +# st.markdown("2. Provide local port to deploy instance.") +# add_vdms_instance() +# st.markdown("\n\n") + +# st.markdown("### VDMS Instances") +# st.dataframe(st.session_state.vdms_instance_df, use_container_width=True) + + +MASK_THRESHOLD_VALUE = 127 +MASK_MAX_VALUE = 255 +MAX_DETECTIONS = 100 + +# Plot variables +THICKNESS_SCALE_FACTOR = 1e-3 +FONT_SCALE_FACTOR = 1e-3 + + +YOLO_CLASS_NAMES = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", +] + +PLOT_HEXS = ( + "042AFF", + "0BDBEB", + "F3F3F3", + "00DFB7", + "111F68", + "FF6FDD", + "FF444F", + "CCED00", + "00F344", + "BD00FF", + "00B4FF", + "DD00BA", + "00FFFF", + "26C000", + "01FFB3", + "7D24FF", + "7B0068", + "FF1B6C", + "FC6D2F", + "A2FF0B", +) + +DETECTION_COLORS = [] +for h in PLOT_HEXS: + DETECTION_COLORS.append( + tuple(int(f"#{h}"[1 + i : 1 + i + 2], 16) for i in (0, 2, 4)) + ) + + +def get_detection_color(index, is_bgr=False): + ind = int(index) % len(PLOT_HEXS) + color = DETECTION_COLORS[ind] + if is_bgr: + return (color[2], color[1], color[0]) + else: + return color + + +def get_line_thickness(npixels, ref_pixels=(1280 * 720)): + ref_thickness = 1 + factor = npixels / ref_pixels + thickness = int(ref_thickness * factor) + if thickness < 1: + thickness = 1 + return thickness + + +def draw_label( + image, + label, + txt_bt_lft_corner, + font_face=cv2.FONT_HERSHEY_SIMPLEX, + color=(255, 255, 255), + padding=5, +): + height, width, _ = image.shape + + # Scale font and thickness based on the image's smaller dimension + scaled_font_scale = min(width, height) * FONT_SCALE_FACTOR + scaled_thickness = max(1, ceil(min(width, height) * THICKNESS_SCALE_FACTOR)) + + # Get text size and define position for the label background + (label_W, label_H), baseline = cv2.getTextSize( + label, font_face, scaled_font_scale, scaled_thickness + ) + label_y1 = (txt_bt_lft_corner[0], txt_bt_lft_corner[1] - label_H - padding) + label_y2 = ( + txt_bt_lft_corner[0] + label_W + padding, + txt_bt_lft_corner[1] + baseline, + ) + cv2.rectangle(image, label_y1, label_y2, color, -1) + + # Print label + cv2.putText( + image, + label, + (txt_bt_lft_corner[0] + padding // 2, txt_bt_lft_corner[1] - padding // 2), + font_face, + scaled_font_scale, + (0, 0, 0), # Black text + scaled_thickness, + cv2.LINE_AA, + ) + + +@dataclass +class PipelineMapping: + resize_device: str.lower = "cpu" + bkgd_subtraction_device: str.lower = "cpu" + threshold_device: str.lower = "cpu" + erodeAndDilate_device: str.lower = "cpu" + contour_device: str.lower = "cpu" + detection_device: str.lower = "cpu" + + +def merge_boxes_limit(bbs_full_res, dist_threshold=50, size_limit=640): + """ + boxes: list of [x1, y1, x2, y2] + dist_threshold: max distance between boxes to consider them 'connected' + size_limit: max width/height for a merged box + """ + if len(bbs_full_res) == 0: + return [] + + rects = np.array(bbs_full_res) + num_boxes = len(rects) + parent = list(range(num_boxes)) + + def find(i): + if parent[i] == i: + return i + parent[i] = find(parent[i]) + return parent[i] + + def union(i, j): + root_i, root_j = find(i), find(j) + if root_i != root_j: + # Check if merging exceeds size limit + temp_x1 = min(rects[root_i][0], rects[root_j][0]) + temp_y1 = min(rects[root_i][1], rects[root_j][1]) + temp_x2 = max(rects[root_i][2], rects[root_j][2]) + temp_y2 = max(rects[root_i][3], rects[root_j][3]) + + if (temp_x2 - temp_x1 <= size_limit) and (temp_y2 - temp_y1 <= size_limit): + parent[root_i] = root_j + # Update the root rectangle to the new merged bounds + rects[root_j] = [temp_x1, temp_y1, temp_x2, temp_y2] + + # 2. Compare boxes (Optimized: only check nearby ones if sorted by X) + for i in range(num_boxes): + for j in range(i + 1, num_boxes): + # Proximity check (Manhattan distance or check if boxes are 'close') + dx = max(0, max(rects[i][0], rects[j][0]) - min(rects[i][2], rects[j][2])) + dy = max(0, max(rects[i][1], rects[j][1]) - min(rects[i][3], rects[j][3])) + + if dx < dist_threshold and dy < dist_threshold: + union(i, j) + + # 3. Extract unique merged boxes + final_boxes = [] + unique_roots = set() + for i in range(num_boxes): + root = find(i) + if root not in unique_roots: + unique_roots.add(root) + final_boxes.append(rects[root]) + + return final_boxes + + +def filter_contained_boxes(boxes, containment_thresh=0.90): + """ + Deletes redundant boxes that are mostly inside another larger box. + """ + if not boxes: + return [] + + # 1. Sort by area (Largest boxes first) + boxes = sorted(boxes, key=lambda b: (b[2] - b[0]) * (b[3] - b[1]), reverse=True) + keep = [] + + for child in boxes: + is_contained = False + for parent in keep: + # Intersection coordinates + ix1, iy1 = max(child[0], parent[0]), max(child[1], parent[1]) + ix2, iy2 = min(child[2], parent[2]), min(child[3], parent[3]) + + if ix2 > ix1 and iy2 > iy1: + inter_area = (ix2 - ix1) * (iy2 - iy1) + child_area = (child[2] - child[0]) * (child[3] - child[1]) + + # If child is 90% inside a larger box, it's redundant + if inter_area / child_area >= containment_thresh: + is_contained = True + break + + if not is_contained: + keep.append(child) + + return keep + + +# from random import randint +# # Generate and run UDF query +# def get_udf_query( +# filename_path, +# properties, +# ingest_mode, +# new_size, +# id="udf_metadata", +# metadata=None, +# test_mode=TEST_MODE, +# ): +# query = { +# "AddVideo": { +# "from_file_path": str(filename_path), # from_server_file +# "is_local_file": True, +# "properties": properties, +# "operations": [ +# { +# "type": "syncremoteOp", # "remoteOp", +# "url": f"http://{UDF_HOST}:{UDF_PORT}/video", +# "options": { +# "id": id, +# "otype": ingest_mode, +# "media_type": "video", +# "input_sizeWH": new_size, +# "filename": properties["Name"], +# "ingestion": 1, +# }, +# } +# ], +# } +# } + +# if id == "udf_metadata" and metadata is not None: +# query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata + +# if test_mode: +# return + +# filename = str(Path(filename_path).name) +# if DEBUG_FLAG: +# print( +# f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), +# flush=True, +# ) +# try: +# res = retry_query([query], sleep_timer=randint(1, 5)) + +# if DEBUG_FLAG: +# print( +# f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), +# flush=True, +# ) +# print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) +# print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) +# except Exception: +# e = traceback.format_exc() +# print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) + +# # elapsed_time = time.time() - start_t + +# # db.disconnect() +# # del db + + +# def _sort_dict_by_frame(in_dict): +# def _by_int(key): +# return tuple(int(k) for k in key.split("_")) + +# return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) + + +# # method to send metadata to VDMS once clip is saved +# def metadata2vdms( +# clip_key, +# clip_filename, +# clip_metadata, +# width, +# height, +# ): +# if DEBUG == "1": +# print( +# f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", +# flush=True, +# ) + +# # Send metadata to UDF +# properties = { +# "Name": clip_key, # .split("/")[-1], +# "category": "video_path_rop", +# } + +# combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} +# if "face" in clip_metadata: +# for face_frameidx_bbidx, value in clip_metadata["face"].items(): +# face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") +# max_obj_idx = 0 +# for obj_frameidx_bbidx in combined_metadata: +# if face_frameidx in obj_frameidx_bbidx: +# _, obj_bbidx_ = obj_frameidx_bbidx.split("_") +# max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) + +# if max_obj_idx > 0: +# new_face_bbidx = max_obj_idx + 1 +# new_key = f"{face_frameidx}_{new_face_bbidx:04d}" +# combined_metadata[new_key] = value +# combined_metadata[new_key]["bbId"] = new_key +# else: +# combined_metadata[face_frameidx_bbidx] = value + +# combined_metadata = _sort_dict_by_frame(combined_metadata) +# get_udf_query( +# clip_filename, +# properties, +# INGESTION.replace(",", "+"), +# (width, height), +# id="udf_metadata", +# metadata=combined_metadata, +# test_mode=TEST_MODE, +# ) + +# if DEBUG == "1": +# print( +# f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", +# flush=True, +# ) + + +# # method to create clips (read frame write to file; add name to list) +# # def send_metadata(): +# # global all_metadata +# # clip_filename = "" +# # clip_key = "" +# # width = 0 +# # height = 0 +# # while True: +# # try: +# # queue_details = send_metadata_queue.get() +# # if queue_details is None: +# # break + +# # (clip_key, clip_filename, width, height) = queue_details + +# # metadata2vdms( +# # clip_key, +# # clip_filename, +# # all_metadata[clip_key], +# # width, +# # height, +# # ) +# # del all_metadata[clip_key] + +# # except queue.Empty: +# # pass + + +# class StreamProcessor: +# def __init__(self, model_path, source, name): +# self.model = YOLO(model_path, task="detect") +# self.cap = cv2.VideoCapture(source) +# self.name = name +# self.source =source + +# self.video_writer = None +# self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") +# self.clip_id = 0 +# self.clip_filename = "" +# self.clip_key = "" +# self.tmp_file = "" + +# self.resize_h, self.resize_w = [MODEL_H, MODEL_W] +# self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) +# self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) +# self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) +# self.fps = int(self.cap.get(cv2.CAP_PROP_FPS)) +# self.scale_x = self.frame_width / MODEL_W +# self.scale_y = self.frame_height / MODEL_H +# self.min_contour_area = int((0.005 * self.frame_width) * (0.005 * self.frame_height) ) # 207 + +# self.operation_device_map = PipelineMapping(detection_device="cpu") # No CUDA HERE +# self.device_input = ( +# self.operation_device_map.detection_device +# if self.operation_device_map.detection_device == "cpu" +# else "cuda" +# ) + +# self.cpu_resized_frame = None + +# # Subtraction +# history= 300 # int(5 * self.fps) +# background_thresh = 350 +# NSamples = 10 +# kNNSamples = 2 +# self.lr = -1 #.01 #-1 # 0.001 #1 / (5 * self.fps) # -1 # 0.01 # 1 / history +# bkgd_mask_queue_size = 3 +# self.backSub_cpu = cv2.createBackgroundSubtractorKNN( +# history=history, # default 500 +# dist2Threshold=background_thresh, # default 400 +# detectShadows=False, # default True +# ) +# self.backSub_cpu.setkNNSamples(kNNSamples) +# self.backSub_cpu.setNSamples(NSamples) + +# prev_bkgd = np.zeros((MODEL_H, MODEL_W), dtype="uint8") +# self.mask_history = deque(maxlen=bkgd_mask_queue_size) +# self.mask_history.append(prev_bkgd) + +# self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) +# self.dilate_kernel_for_enhanced_mask = np.ones((21,21), np.uint8) + +# # Create ThreadPoolExecutor +# self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) + +# def new_get_detections_for_contours_bbs( +# self, frameNum, foi, contours, thickness=2, device_input="cuda" +# ): +# global active_streams +# # source = self.source +# stream_name = self.name +# num_objs = 0 +# # predictions = [] +# metadata = dict() +# cropped_imgs, cropped_coords = [], [] +# H, W = foi.shape[:2] # Unpack once +# bbs_full_res = [] + +# # Filter and Sort in one go (Minimize Python-to-C++ crossings) +# raw_bbs=[] +# padding = 64 +# for c in contours: +# area = cv2.contourArea(c) +# x1,y1,w,h = cv2.boundingRect(c) +# if area > self.min_contour_area: # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect +# xx1 = max(0, int((x1 * self.scale_x)) - padding) +# yy1 = max(0, int((y1 * self.scale_y)) - padding) +# xx2 = min(W, int(((x1+w) * self.scale_x)) + padding) +# yy2 = min(H, int(((y1+h) * self.scale_y)) + padding) +# raw_bbs.append([area,[xx1,yy1,xx2,yy2]]) +# bbs_full_res = sorted( +# [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], +# key=lambda x: x[0], +# reverse=True, +# )[:MAX_DETECTIONS] + +# dist_thresh = min(0.05*W,0.05*H) +# merged = merge_boxes_limit(bbs_full_res, dist_threshold=dist_thresh, size_limit=640) + +# merged = filter_contained_boxes(merged, containment_thresh=0.9) + +# # for cnt, area in merged: +# for (x1, y1, x2, y2) in merged: + +# if x2 > x1 and y2 > y1 and (x2-x1) < self.frame_width and (y2-y1) < self.frame_height : +# crop = foi[y1:y2, x1:x2] +# if crop.size > 0: +# cropped_imgs.append(crop) +# cropped_coords.append((x1, y1)) + +# if not cropped_imgs: +# return metadata #num_objs, predictions + +# # 2. Inference (Keep stream=False as it is stable) +# results = self.model.predict( +# cropped_imgs, +# imgsz=MODEL_W, +# batch=len(cropped_imgs), +# device=device_input, +# verbose=False, +# stream=True, +# max_det=MAX_DETECTIONS, +# # classes=[0], # only "person", +# # conf=0.45, +# ) + +# label_source = ( +# self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES +# ) + +# for ridx, r in enumerate(results): +# if r.boxes is None or len(r.boxes) == 0: +# continue + +# # Move to CPU in one bulk operation per crop +# boxes = r.boxes.xyxy.cpu().numpy().astype(int) +# clss = r.boxes.cls.cpu().numpy().astype(int) +# confs = r.boxes.conf.cpu().numpy() +# off_x, off_y = cropped_coords[ridx] + +# for j in range(len(boxes)): +# num_objs += 1 +# bx1, by1, bx2, by2 = boxes[j] +# abs_x1, abs_y1 = off_x + bx1, off_y + by1 +# abs_x2, abs_y2 = off_x + bx2, off_y + by2 +# class_id = clss[j] +# class_name = label_source[class_id] +# confidence = confs[j] +# if confidence > DETECTION_THRESHOLD: +# if not OMIT_DETECTIONS_FLAG: +# timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] +# print( +# # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", +# f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", +# flush=True, +# ) + +# bb_color = get_detection_color(class_id, is_bgr=True) + +# cv2.rectangle( +# foi, +# (abs_x1, abs_y1), +# (abs_x2, abs_y2), +# bb_color, +# thickness, +# ) +# label = f"{class_name} {confidence:.2f}" +# draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) + +# height = min(abs_y2, H) - max(0, abs_y1) +# width = min(abs_x2, W) - max(0, abs_x1) +# object_res = [ +# abs_x1, +# abs_y1, +# height, +# width, +# class_name, +# confidence, +# H, +# W, +# ] + +# framenum_str = f"{frameNum:04d}_{j:04d}" +# if DEBUG_FLAG: +# meta_str = ",".join( +# [str(o) for o in object_res + [framenum_str]] +# ) +# print(f"[{stream_name} METADATA],{meta_str}", flush=True) + +# metadata[framenum_str] = { +# "frameId": frameNum, +# "bbId": framenum_str, +# "bbox": { +# "x": int(object_res[0]), +# "y": int(object_res[1]), +# "height": int(object_res[2]), +# "width": int(object_res[3]), +# "object": str(object_res[4]), +# "object_det": { +# "confidence": float(object_res[5]), +# "frameH": int(H), +# "frameW": int(W), +# }, +# }, +# } + +# # annotated_frame = r.plot() + +# # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) +# if self.frame_width > 1280: +# display_frame = cv2.resize(foi, (1280, 720), interpolation=cv2.INTER_AREA) +# _, buffer = cv2.imencode(".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) +# else: +# _, buffer = cv2.imencode(".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) +# frame_bytes = buffer.tobytes() + +# # Maintain only the freshest frame in the queue +# if active_streams[self.name].full(): +# try: +# active_streams[self.name].get_nowait() +# except Exception: +# pass +# active_streams[self.name].put(frame_bytes) + +# try: +# # Use block=False so the inference doesn't wait if the UI is slow +# active_streams[self.name].put(frame_bytes, block=False) +# except: +# pass # Skip frame if queue is full + +# # # Handle Video Writing (Cycle every 10 seconds) +# # clip_frameNum = (frameNum - 1) % MAX_FRAMES_PER_CLIP +# # print(f"frameNum: {frameNum} ({clip_frameNum})") +# # if clip_frameNum == 0: +# # if self.video_writer: +# # # video_writer.release() +# # self.video_writer = release_clip_and_reencode( +# # self.clip_key, self.video_writer, self.clip_filename, self.tmp_file, TARGET_FPS +# # ) + +# # send_metadata_queue.put( +# # ( +# # self.clip_key, +# # self.clip_filename, +# # self.frame_width, +# # self.frame_height, +# # ) +# # ) +# # self.clip_id += 1 + +# # if "://" not in str(source): +# # self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{self.clip_id}.mp4" +# # else: +# # self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{time.time()}.mp4" + +# # self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] +# # self.clip_key = Path(self.clip_filename).name + +# # # timestamp = int(time.time()) +# # # filename = f"clip_{timestamp}.mp4" +# # self.video_writer = cv2.VideoWriter(self.tmp_file, self.fourcc, TARGET_FPS, (width, height)) +# # main_app_logger.info(f"Started new clip: {self.tmp_file}") + +# # # 3. Write frame +# # self.video_writer.write(foi) +# # frame_counter += 1 +# return metadata + + +# # if not results: +# # return num_objs, predictions + +# # # 3. Post-processing +# # label_source = ( +# # self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES +# # ) +# # predictions = [] +# # for ridx, r in enumerate(results): +# # if r.boxes is None or len(r.boxes) == 0: +# # continue + +# # # Move to CPU in one bulk operation per crop +# # boxes = r.boxes.xyxy.cpu().numpy().astype(int) +# # clss = r.boxes.cls.cpu().numpy().astype(int) +# # confs = r.boxes.conf.cpu().numpy() +# # off_x, off_y = cropped_coords[ridx] + +# # for j in range(len(boxes)): +# # num_objs += 1 +# # bx1, by1, bx2, by2 = boxes[j] +# # abs_x1, abs_y1 = off_x + bx1, off_y + by1 +# # class_id = clss[j] +# # bb_color = get_detection_color(class_id, is_bgr=True) + +# # cv2.rectangle( +# # foi, +# # (abs_x1, abs_y1), +# # (off_x + bx2, off_y + by2), +# # bb_color, +# # thickness, +# # ) +# # label = f"{label_source[class_id]} {confs[j]:.2f}" +# # draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) +# # predictions.append( +# # [class_id, abs_x1, abs_y1, off_x + bx2, off_y + by2, confs[j]] +# # ) + +# # return num_objs, predictions + +# def new_contour2predictions(self, frameNum, mask, frame, device_input="cpu"): +# # global all_metadata +# manager, active_streams, all_metadata, send_metadata_queue = get_manager_stuff() +# source = self.source +# stream_name = self.name +# # Find movement areas +# # if cv2.countNonZero(mask) > (mask.size * 0.5): +# # return 0, [] +# contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + +# # Handle Video Writing (Cycle every 10 seconds) +# clip_frameNum = (frameNum - 1) % MAX_FRAMES_PER_CLIP +# print(f"frameNum: {frameNum} ({clip_frameNum})") +# if clip_frameNum == 0: +# if self.video_writer: +# # video_writer.release() +# self.video_writer = release_clip_and_reencode( +# self.clip_key, self.video_writer, self.clip_filename, self.tmp_file, TARGET_FPS +# ) + +# send_metadata_queue.put( +# ( +# self.clip_key, +# self.clip_filename, +# self.frame_width, +# self.frame_height, +# ) +# ) +# self.clip_id += 1 + +# if "://" not in str(source): +# self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{self.clip_id}.mp4" +# else: +# self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{time.time()}.mp4" + +# self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] +# self.clip_key = Path(self.clip_filename).name + +# # timestamp = int(time.time()) +# # filename = f"clip_{timestamp}.mp4" +# self.video_writer = cv2.VideoWriter(self.tmp_file, self.fourcc, TARGET_FPS, (self.frame_width, self.frame_height)) +# main_app_logger.info(f"Started new clip: {self.tmp_file}") + +# # 3. Write frame +# self.video_writer.write(frame) + +# # num_objs = 0 +# # predictions = [] +# metadata = dict() +# if contours: +# # Pass contours directly to the detection logic +# # Skip the 'get_bb_mask' and 'morphologyEx' on full frames +# # num_objs, predictions = +# # self.new_get_detections_for_contours_bbs( +# # queue, +# # frame, +# # contours, +# # device_input=device_input, +# # ) + +# metadata = self.new_get_detections_for_contours_bbs( +# frameNum, frame, contours, thickness=2, device_input=device_input +# ) + +# if metadata: +# all_metadata.setdefault( +# self.clip_key, +# { +# "object": {}, +# "face": {}, +# } +# ) +# all_metadata[self.clip_key]["object"].update(metadata) +# # all_metadata[clip_key]["face"].update(metadata_face) +# # return metadata +# # return num_objs, predictions + +# def test_full_cpu_detection_gpu(self, frame, frameNum): +# # Resize directly into the pre-allocated Pinned Memory +# # This avoids a temporary CPU allocation +# H, W = self.resize_h, self.resize_w +# self.cpu_resized_frame = cv2.resize(frame, (W, H)) + +# # if frameNum == 1: +# # # Do the same for CPU if needed (OpenCV does this internally, but seeding helps) +# # for _ in range(self.backSub_cpu.getNSamples()): +# # self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=1.0) + +# # Background Subtraction on CPU +# fgMask = self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=self.lr) + +# # Skip detection/prediction for the first 10-15 frames of a new stream +# # Just update the background, don't run the rest of the pipeline +# # if frameNum < self.fps/2: +# # return 0, [] + +# prev_bkgd = np.ones_like(fgMask) # AND +# for m in self.mask_history: +# # Dilate the historical mask +# dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) +# cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) +# self.mask_history.append(fgMask) + +# if prev_bkgd.max()!=prev_bkgd.min(): +# combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) + +# # Convert the boolean array back to uint8 with 0 and 255 values +# fgMask = combined_mask_bool.astype(np.uint8) * 255 + +# # Thresholding +# _, mask = cv2.threshold( +# fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY +# ) + +# mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + +# # Get Contours & Run Inference on detection_device +# device_input = ( +# self.operation_device_map.detection_device +# if self.operation_device_map.detection_device == "cpu" +# else "cuda" +# ) + +# # num_objs, predictions = +# self.new_contour2predictions(frameNum, mask, frame, device_input=device_input) + +# # method to start thread +# def start(self): +# self.stopped = False +# self.t = [] +# # self.t.append( +# # self.executor.submit( +# # self.get_frames, +# # ) +# # ) +# self.t.append( +# self.executor.submit( +# send_metadata, +# ) +# ) + +# # method to stop reading frames +# def stop(self): +# for t in as_completed(self.t): +# try: +# _ = t.result() +# except Exception as t_e: +# print(f"[DEBUG] Exception occurred in thread: {t_e}") + +# # self.stopped = True +# self.cap.release() diff --git a/fastapi/main.py b/fastapi/main.py new file mode 100644 index 0000000..8d6266c --- /dev/null +++ b/fastapi/main.py @@ -0,0 +1,1132 @@ +import asyncio +import logging +import os +import queue +import shlex +import subprocess +import sys + +# Force FFmpeg to use more threads for decoding +import threading +import time +import traceback +from collections import deque +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime +from pathlib import Path +from random import randint + +import cv2 +import numpy as np +from pydantic import BaseModel +from ultralytics import YOLO +from ultralytics.utils.checks import check_imgsz + +from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse + +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( + "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" +) + + +# from process_stream import extract_metadata_from_results, release_clip_and_reencode, retry_query +from include.utils import ( + UDF_HOST, + UDF_PORT, + YOLO_CLASS_NAMES, + PipelineMapping, + draw_label, + filter_contained_boxes, + get_detection_color, + merge_boxes_limit, + retry_query, +) + +# ----- SETUP LOGGING ----- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +main_app_logger = logging.getLogger("fastapi_app") +uvicorn_logger = logging.getLogger("uvicorn.access") +uvicorn_logger.setLevel(logging.INFO) + + +# ----- SPECIAL VARIABLES ----- +def str2bool(in_val): + if isinstance(in_val, bool): + return in_val + + if not isinstance(in_val, str): + raise ValueError(f"{in_val} is not a bool or string") + + if in_val.title() == "True": + return True + else: + return False + + +CLIP_DURATION = 10 # seconds +CODE_DIR = os.getenv("CODE_DIR", "/home") +CUSTOM_MODEL_FLAG = str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)) +DBHOST = os.getenv("DBHOST", "vdms-service") +DEBUG = os.getenv("DEBUG", "0") +DEBUG_FLAG = True if DEBUG == "1" else False +DETECTION_THRESHOLD = 0.25 +DEVICE = os.getenv("DEVICE", "CPU") +TARGET_FPS = 15 +FRAME_INTERVAL = 1.0 / TARGET_FPS # ~0.0667 seconds +INGESTION = os.getenv("INGESTION", "object,face") +KERNEL_RATIO = 0.05 # 0.03 # .05 # .025 +MASK_MAX_VALUE = 255 +MASK_THRESHOLD_VALUE = 127 +MAX_DETECTIONS = 100 +MAX_FRAMES_PER_CLIP = int(TARGET_FPS * CLIP_DURATION) # 150 frames +MODEL_NAME = os.getenv("MODEL_NAME", "yolo11n") +MODEL_PRECISION = "FP16" +MODEL_W, MODEL_H = (640, 640) +NUM_USUABLE_CPUS = 2 +OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) +SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") +Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) +TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) +TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") + +if CUSTOM_MODEL_FLAG: + model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" +else: + model_path = f"{CODE_DIR}/resources/models/ultralytics/{MODEL_NAME}/{MODEL_PRECISION}/{MODEL_NAME}" + # model_path = f"{CODE_DIR}/{MODEL_NAME}" + +if DEVICE == "GPU": + model_path += ".engine" +else: + model_path += "_openvino_model/" + +# ----- GLOBAL VARIABLES ----- +manager = None # Manager() +local_processes = {} +all_metadata = {} # manager.dict() +send_metadata_queue = queue.Queue() # manager.Queue() + + +# ----- INGESTION FUNCTIONS ----- + + +# Manual FPS calculation if OpenCV reports 0 +def manual_fps_calculation(src, num_frames=10): + vid_obj = cv2.VideoCapture(src) + + frame_count = 0 + start_t = time.time() + + while frame_count < num_frames: + grabbed, frame = vid_obj.read() + + if not grabbed: + break + + frame_count += 1 + + end_t = time.time() + vid_obj.release() + + elapsed_t = end_t - start_t + + if elapsed_t > 0: + return frame_count / elapsed_t + else: + return 0 + + +# Generate and run UDF query +def get_udf_query( + filename_path, + properties, + ingest_mode, + new_size, + id="udf_metadata", + metadata=None, + test_mode=TEST_MODE, +): + query = { + "AddVideo": { + "from_file_path": str(filename_path), # from_server_file + "is_local_file": True, + "properties": properties, + "operations": [ + { + "type": "syncremoteOp", # "remoteOp", + "url": f"http://{UDF_HOST}:{UDF_PORT}/video", + "options": { + "id": id, + "otype": ingest_mode, + "media_type": "video", + "input_sizeWH": new_size, + "filename": properties["Name"], + "ingestion": 1, + }, + } + ], + } + } + + if id == "udf_metadata" and metadata is not None: + query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata + + if test_mode: + return + + filename = str(Path(filename_path).name) + if DEBUG_FLAG: + print( + f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), + flush=True, + ) + try: + res = retry_query([query], sleep_timer=randint(1, 5)) + + if DEBUG_FLAG: + print( + f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), + flush=True, + ) + print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) + print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) + except Exception: + e = traceback.format_exc() + print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) + + +def _sort_dict_by_frame(in_dict): + def _by_int(key): + return tuple(int(k) for k in key.split("_")) + + return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) + + +# method to send metadata to VDMS once clip is saved +def metadata2vdms( + clip_key, + clip_filename, + clip_metadata, + width, + height, +): + if DEBUG == "1": + print( + f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) + + # Send metadata to UDF + properties = { + "Name": clip_key, # .split("/")[-1], + "category": "video_path_rop", + } + + combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} + if "face" in clip_metadata: + for face_frameidx_bbidx, value in clip_metadata["face"].items(): + face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") + max_obj_idx = 0 + for obj_frameidx_bbidx in combined_metadata: + if face_frameidx in obj_frameidx_bbidx: + _, obj_bbidx_ = obj_frameidx_bbidx.split("_") + max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) + + if max_obj_idx > 0: + new_face_bbidx = max_obj_idx + 1 + new_key = f"{face_frameidx}_{new_face_bbidx:04d}" + combined_metadata[new_key] = value + combined_metadata[new_key]["bbId"] = new_key + else: + combined_metadata[face_frameidx_bbidx] = value + + combined_metadata = _sort_dict_by_frame(combined_metadata) + get_udf_query( + clip_filename, + properties, + INGESTION.replace(",", "+"), + (width, height), + id="udf_metadata", + metadata=combined_metadata, + test_mode=TEST_MODE, + ) + + if DEBUG == "1": + print( + f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) + + +# method to create clips (read frame write to file; add name to list) +def send_metadata(): + global all_metadata + clip_filename = "" + clip_key = "" + width = 0 + height = 0 + while True: + try: + queue_details = send_metadata_queue.get() + if queue_details is None: + break + + (clip_key, clip_filename, width, height) = queue_details + + metadata2vdms( + clip_key, + clip_filename, + all_metadata[clip_key], + width, + height, + ) + del all_metadata[clip_key] + + except queue.Empty: + pass + + +# --------------- APP ------------------- +from contextlib import asynccontextmanager + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # This is the ONLY place this should be initialized + if not hasattr(app.state, "active_streams"): + app.state.active_streams = {} + print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") + yield + # Cleanup logic here... + + +app = FastAPI(lifespan=lifespan) + + +def save_and_finalize_clip( + clip_key, + _out_vid, + clip_filename, + tmp_file, + target_fps, + frame_width, + frame_height, +): + if DEBUG == "1": + print( + f"[TIMING],start_release_clip,{clip_key},{time.time()}", + flush=True, + ) + _out_vid.release() + if DEBUG == "1": + print( + f"[TIMING],end_release_clip,{clip_key},{time.time()}", + flush=True, + ) + + # Re-encode video in order to seek via ffmpeg later + GENERAL_OPTS = "-flags -global_header -hide_banner -loglevel error -nostats -tune zerolatency -flush_packets 0" # -filter:v fps={target_fps} + CONVERSION = f"-c:v libx264 -preset ultrafast -filter:v fps=fps={target_fps}" # "-c:v libx264 -preset medium" + reencode_cmd = f"ffmpeg -y -i {tmp_file} {GENERAL_OPTS} {CONVERSION} -crf 23 -c:a copy {clip_filename}" + cmd_list = shlex.split(reencode_cmd) + if DEBUG == "1": + print( + f"[TIMING],start_reencode,{clip_key},{time.time()}", + flush=True, + ) + subprocess.run(cmd_list, check=True) + end_time = time.time() + # filename = str(Path(clip_filename).name) + if DEBUG == "1": + print( + f"[TIMING],end_reencode,{clip_key},{end_time}", + flush=True, + ) + print(f"[TIMING],Save clip,{clip_key},{end_time}", flush=True) + os.remove(tmp_file) + + send_metadata_queue.put( + ( + clip_key, + clip_filename, + frame_width, + frame_height, + ) + ) + + +class VideoStreamHandler: + def __init__(self, source, name): + self.model = YOLO(model_path, verbose=False, task="detect") + self.source = source + self.name = name + # self.cap = cv2.VideoCapture(source) + # Use CAP_FFMPEG and increase internal buffer for RTSP + self.cap = cv2.VideoCapture(source, cv2.CAP_FFMPEG) + # Force the hardware buffer to 1 so we don't lag + # self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + # Set a timeout so it doesn't hang + self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) + + self.video_writer = None + self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") + self.clip_id = 0 + self.clip_filename = "" + self.clip_key = "" + self.tmp_file = "" + + self.active = True + self.frame = None + self.latest_processed_frame = None + self.last_write_time = time.time() + + self.resize_h, self.resize_w = [MODEL_H, MODEL_W] + self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + self.get_fps_and_framecnt() + + self.get_frameWH() + + self.scale_x = self.frame_width / MODEL_W + self.scale_y = self.frame_height / MODEL_H + self.min_contour_area = int( + (0.005 * self.frame_width) * (0.005 * self.frame_height) + ) # 207 + + self.operation_device_map = PipelineMapping( + detection_device="cpu" + ) # No CUDA HERE + self.device_input = ( + self.operation_device_map.detection_device + if self.operation_device_map.detection_device == "cpu" + else "cuda" + ) + + self.cpu_resized_frame = None + + # Subtraction + history = 300 # int(5 * self.fps) + background_thresh = 350 + NSamples = 10 + kNNSamples = 2 + self.lr = ( + -1 + ) # .01 #-1 # 0.001 #1 / (5 * self.fps) # -1 # 0.01 # 1 / history + bkgd_mask_queue_size = 3 + self.backSub_cpu = cv2.createBackgroundSubtractorKNN( + history=history, # default 500 + dist2Threshold=background_thresh, # default 400 + detectShadows=False, # default True + ) + self.backSub_cpu.setkNNSamples(kNNSamples) + self.backSub_cpu.setNSamples(NSamples) + + prev_bkgd = np.zeros((MODEL_H, MODEL_W), dtype="uint8") + self.mask_history = deque(maxlen=bkgd_mask_queue_size) + self.mask_history.append(prev_bkgd) + + self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) + self.dilate_kernel_for_enhanced_mask = np.ones((21, 21), np.uint8) + + # Create ThreadPoolExecutor + self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) + + self.thread = threading.Thread(target=self.update, daemon=True) + + self.process_thread = threading.Thread(target=self.run_inference, daemon=True) + self.start() + + # Gets video fps and framecount + def get_fps_and_framecnt(self): + self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps + if self.input_fps == 0: # Case when FPs isn't available + self.input_fps = manual_fps_calculation(self.stream_id, num_frames=10) + + self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps + self.frame_skip = int(self.input_fps / self.target_fps) + if self.frame_skip < 1: + self.frame_skip = 1 + + print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) + print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) + + # Frame count for videos + self.frame_count = None + if "://" not in str(self.source): + self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + # Gets frame W and H details + def get_frameWH(self): + input_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + input_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + if (input_height * input_width) < (MODEL_H * MODEL_W): + new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw + else: + new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw + + new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) + + self.width = new_sizeWH[0] + self.height = new_sizeWH[1] + + # def run_inference(self): + # # # streamer = VideoStreamHandler(source_url, name) + + # # # Skip frames if they aren't fresh + # last_frame_time = time.time() + # frame_counter = 0 + # print(f"Inference thread for {self.name} started...") + + # while self.active: + # current_time = time.time() + # # Only process if 66ms has passed + # if current_time - last_frame_time < FRAME_INTERVAL: + # time.sleep(0.001) + # continue + + # # 1. Get frame from the update() thread + # frame = self.get_frame() + # if frame is None: + # time.sleep(0.01) + # continue + + # # if success: + # try: + # frame_bytes = self.test_full_cpu_detection_gpu(frame, frame_counter + 1) + # if frame_bytes is not None: + # frame_counter += 1 + # self.latest_processed_frame = frame_bytes + # else: + # print("DEBUG: test_full_cpu_detection_gpu returned None") + + # # Reset frame to signal we are ready for the next one + # last_frame_time = current_time + # self.frame = None + + # except Exception as e: + # print(f"Inference Error: {e}") + + # time.sleep(0.001) + + # def run_inference(self): + # frames_written = 0 + # frame_counter = 0 + # print(f"Inference thread for {self.name} started...") + + # last_frame_time = time.time() + # while self.active: + # # elapsed_t = time.time() - last_frame_time + # # expected_frames = int(elapsed_t * TARGET_FPS) + + # # Only process if 66ms has passed + # if frame_counter + 1 > frames_written: + # print(f"expected_frames > frames_written: {frame_counter + 1} > {frames_written}") + # frame = self.get_frame() + # if frame is None: + # time.sleep(0.01) + # continue + + # try: + # frameNum = frame_counter + 1 + # frame_bytes = self.test_full_cpu_detection_gpu(frame, frameNum ) + # frames_written = frameNum + # self.latest_processed_frame = frame_bytes + # self.frame = None + # frame_counter += 1 + + # except Exception as e: + # print(f"Inference Error: {e}") + + # time.sleep(0.001) + + # def run_inference(self): + # start_time = time.time() + # frames_accounted_for = 0 + + # while self.active: + # current_time = time.time() + # elapsed_real_time = current_time - start_time + # expected_total_frames = int(elapsed_real_time * self.target_fps) + + # # How many 15fps 'slots' have passed since we last processed? + # # If processing took 133ms, this will be 2. + # frames_to_write = expected_total_frames - frames_accounted_for + + # if frames_to_write > 0: + # frame = self.get_frame() + # if frame is None: + # time.sleep(0.01) + # continue + + # try: + # # PASS THE REPEAT COUNT to your function + # frame_bytes = self.test_full_cpu_detection_gpu( + # frame, + # expected_total_frames, + # repeat_count=frames_to_write + # ) + + # frames_accounted_for = expected_total_frames + # self.latest_processed_frame = frame_bytes + # self.frame = None + # except Exception as e: + # print(f"Inference Error: {e}") + # else: + # time.sleep(0.005) # Wait for the next 66ms slot + + def run_inference(self): + print(f"Inference thread for {self.name} started...") + + # 1. Initialize the start time and a counter for frames actually written + start_time = time.time() + total_frames_written = 0 + target_fps = 15.0 # This must match your VideoWriter TARGET_FPS + + while self.active: + # 2. Calculate how many frames SHOULD be in the file by now + elapsed_real_time = time.time() - start_time + expected_total_frames = int(elapsed_real_time * target_fps) + + # 3. Determine the "Gap": How many frames do we need to write to stay in sync? + # If processing took 200ms, slots_to_fill will be ~3. + slots_to_fill = expected_total_frames - total_frames_written + + if slots_to_fill > 0: + frame = self.get_frame() + if frame is None: + time.sleep(0.01) + continue + + try: + # 4. PASS THE REPEAT COUNT to your function + # This ensures the video length matches the stopwatch + self.test_full_cpu_detection_gpu( + frame, expected_total_frames, repeat_count=slots_to_fill + ) + + # 5. Sync the counter + total_frames_written = expected_total_frames + self.frame = None + except Exception as e: + print(f"Inference Error: {e}") + else: + # 6. We are ahead of the clock; wait for the next 66ms window + time.sleep(0.005) + + # def update(self): + # while self.active: + # if not self.cap.isOpened(): + # print(f"[ERROR] Camera {self.name} lost connection.") + # self.active = False + # break + + # # # 1. Check if the frame was successfully grabbed + # # grabbed = self.cap.grab() + # # if not grabbed: + # # print(f"[INFO] Stream ended or disconnected: {self.name}") + # # self.active = False # <--- TRIGGER SHUTDOWN + # # break + + # # # 2. Only retrieve if main loop is ready + # # if self.frame is None: + # # success, frame = self.cap.retrieve() + # # if not success: + # # self.active = False + # # break + # # self.frame = frame + + # # time.sleep(0.001) + # success, frame = self.cap.read() + # if success: + # self.frame = frame + # # print("READER THREAD: Frame captured") # Silent once working + # else: + # # print("READER THREAD: Failed to read from file!") + # # time.sleep(1) + # self.active = False + # break + + # # 3. Clean up hardware handles automatically + # # self.stop() + + # def update(self): + # print(f"READER THREAD: Started for {self.name}") + # while self.active: + # if not self.cap.isOpened(): + # self.active = False + # break + + # # 1. Calculate how many frames to SKIP + # # We want to skip 'self.frame_skip - 1' frames + # for _ in range(self.frame_skip - 1): + # # grab() is 5x faster than read() because it doesn't decode + # if not self.cap.grab(): + # self.active = False + # return + + # # 2. Only decode (read/retrieve) the ONE frame we actually want + # success, frame = self.cap.read() + + # if not success: + # print(f"READER THREAD: {self.name} reached end of file.") + # self.active = False + # break + + # # 3. Hand the decoded frame to the inference thread + # # No 'if skip_frame_num' logic needed here anymore + # self.frame = frame + + # # 4. Tiny sleep to prevent this thread from starving the YOLO thread + # time.sleep(0.001) + + def update(self): + # Calculate exactly how much time should pass between 15fps frames + target_interval = 1.0 / self.target_fps # 0.0666s + last_grab_time = time.time() + + while self.active: + # 1. Fast-forward the internal buffer using grab() + # This clears out the 21fps 'junk' frames + success = self.cap.grab() + if not success: + self.active = False + break + + # 2. Check if enough time has passed to 'retrieve' a 15fps frame + current_time = time.time() + if current_time - last_grab_time >= target_interval: + success, frame = self.cap.retrieve() + if success: + self.frame = frame + last_grab_time = current_time + + time.sleep(0.001) + + def get_frame(self): + return self.frame + + def new_get_detections_for_contours_bbs( + self, frameNum, foi, contours, thickness=2, device_input="cuda" + ): + # global active_streams + # source = self.source + stream_name = self.name + num_objs = 0 + # predictions = [] + metadata = dict() + # frame_bytes = 'b' + cropped_imgs, cropped_coords = [], [] + H, W = foi.shape[:2] # Unpack once + bbs_full_res = [] + + # Filter and Sort in one go (Minimize Python-to-C++ crossings) + raw_bbs = [] + padding = 64 + for c in contours: + area = cv2.contourArea(c) + x1, y1, w, h = cv2.boundingRect(c) + if ( + area > self.min_contour_area + ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect + xx1 = max(0, int((x1 * self.scale_x)) - padding) + yy1 = max(0, int((y1 * self.scale_y)) - padding) + xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) + yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) + raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) + bbs_full_res = sorted( + [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], + key=lambda x: x[0], + reverse=True, + )[:MAX_DETECTIONS] + + dist_thresh = min(0.05 * W, 0.05 * H) + merged = merge_boxes_limit( + bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + ) + + merged = filter_contained_boxes(merged, containment_thresh=0.9) + + # for cnt, area in merged: + for x1, y1, x2, y2 in merged: + if ( + x2 > x1 + and y2 > y1 + and (x2 - x1) < self.frame_width + and (y2 - y1) < self.frame_height + ): + crop = foi[y1:y2, x1:x2] + if crop.size > 0: + cropped_imgs.append(crop) + cropped_coords.append((x1, y1)) + + if not cropped_imgs: + if self.frame_width > 1280: + display_frame = cv2.resize( + foi, (1280, 720), interpolation=cv2.INTER_AREA + ) + _, buffer = cv2.imencode( + ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 50] + ) + else: + _, buffer = cv2.imencode( + ".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), 50] + ) + frame_bytes = buffer.tobytes() + return metadata, frame_bytes # num_objs, predictions + + # 2. Inference (Keep stream=False as it is stable) + results = self.model.predict( + cropped_imgs, + imgsz=MODEL_W, + batch=len(cropped_imgs), + device=device_input, + verbose=False, + stream=True, + max_det=MAX_DETECTIONS, + # classes=[0], # only "person", + # conf=0.45, + ) + + label_source = ( + self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES + ) + + for ridx, r in enumerate(results): + if r.boxes is None or len(r.boxes) == 0: + continue + + # Move to CPU in one bulk operation per crop + boxes = r.boxes.xyxy.cpu().numpy().astype(int) + clss = r.boxes.cls.cpu().numpy().astype(int) + confs = r.boxes.conf.cpu().numpy() + off_x, off_y = cropped_coords[ridx] + + for j in range(len(boxes)): + num_objs += 1 + bx1, by1, bx2, by2 = boxes[j] + abs_x1, abs_y1 = off_x + bx1, off_y + by1 + abs_x2, abs_y2 = off_x + bx2, off_y + by2 + class_id = clss[j] + class_name = label_source[class_id] + confidence = confs[j] + if confidence > DETECTION_THRESHOLD: + if not OMIT_DETECTIONS_FLAG: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + print( + # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", + f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", + flush=True, + ) + + bb_color = get_detection_color(class_id, is_bgr=True) + + cv2.rectangle( + foi, + (abs_x1, abs_y1), + (abs_x2, abs_y2), + bb_color, + thickness, + ) + label = f"{class_name} {confidence:.2f}" + draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) + + height = min(abs_y2, H) - max(0, abs_y1) + width = min(abs_x2, W) - max(0, abs_x1) + # object_res = [ + # abs_x1, + # abs_y1, + # height, + # width, + # class_name, + # confidence, + # H, + # W, + # ] + + # Resized + scale_x = self.resize_w / W + scale_y = self.resize_h / H + object_res = [ + int(abs_x1 * scale_x), + int(abs_y1 * scale_y), + int(height * scale_y), + int(width * scale_x), + class_name, + confidence, + int(self.resize_h), + int(self.resize_w), + ] + + framenum_str = f"{frameNum:04d}_{j:04d}" + if DEBUG_FLAG: + meta_str = ",".join( + [str(o) for o in object_res + [framenum_str]] + ) + print(f"[{stream_name} METADATA],{meta_str}", flush=True) + + # Full Res + metadata[framenum_str] = { + "frameId": frameNum, + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(object_res[6]), + "frameW": int(object_res[7]), + }, + }, + } + + # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) + if self.frame_width > 1280: + display_frame = cv2.resize(foi, (1280, 720), interpolation=cv2.INTER_AREA) + ret, buffer = cv2.imencode( + ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 50] + ) + else: + ret, buffer = cv2.imencode(".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), 50]) + if ret: + frame_bytes = buffer.tobytes() + else: + frame_bytes = None + + return metadata, frame_bytes + + def release_clip_and_reencode(self): + if self.video_writer is not None: + threading.Thread( + target=save_and_finalize_clip, + args=( + self.clip_key, + self.video_writer, + self.clip_filename, + self.tmp_file, + self.target_fps, + MODEL_W, + MODEL_H, + # self.frame_width, + # self.frame_height, + ), + daemon=True, + ).start() + + self.video_writer = None + self.clip_id += 1 + + def new_contour2predictions( + self, frameNum, mask, frame, device_input="cpu", repeat_count=1 + ): + source = self.source + stream_name = self.name + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Handle Video Writing (Cycle every 10 seconds) + clip_frameNum = (frameNum - 1) % MAX_FRAMES_PER_CLIP + if clip_frameNum == 0: + print(f"frameNum: {frameNum} ({clip_frameNum})") + if self.video_writer: + self.release_clip_and_reencode() + if "://" not in str(source): + self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{self.clip_id}.mp4" + else: + self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{time.time()}.mp4" + + self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] + self.clip_key = Path(self.clip_filename).name + + # timestamp = int(time.time()) + # filename = f"clip_{timestamp}.mp4" + self.video_writer = cv2.VideoWriter( + self.tmp_file, + self.fourcc, + self.target_fps, + (MODEL_W, MODEL_H), + # (self.width, self.height) + # (self.frame_width, self.frame_height), + ) + main_app_logger.info(f"Started new clip: {self.tmp_file}") + + # 3. Write frame + # self.video_writer.write(frame) + if self.video_writer: + for _ in range(repeat_count): + self.video_writer.write(self.cpu_resized_frame) + + # num_objs = 0 + # predictions = [] + metadata = dict() + if contours: + metadata, frame_bytes = self.new_get_detections_for_contours_bbs( + frameNum, frame, contours, thickness=2, device_input=device_input + ) + + if metadata: + all_metadata.setdefault( + self.clip_key, + { + "object": {}, + "face": {}, + }, + ) + all_metadata[self.clip_key]["object"].update(metadata) + # all_metadata[clip_key]["face"].update(metadata_face) + return frame_bytes + + def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + H, W = self.resize_h, self.resize_w + self.cpu_resized_frame = cv2.resize(frame, (W, H)) + + # Background Subtraction on CPU + fgMask = self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=self.lr) + + prev_bkgd = np.ones_like(fgMask) # AND + for m in self.mask_history: + # Dilate the historical mask + dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) + cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) + self.mask_history.append(fgMask) + + if prev_bkgd.max() != prev_bkgd.min(): + combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) + + # Convert the boolean array back to uint8 with 0 and 255 values + fgMask = combined_mask_bool.astype(np.uint8) * 255 + + # Thresholding + _, mask = cv2.threshold( + fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY + ) + + mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + + # Get Contours & Run Inference on detection_device + device_input = ( + self.operation_device_map.detection_device + if self.operation_device_map.detection_device == "cpu" + else "cuda" + ) + + # num_objs, predictions = + frame_bytes = self.new_contour2predictions( + frameNum, mask, frame, device_input=device_input, repeat_count=repeat_count + ) + return frame_bytes + + def start(self): + self.t = [] + self.t.append( + self.executor.submit( + send_metadata, + ) + ) + self.thread.start() + self.process_thread.start() + + def stop(self): + self.active = False + for t in as_completed(self.t): + try: + _ = t.result() + except Exception as t_e: + print(f"[DEBUG] Exception occurred in thread: {t_e}") + + self.cap.release() + + +class StreamRequest(BaseModel): + url: str + name: str + + +@app.post("/stream") +async def stream_video( + # url: str = Query(..., description="RTSP URL or Local File Path"), + # name: str = Query(..., description="Name of stream"), + data: StreamRequest, +): + url, name = data.url, data.name + # Start background thread + if name not in app.state.active_streams: + print(f"Starting background worker for {name}...") + app.state.active_streams[name] = VideoStreamHandler(url, name) + # DEBUG START + curr_keys = list(app.state.active_streams.keys()) + print( + f"stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" + ) + # DEBUG END + + return {"status": "started", "keys": list(app.state.active_streams.keys())} + + +@app.get("/view_stream") +async def view_stream(name: str): + # DEBUG START + curr_keys = list(app.state.active_streams.keys()) + print( + f"view_stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" + ) + # DEBUG END + # This will now find 'test_vid' because it's the same memory! + streamer = app.state.active_streams.get(name) + + if not streamer: + raise HTTPException(status_code=404, detail="Stream not found") + + async def get_frames(): + while streamer.active: + frame_bytes = streamer.latest_processed_frame + if frame_bytes is None: + print(f"DEBUG: {streamer.name} frame is still None...") + await asyncio.sleep(0.1) # Wait for first inference to finish + continue + + yield ( + b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n" + ) + await asyncio.sleep(0.06) + + return StreamingResponse( + get_frames(), media_type="multipart/x-mixed-replace; boundary=frame" + ) + + +@app.get("/debug_frame/{name}") +async def debug_frame(name: str): + streamer = app.state.active_streams.get(name) + if not streamer: + return {"error": "not found"} + # DEBUG START + curr_keys = list(app.state.active_streams.keys()) + print( + f"debug_frame DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" + ) + # DEBUG END + return { + "active": streamer.active, + "has_frame": streamer.latest_processed_frame is not None, + "frame_size": len(streamer.latest_processed_frame) + if streamer.latest_processed_frame + else 0, + } + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") diff --git a/fastapi/nginx.conf b/fastapi/nginx.conf new file mode 100644 index 0000000..333185c --- /dev/null +++ b/fastapi/nginx.conf @@ -0,0 +1,36 @@ +events { worker_connections 1024; } + +http { + include mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name _; + + location / { + # Proxy to the Uvicorn server running in the same container + proxy_pass http://fastapi-service:8000; + + proxy_http_version 1.1; + proxy_set_header Connection ""; # Keep connection open + + # Absolute zero buffering + proxy_buffering off; # DISBALE Nginx buffering + proxy_request_buffering off; + proxy_cache off; # Ensure no caching of the video + tcp_nodelay on; + tcp_nopush off; + + # Pass original headers for FastAPI's identification + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Increase timeouts for long-running video streams + proxy_read_timeout 3600s; # Keep stream open for up to 1 hour + proxy_send_timeout 3600s; + } + } +} \ No newline at end of file diff --git a/fastapi/requirements.GPU.txt b/fastapi/requirements.GPU.txt new file mode 100644 index 0000000..113f8d9 --- /dev/null +++ b/fastapi/requirements.GPU.txt @@ -0,0 +1,6 @@ +cuda-python #==12.2.0 +onnx>=1.12.0,<=1.19.1 +onnxruntime-gpu +onnxslim>=0.1.71 +tensorrt==10.9.0.34 #>=10.12.0.36 +# cupy==14.0.1 diff --git a/fastapi/requirements.txt b/fastapi/requirements.txt new file mode 100644 index 0000000..39ab158 --- /dev/null +++ b/fastapi/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +opencv-python-headless #==4.12.0.88 # Newer versions (4.13.x) have packaging dependency on XCB libraries +openvino-dev==2024.6.0 +ultralytics +numpy +vdms \ No newline at end of file diff --git a/fastapi/shell.sh b/fastapi/shell.sh new file mode 100755 index 0000000..9d60aaa --- /dev/null +++ b/fastapi/shell.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +IMAGE="lcc_fastapi" +DIR=$(dirname $(readlink -f "$0")) + +. "$DIR/../script/shell.sh" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 98427f2..27b11ec 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,19 +3,22 @@ FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063 ARG DEBUG="0" ENV DEBUG="${DEBUG}" +ARG DEBIAN_FRONTEND=noninteractive # Prevent Python from writing .pyc files and enable unbuffered logging ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 # fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 -RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ +RUN apt-get update +RUN apt-get install --only-upgrade libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-pip && \ rm -rf /var/lib/apt/lists/* -COPY requirements.txt /home/requirements.txt -RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt +# COPY requirements.txt /home/requirements.txt +# RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt +COPY requirements.in /home/requirements.in +RUN pip3 install --no-cache-dir -r /home/requirements.in COPY *.py /home/ COPY *.conf /etc/nginx/ diff --git a/frontend/requirements.txt b/frontend/requirements.txt index 69949ba..c3cbdb3 100644 --- a/frontend/requirements.txt +++ b/frontend/requirements.txt @@ -123,12 +123,10 @@ charset-normalizer==3.4.4 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via - # -r frontend/requirements.in - # requests +idna==2.10 \ + --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 + # via requests ply==3.11 \ --hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \ --hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce @@ -181,8 +179,8 @@ psutil==5.9.0 \ --hash=sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3 # via -r frontend/requirements.in requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ + --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e # via -r frontend/requirements.in tornado==6.5 \ --hash=sha256:007f036f7b661e899bd9ef3fa5f87eb2cb4d1b2e7d67368e778e140a2f101a7a \ @@ -198,12 +196,10 @@ tornado==6.5 \ --hash=sha256:f81067dad2e4443b015368b24e802d0083fecada4f0a4572fdb72fc06e54a9a6 \ --hash=sha256:fd20c816e31be1bbff1f7681f970bbbd0bb241c364220140228ba24242bcdc59 # via -r frontend/requirements.in -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via - # -r frontend/requirements.in - # requests +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 + # via requests vdms==0.0.22 \ --hash=sha256:4d59fedd914a645fb8a42c504c9535131f9de9a435b5add00900a2abade50036 \ --hash=sha256:a1ca7fb79f81526ccf5cc9b5066bbbaa513a9b258002e40b4b93765b4739818a diff --git a/video/Dockerfile b/video/Dockerfile index d64aba7..77ee34a 100644 --- a/video/Dockerfile +++ b/video/Dockerfile @@ -1,19 +1,27 @@ -FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a as build +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a ENV VIRTUAL_ENV=/opt/venv ARG DEBIAN_FRONTEND=noninteractive -# fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 -RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get update +RUN apt-get install --only-upgrade libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-setuptools \ - python3-tornado python3-ply python3-pip python3-venv \ - && rm -rf /var/lib/apt/lists/* -RUN python3 -m venv ${VIRTUAL_ENV} + python3-tornado curl libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* + +COPY --from=lcc_base_video_image:latest ${VIRTUAL_ENV} ${VIRTUAL_ENV} +COPY --from=lcc_base_video_image:latest /home /home + +# activate virtual environment ENV PATH="$VIRTUAL_ENV/bin:$PATH" -COPY resources /home/resources +# Set the working directory in the container +WORKDIR /home + COPY requirements.* /home/ ARG DEVICE="CPU" @@ -24,37 +32,19 @@ RUN if [ "${DEVICE}" = "CPU" ]; then \ else \ pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ fi; -RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ - pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt - -RUN omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 +# RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ +# pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt +RUN pip3 install --no-cache-dir -r /home/requirements.in && \ + pip3 install --no-cache-dir -r /home/requirements.${DEVICE}.txt -# FINAL IMAGE -FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a -ENV VIRTUAL_ENV=/opt/venv -ARG DEBIAN_FRONTEND=noninteractive - -# Prevent Python from writing .pyc files and enable unbuffered logging -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ - apt-get install -y -q --no-install-recommends python3-tornado \ - curl libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -# COPY --from=build /usr/local/lib/python3.10 /usr/local/lib/python3.10 -COPY --from=build ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --from=build /home /home - - -WORKDIR /home COPY *.py /home/ +COPY *.yaml /home/ +COPY *.conf /etc/nginx/ COPY cleanup.sh /home/ COPY manage.sh /home/ -COPY *.conf /etc/nginx/ -CMD ["/bin/bash","-c","python3 /home/resources/models/download_yolo.py && /home/manage.sh"] +COPY frontend /home/frontend +# COPY include /home/include +CMD ["/bin/bash","-c","python /home/resources/models/download_yolo.py && /home/manage.sh"] #### ARG USER @@ -67,7 +57,7 @@ RUN if [ ${GID} -gt 0 ]; then groupadd -f -g ${GID} ${GROUP}; fi; \ if [ ${UID} -gt 0 ]; then useradd --no-log-init -d /home/${USER} -g ${GID} -K UID_MAX=${UID} -K UID_MIN=${UID} ${USER}; fi; \ touch /var/run/nginx.pid && \ mkdir -p /var/log/nginx /var/lib/nginx /var/www/cache /var/www/gen /var/www/mp4 && \ - chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home/resources + chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home VOLUME ["/var/www"] USER ${UID} #### diff --git a/video/Dockerfile.base b/video/Dockerfile.base new file mode 100644 index 0000000..1536390 --- /dev/null +++ b/video/Dockerfile.base @@ -0,0 +1,34 @@ + +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a as build + +ENV VIRTUAL_ENV=/opt/venv +ARG DEBIAN_FRONTEND=noninteractive + +# fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 +RUN apt-get update +RUN apt-get install --only-upgrade libc-bin libc6 && \ + apt-get install -y -q --no-install-recommends python3-setuptools \ + python3-tornado python3-ply python3-pip python3-venv \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m venv ${VIRTUAL_ENV} +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +COPY resources /home/resources +COPY requirements.* /home/ + +ARG DEVICE="CPU" +ENV DEVICE="${DEVICE}" + +RUN if [ "${DEVICE}" = "CPU" ]; then \ + pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ + else \ + pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ + fi; +# RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ +# pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt +RUN pip3 install --no-cache-dir -r /home/requirements.in && \ + pip3 install --no-cache-dir -r /home/requirements.${DEVICE}.txt + +# RUN pip3 install --no-cache-dir openvino-dev>=2024.6.0 +RUN omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 diff --git a/video/manage.sh b/video/manage.sh index ac4247d..59f0c02 100755 --- a/video/manage.sh +++ b/video/manage.sh @@ -1,7 +1,8 @@ #!/bin/bash -e # Watch directory +echo "WATCH_DIR: ${WATCH_DIR}" python3 /home/watch_and_send2vdms.py ${WATCH_DIR} & # run tornado -exec /home/manage.py +exec ${VIRTUAL_ENV}/bin/python /home/manage.py diff --git a/video/nginx.conf b/video/nginx.conf index bfc5853..c9150d3 100644 --- a/video/nginx.conf +++ b/video/nginx.conf @@ -8,6 +8,19 @@ events { } http { + + # Include MIME types + include mime.types; + default_type application/octet-stream; + + # Basic logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + # `proxy_read_timeout` maximum time Nginx will wait for a response from a proxied server. # If the proxied server does not send any data within this time, Nginx will close the connection. # Default: 60 @@ -22,15 +35,16 @@ http { # Default: 60 # proxy_connect_timeout 300s; - include mime.types; - default_type application/octet-stream; + # include mime.types; + # default_type application/octet-stream; server { listen 8080; server_name _; # client_body_timeout 10s; # client_header_timeout 10s; - client_max_body_size 1024M; + # client_max_body_size 1024M; + client_max_body_size 2G; # 8K frames are huge sendfile on; location /api/ { @@ -46,5 +60,21 @@ http { internal; root /var/www; } + + location / { + # proxy_http_version 1.1; + + # Mandatory for WebSocket support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + } } } + diff --git a/video/process_stream.py b/video/process_stream.py deleted file mode 100644 index eca85bf..0000000 --- a/video/process_stream.py +++ /dev/null @@ -1,988 +0,0 @@ -import gc -import multiprocessing as mp -import os -import queue -import shlex -import subprocess -import sys -import time # time library -import traceback -from concurrent.futures import ThreadPoolExecutor, as_completed -from datetime import datetime -from pathlib import Path -from random import randint - -import cv2 # OpenCV library -from openvino.runtime import Core -from ultralytics import YOLO -from ultralytics.utils.checks import check_imgsz -from utils import str2bool - -import vdms - - -def _sort_dict_by_frame(in_dict): - def _by_int(key): - return tuple(int(k) for k in key.split("_")) - - return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) - - -""" GENERAL VARIABLES """ -CODE_DIR = os.getenv("CODE_DIR", "/home") -CUSTOM_MODEL_FLAG = str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)) -DBHOST = os.getenv("DBHOST", "vdms-service") -DEBUG = os.getenv("DEBUG", "0") -DEVICE = os.getenv("DEVICE", "CPU") -INGESTION = os.getenv("INGESTION", "object,face") -RESIZE_FLAG = str2bool(os.getenv("RESIZE_FLAG", False)) -OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) -SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") -Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) -TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) -TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") -UDF_HOST = os.getenv("UDF_HOST", "video-service") -MODEL_NAME = os.getenv("MODEL_NAME", "yolo11n") - -LOCKTIMEOUT_RETRIES = 5 -ERR_KEYWORDS = [ - "timeout", - "null search iterator", - "outoftransactions", - "internal server", -] - -BATCH_SIZE = 1 -DBPORT = 55555 -DEBUG_FLAG = True if DEBUG == "1" else False -DETECTION_THRESHOLD = 0.25 -DEVICE_OV = "AUTO" -HALF_FLAG = True -IOU_THRESHOLD = 0.7 -MODEL_PRECISION = "FP16" -MODEL_W, MODEL_H = (640, 640) -TARGET_FPS = 15 -UDF_PORT = 5011 -WRITER_FOURCC = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v -NUM_USUABLE_CPUS = 2 # os.cpu_count() - -if CUSTOM_MODEL_FLAG: - model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" -else: - model_path = f"{CODE_DIR}/resources/models/ultralytics/{MODEL_NAME}/{MODEL_PRECISION}/{MODEL_NAME}" - -if not TEST_MODE: - db = vdms.vdms() - db.connect(DBHOST, DBPORT) - -if DEVICE == "GPU": - model_path += ".engine" - batch_size = int(os.environ.get("GPU_BATCH_SIZE", 1)) - os.environ["CUDA_VISIBLE_DEVICES"] = "0" - from torch.cuda import empty_cache -else: - model_path += "_openvino_model/" - batch_size = int(os.environ.get("CPU_BATCH_SIZE", 1)) # 8 - -device_input = DEVICE.lower() if DEVICE == "CPU" else "cuda" - -all_metadata = {} -send_metadata_queue = mp.Queue() - - -def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): - global db - for ridx in range(num_retries + 1): - response, _ = db.query(query, [[]]) - if "FailedCommand" in response[0] and any( - k in response[0]["info"].lower() for k in ERR_KEYWORDS - ): - err = response[0]["info"] - if DEBUG == "1": - query_type = list(query[0].keys())[0] - print( - f"DEBUG [process_stream Attempt #{ridx}] Received '{err}' for {query_type} query", - flush=True, - ) - if sleep_timer > 0: - time.sleep(sleep_timer) - else: - if DEBUG == "1": - print( - f"[DEBUG process_stream] Successful query response: {response}", - flush=True, - ) - break # Continue - return response - - -def load_models(): - # OBJECT DETECTION - model = YOLO(model_path, verbose=False, task="detect") - - # FACE, AGE, GENDER, AND EMOTIONS - ie = Core() - face_detection_model_xml = f"{CODE_DIR}/resources/models/intel/face-detection-adas-0001/{MODEL_PRECISION}/face-detection-adas-0001.xml" - face_detection_model = ie.read_model( - model=face_detection_model_xml, - weights=face_detection_model_xml.replace(".xml", ".bin"), - ) - # face_det_w, face_det_h = 672, 384 - _, face_det_c, face_det_h, face_det_w = face_detection_model.inputs[0].shape - face_det_compiled_model = ie.compile_model(face_detection_model, DEVICE_OV) - - age_gender_classification_model_xml = f"{CODE_DIR}/resources/models/intel/age-gender-recognition-retail-0013/{MODEL_PRECISION}/age-gender-recognition-retail-0013.xml" - age_gender_classification_model = ie.read_model( - model=age_gender_classification_model_xml, - weights=age_gender_classification_model_xml.replace(".xml", ".bin"), - ) - # ag_w, ag_h = 62, 62 - _, ag_c, ag_h, ag_w = age_gender_classification_model.inputs[0].shape - ag_compiled_model = ie.compile_model(age_gender_classification_model, DEVICE_OV) - - emotions_classification_model_xml = f"{CODE_DIR}/resources/models/intel/emotions-recognition-retail-0003/{MODEL_PRECISION}/emotions-recognition-retail-0003.xml" - emotions_classification_model = ie.read_model( - model=emotions_classification_model_xml, - weights=emotions_classification_model_xml.replace(".xml", ".bin"), - ) - # em_w, em_h = 64, 64 - _, em_c, em_h, em_w = emotions_classification_model.inputs[0].shape - em_compiled_model = ie.compile_model(emotions_classification_model, DEVICE_OV) - - return ( - model, - [face_det_compiled_model, ag_compiled_model, em_compiled_model], - [face_det_c, face_det_h, face_det_w], - [ag_c, ag_h, ag_w], - [em_c, em_h, em_w], - ) - - -model, face_models, face_det_CHW, ag_CHW, em_CHW = load_models() - - -""" DETECTION FUNCTIONS """ - - -# Extract metadata from object model results -def extract_metadata_from_results( - stream_name, frameNum, results, img_size, fps=TARGET_FPS -): - fW, fH = img_size - metadata = dict() - try: - for _, result in enumerate(results): - # GET METADATA FOR CLIP - boxes = result.boxes.cpu() - oidx = 0 - for box in boxes: - confidence = float(box.conf.item()) - if confidence > DETECTION_THRESHOLD: - class_id = int(box.cls.item()) - class_name = str(result.names[class_id]) - - if not OMIT_DETECTIONS_FLAG: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - print( - # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", - f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", - flush=True, - ) - x1, y1, x2, y2 = box.xyxy.tolist()[0] - height = min(y2, fH) - max(0, y1) - width = min(x2, fW) - max(0, x1) - object_res = [ - x1, - y1, - height, - width, - result.names[class_id], - confidence, - fH, - fW, - ] - - framenum_str = f"{frameNum:04d}_{oidx:04d}" - if DEBUG_FLAG: - meta_str = ",".join( - [str(o) for o in object_res + [framenum_str]] - ) - print(f"[{stream_name} METADATA],{meta_str}", flush=True) - - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": { - "x": int(object_res[0]), - "y": int(object_res[1]), - "height": int(object_res[2]), - "width": int(object_res[3]), - "object": str(object_res[4]), - "object_det": { - "confidence": float(object_res[5]), - "frameH": int(fH), - "frameW": int(fW), - }, - }, - } - oidx += 1 - - except Exception: - e = traceback.format_exc() - print(f"Error in {stream_name} extract_metadata_from_results: {e}", flush=True) - - return metadata - - -# Detect faces from frame -def face_detection( - stream_name, frameNum, frame, img_size, face_models, face_det_CHW, ag_CHW, em_CHW -): - face_det_compiled_model, ag_compiled_model, em_compiled_model = face_models - face_det_c, face_det_h, face_det_w = face_det_CHW - ag_c, ag_h, ag_w = ag_CHW - em_c, em_h, em_w = em_CHW - W, H = img_size - bs = 1 - - genders = ["female", "male"] - emotions = ["neutral", "happy", "sad", "surprise", "anger"] - - # Resize expects HWC - input_image = cv2.resize( - frame, (face_det_w, face_det_h), interpolation=cv2.INTER_AREA - ) - input_image = input_image.transpose(2, 0, 1) # Shape: CHW - input_image = input_image.reshape((bs, face_det_c, face_det_h, face_det_w)) - - output_layer = face_det_compiled_model.output(0) - result = face_det_compiled_model([input_image])[output_layer] - - # Process the detections - faces = [] - metadata = dict() - oidx = 1 - for detection in result[0][0]: - confidence = float(detection[2]) - if confidence > DETECTION_THRESHOLD: - # Draw a bounding box around the face - x1 = int(detection[3] * frame.shape[1]) - if x1 < 0: - x1 = 0 - - y1 = int(detection[4] * frame.shape[0]) - if y1 < 0: - y1 = 0 - - x2 = int(detection[5] * frame.shape[1]) - if x2 > frame.shape[1] - 1: - x2 = frame.shape[1] - 1 - - y2 = int(detection[6] * frame.shape[0]) - if y2 > frame.shape[0] - 1: - y2 = frame.shape[0] - 1 - - height = y2 - y1 - width = x2 - x1 - - face_roi = frame[y1:y2, x1:x2] - # print(face_roi.shape) - age = gender = emotion = None - try: - ag_face_blob = cv2.resize( - face_roi, (ag_w, ag_h), interpolation=cv2.INTER_AREA - ) - ag_face_blob = ag_face_blob.transpose((2, 0, 1)) - ag_face_blob = ag_face_blob.reshape((bs, ag_c, ag_h, ag_w)) - ag_result = ag_compiled_model([ag_face_blob]) - age = int(ag_result["fc3_a"].flatten()[0] * 100) - gender = str(genders[ag_result["prob"].argmax()]) - except Exception as e: - print(f"Error occurred: {e}. Skipping age-gender model", flush=True) - - try: - em_face_blob = cv2.resize( - face_roi, (em_w, em_h), interpolation=cv2.INTER_AREA - ) - em_face_blob = em_face_blob.transpose((2, 0, 1)) - em_face_blob = em_face_blob.reshape((bs, em_c, em_h, em_w)) - em_result = em_compiled_model([em_face_blob])[ - em_compiled_model.output(0) - ] - emotion = str(emotions[em_result.argmax()]) - except Exception as e: - print(f"Error occurred: {e}. Skipping emotion model", flush=True) - face_res = [x1, y1, height, width, age, gender, emotion, confidence, H, W] - # print(face_res) - faces.append(face_res) - - tdict = { - "x": int(face_res[0]), - "y": int(face_res[1]), - "height": int(face_res[2]), - "width": int(face_res[3]), - "object": "face", - "object_det": { - "age": int(face_res[4]), - "gender": str(face_res[5]), - "emotion": str(face_res[6]), - "confidence": float(face_res[7]), - "frameH": int(H), - "frameW": int(W), - }, - } - - framenum_str = f"{frameNum:04d}_{oidx:04d}" - if DEBUG_FLAG: - meta_str = ",".join([str(o) for o in face_res + [framenum_str]]) - print(f"[{stream_name} METADATA],{meta_str}", flush=True) - - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": tdict, - } - oidx += 1 - - return metadata - - -# Inference Function -def infer_worker( - stream_name, - frameNum, - frame, - img_size, - INGESTION, - fps=TARGET_FPS, -): # img_size:(W,H) - global model, face_models, face_det_CHW, ag_CHW, em_CHW - - height, width = frame.shape[:2] - if (width, height) != img_size: - frame = cv2.resize(frame, img_size) - - metadata = {} - metadata_face = {} - try: - if "object" in INGESTION: - results = model.predict( - frame, - imgsz=(img_size[1], img_size[0]), - batch=BATCH_SIZE, - conf=DETECTION_THRESHOLD, - iou=IOU_THRESHOLD, - half=HALF_FLAG, - device=device_input, - verbose=False, - stream=True, - ) - - metadata = extract_metadata_from_results( - stream_name, frameNum, results, img_size, fps=fps - ) - del results - - if "face" in INGESTION: - metadata_face = face_detection( - stream_name, - frameNum, - frame, - img_size, - face_models, - face_det_CHW, - ag_CHW, - em_CHW, - ) - - if DEVICE == "GPU": - empty_cache() # Frees memory no longer used - gc.collect() # Forces garbage collector - except Exception: - e = traceback.format_exc() - print(f"Error in {stream_name} infer_worker: {e}", flush=True) - return metadata, metadata_face - - -""" HELPFUL FUNCTIONS """ - - -# Manual FPS calculation if OpenCV reports 0 -def manual_fps_calculation(src, num_frames=10): - vid_obj = cv2.VideoCapture(src) - - frame_count = 0 - start_t = time.time() - - while frame_count < num_frames: - grabbed, frame = vid_obj.read() - - if not grabbed: - break - - frame_count += 1 - - end_t = time.time() - vid_obj.release() - - elapsed_t = end_t - start_t - - if elapsed_t > 0: - return frame_count / elapsed_t - else: - return 0 - - -# Generate and run UDF query -def get_udf_query( - filename_path, - properties, - ingest_mode, - new_size, - id="udf_metadata", - metadata=None, - test_mode=TEST_MODE, -): - query = { - "AddVideo": { - "from_file_path": str(filename_path), # from_server_file - "is_local_file": True, - "properties": properties, - "operations": [ - { - "type": "syncremoteOp", # "remoteOp", - "url": f"http://{UDF_HOST}:{UDF_PORT}/video", - "options": { - "id": id, - "otype": ingest_mode, - "media_type": "video", - "input_sizeWH": new_size, - "filename": properties["Name"], - "ingestion": 1, - }, - } - ], - } - } - - if id == "udf_metadata" and metadata is not None: - query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata - - if test_mode: - return - - filename = str(Path(filename_path).name) - if DEBUG_FLAG: - print( - f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), - flush=True, - ) - try: - res = retry_query([query], sleep_timer=randint(1, 5)) - - if DEBUG_FLAG: - print( - f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), - flush=True, - ) - print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) - print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) - except Exception: - e = traceback.format_exc() - print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) - - # elapsed_time = time.time() - start_t - - # db.disconnect() - # del db - - -# Release Video Writer object and re-encode video to seek via ffmpeg later -def release_clip_and_reencode(clip_key, _out_vid, clip_filename, tmp_file, target_fps): - if DEBUG == "1": - print( - f"[TIMING],start_release_clip,{clip_key},{time.time()}", - flush=True, - ) - _out_vid.release() - if DEBUG == "1": - print( - f"[TIMING],end_release_clip,{clip_key},{time.time()}", - flush=True, - ) - _out_vid = None - - # Re-encode video in order to seek via ffmpeg later - GENERAL_OPTS = "-flags -global_header -hide_banner -loglevel error -nostats -tune zerolatency -flush_packets 0" # -filter:v fps={target_fps} - CONVERSION = f"-c:v libx264 -preset ultrafast -filter:v fps=fps={target_fps}" # "-c:v libx264 -preset medium" - reencode_cmd = f"ffmpeg -y -i {tmp_file} {GENERAL_OPTS} {CONVERSION} -crf 23 -c:a copy {clip_filename}" - cmd_list = shlex.split(reencode_cmd) - if DEBUG == "1": - print( - f"[TIMING],start_reencode,{clip_key},{time.time()}", - flush=True, - ) - subprocess.run(cmd_list, check=True) - end_time = time.time() - # filename = str(Path(clip_filename).name) - if DEBUG == "1": - print( - f"[TIMING],end_reencode,{clip_key},{end_time}", - flush=True, - ) - print(f"[TIMING],Save clip,{clip_key},{end_time}", flush=True) - os.remove(tmp_file) - return _out_vid - - -# method to send metadata to VDMS once clip is saved -def metadata2vdms( - clip_key, - clip_filename, - clip_metadata, - width, - height, -): - if DEBUG == "1": - print( - f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", - flush=True, - ) - - # Send metadata to UDF - properties = { - "Name": clip_key, # .split("/")[-1], - "category": "video_path_rop", - } - - combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} - if "face" in clip_metadata: - for face_frameidx_bbidx, value in clip_metadata["face"].items(): - face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") - max_obj_idx = 0 - for obj_frameidx_bbidx in combined_metadata: - if face_frameidx in obj_frameidx_bbidx: - _, obj_bbidx_ = obj_frameidx_bbidx.split("_") - max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) - - if max_obj_idx > 0: - new_face_bbidx = max_obj_idx + 1 - new_key = f"{face_frameidx}_{new_face_bbidx:04d}" - combined_metadata[new_key] = value - combined_metadata[new_key]["bbId"] = new_key - else: - combined_metadata[face_frameidx_bbidx] = value - - combined_metadata = _sort_dict_by_frame(combined_metadata) - get_udf_query( - clip_filename, - properties, - INGESTION.replace(",", "+"), - (width, height), - id="udf_metadata", - metadata=combined_metadata, - test_mode=TEST_MODE, - ) - - if DEBUG == "1": - print( - f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", - flush=True, - ) - - -""" CLASSES """ - - -# method to save clip -def save_clip( - clip_filename, clip_id, tmp_file, _out_vid, frame_count, frameNum, target_fps -): - clip_key = Path(clip_filename).name - if DEBUG == "1": - print( - f"[DEBUG] Clip {clip_key} (clip_id: {clip_id}) contains {frame_count} frames", - flush=True, - ) - _out_vid = release_clip_and_reencode( - clip_key, - _out_vid, - clip_filename, - tmp_file, - target_fps, - ) - if DEBUG == "1": - print( - f"[TIMING],end_get_clips,{clip_key},{time.time()}", - flush=True, - ) - return _out_vid - - -# method to create clips (read frame write to file; add name to list) -def send_metadata(): - global all_metadata - clip_filename = "" - clip_key = "" - width = 0 - height = 0 - while True: - try: - queue_details = send_metadata_queue.get() - if queue_details is None: - break - - (clip_key, clip_filename, width, height) = queue_details - - metadata2vdms( - clip_key, - clip_filename, - all_metadata[clip_key], - width, - height, - ) - del all_metadata[clip_key] - - except queue.Empty: - pass - - -# defining a helper class for implementing multi-threading -class VideoStream: - # initialization method - def __init__(self, src, fps=TARGET_FPS, fourcc=WRITER_FOURCC, camera_name=None): - self.stream_id = src - self.fourcc = fourcc - - if "://" in str(self.stream_id): - if camera_name is not None: - self.stream_name = camera_name - else: - self.stream_name = str(self.stream_id).split("/")[-1] - else: - self.stream_name = Path(self.stream_id).stem - - if self.stream_id.startswith("rtsp"): - os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" - - # Check that object is opened successfully - self.connect_to_stream(time_limit_mins=5) - if DEBUG == "1": - print( - f"[TIMING],Start processing,{self.stream_name},{time.time()}", - flush=True, - ) - - self.setup_stream(fps) - - # Create ThreadPoolExecutor - self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) - - # method to start thread - def start(self): - self.stopped = False - self.t = [] - self.t.append( - self.executor.submit( - self.get_frames, - ) - ) - self.t.append( - self.executor.submit( - send_metadata, - ) - ) - - # method to stop reading frames - def stop(self): - for t in as_completed(self.t): - try: - _ = t.result() - except Exception as t_e: - print(f"[DEBUG] Exception occurred in thread: {t_e}") - - # self.stopped = True - self.video_obj.release() - - # method to open stream/video within 5min (default) limit - def connect_to_stream(self, time_limit_mins=5): - # opening video capture stream - self.video_obj = cv2.VideoCapture(self.stream_id, cv2.CAP_FFMPEG) - - stream_available = False - time_limit_secs = time_limit_mins * 60 - connect_time = time.time() - while not stream_available: - if self.video_obj.isOpened(): - stream_available = True - elif self.stream_id.startswith("rtsp"): - if time.time() - connect_time < time_limit_secs: - self.video_obj = cv2.VideoCapture(self.stream_id, cv2.CAP_FFMPEG) - else: - print( - f"Exceeds {time_limit_mins} mins limit to connect to {self.stream_name}. Exiting ..." - ) - exit(1) - - # Gets video fps and framecount - def get_fps_and_framecnt(self, fps): - self.input_fps = int(self.video_obj.get(cv2.CAP_PROP_FPS)) # hardware fps - if self.input_fps == 0: # Case when FPs isn't available - self.input_fps = manual_fps_calculation(self.stream_id, num_frames=10) - - self.target_fps = fps if self.input_fps > fps else self.input_fps - self.frame_skip = int(self.input_fps / self.target_fps) - if self.frame_skip < 1: - self.frame_skip = 1 - - print(f"FPS of {self.stream_name} input stream: {self.input_fps}", flush=True) - print(f"FPS of {self.stream_name} output mp4: {self.target_fps}", flush=True) - - # Frame count for videos - self.frame_count = None - if "://" not in str(self.stream_id): - self.frame_count = int(self.video_obj.get(cv2.CAP_PROP_FRAME_COUNT)) - - # Gets frame W and H details - def get_frameWH(self): - input_width = int(self.video_obj.get(cv2.CAP_PROP_FRAME_WIDTH)) - input_height = int(self.video_obj.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - if RESIZE_FLAG or ((input_height * input_width) < (MODEL_H * MODEL_W)): - new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw - else: - new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw - - new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) - - self.width = new_sizeWH[0] - self.height = new_sizeWH[1] - - # Sets up important info for stream - def setup_stream(self, fps): - self.inference_queue = mp.Queue() - self.retrieved_frames = 0 - self.num_frames_processed = 0 - - self.get_fps_and_framecnt(fps) - - self.get_frameWH() - - self._out_vid = None - self.clip_end_frame = {} - self.clip_filename = "" - self.clip_frame_count = 0 - self.clip_frame_inds = [] - self.clip_id = 0 - self.clip_length_in_secs = 10 - self.clip_total_frames = int(float(self.clip_length_in_secs * self.target_fps)) - - # method to process a frame - def get_frames(self): - clip_frame_idx = 0 - clip_id = 0 - if DEBUG == "1": - print( - f"[TIMING],start_get_frames,{self.stream_name},{time.time()}", - flush=True, - ) - while True: - grabbed, frame = self.video_obj.read() # Read next frame - - if not grabbed: # or self.stopped: - # self.stopped = True - self.inference_queue.put(None) - break - - frameNum = int(self.video_obj.get(cv2.CAP_PROP_POS_FRAMES)) - skip_frame_num = (frameNum - 1) % self.frame_skip - - if clip_frame_idx % self.clip_total_frames == 0: - if "://" not in str(self.stream_id): - clip_filename = f"{SHARED_OUTPUT}/{self.stream_name}_{clip_id}.mp4" - else: - clip_filename = ( - f"{SHARED_OUTPUT}/{self.stream_name}_{time.time()}.mp4" - ) - - tmp_file = TMP_LOCATION + clip_filename.split("/")[-1] - - if skip_frame_num == 0: - h, w = frame.shape[:2] - if (w, h) != (self.width, self.height): - frame = cv2.resize(frame, (self.width, self.height)) - - queue_details = ( - frameNum, # Overall frame number - (clip_frame_idx % self.clip_total_frames) - + 1, # Frame index in clip [1-indexed like opencv] - clip_id, # Clip number - clip_filename, - tmp_file, - frame.copy(), # Frame - ) - - self.inference_queue.put(queue_details) - self.retrieved_frames += 1 - - clip_frame_idx += 1 - if clip_frame_idx % self.clip_total_frames == 0: - clip_id += 1 - - if DEBUG == "1": - print( - f"[TIMING],end_get_frames,{self.stream_name},{time.time()}", flush=True - ) - - -def process_stream(camera_src, camera_name=None): - global all_metadata - webcam_stream = VideoStream(str(camera_src), camera_name=camera_name) - if DEBUG == "1": - print( - f"[TIMING],Start processing,{webcam_stream.stream_name},{time.time()}", - flush=True, - ) - - start = time.time() - # Start retrieving frames and add to queue - webcam_stream.start() - _out_vid = None - clip_filename = "" - clip_frame_idx = 0 - clip_id = 0 - clip_key = "" - frameNum = 0 - tmp_file = "" - while True: - queue_details = webcam_stream.inference_queue.get() - - if queue_details is None: - if _out_vid is not None: - frame_count = clip_frame_idx - if frame_count > TARGET_FPS: - _out_vid = save_clip( - clip_filename, - clip_id, - tmp_file, - _out_vid, - frame_count, - frameNum, - webcam_stream.target_fps, - ) - send_metadata_queue.put( - ( - clip_key, - clip_filename, - webcam_stream.width, - webcam_stream.height, - ) - ) - else: - _out_vid = None - send_metadata_queue.put(None) - print("End of stream") - break - - frameNum, clip_frame_idx, clip_id, clip_filename, tmp_file, frame = ( - queue_details - ) - clip_key = Path(clip_filename).name - if DEBUG == "1": - print( - f"[TIMING],start_infer_worker,{clip_key}-{clip_frame_idx},{time.time()}", - flush=True, - ) - - metadata, metadata_face = infer_worker( - webcam_stream.stream_name, - clip_frame_idx, - frame, - (webcam_stream.width, webcam_stream.height), # img_size, - INGESTION, - fps=webcam_stream.target_fps, - ) - - if DEBUG == "1": - print( - f"[TIMING],end_infer_worker,{clip_key}-{clip_frame_idx},{time.time()}", - flush=True, - ) - - all_metadata.setdefault(clip_key, {}) - all_metadata[clip_key].setdefault("object", {}) - all_metadata[clip_key]["object"].update(metadata) - all_metadata[clip_key].setdefault("face", {}) - all_metadata[clip_key]["face"].update(metadata_face) - webcam_stream.num_frames_processed += 1 - - if clip_frame_idx - 1 == 0: - if DEBUG == "1": - print( - f"[TIMING],start_get_clips,{clip_key},{time.time()}", - flush=True, - ) - _out_vid = cv2.VideoWriter( - tmp_file, - fourcc=webcam_stream.fourcc, - fps=webcam_stream.target_fps, - frameSize=(webcam_stream.width, webcam_stream.height), - ) - if DEBUG == "1": - print( - f"[TIMING],Start new clip,{clip_key},{time.time()}", - flush=True, - ) - - _out_vid.write(frame) - - if clip_frame_idx == webcam_stream.clip_total_frames: - frame_count = clip_frame_idx - _out_vid = save_clip( - clip_filename, - clip_id, - tmp_file, - _out_vid, - frame_count, - frameNum, - webcam_stream.target_fps, - ) - queue_details = ( - clip_key, - clip_filename, - webcam_stream.width, - webcam_stream.height, - ) - send_metadata_queue.put(queue_details) - - webcam_stream.stop() - - end = time.time() - - # printing time elapsed and fps - if DEBUG == "1": - elapsed = end - start - print( - "[DEBUG] Stream name:{}, FPS: {} , Elapsed Time: {}, Num. Retrieved Frames: {}, Num. Processed Frames: {}".format( - webcam_stream.stream_name, - webcam_stream.target_fps, - elapsed, - webcam_stream.retrieved_frames, - webcam_stream.num_frames_processed, - ), - flush=True, - ) - - print( - f"[TIMING],Completed processing,{webcam_stream.stream_name},{end}", - flush=True, - ) - - -""" MAIN FUNCTION """ - -if __name__ == "__main__": - camera_src = sys.argv[1] - camera_name = sys.argv[2] if len(sys.argv) == 3 else None - - process_stream(str(camera_src), camera_name) diff --git a/video/requirements.CPU.txt b/video/requirements.CPU.txt index c3440fb..ff2846d 100644 --- a/video/requirements.CPU.txt +++ b/video/requirements.CPU.txt @@ -108,9 +108,9 @@ cffi==2.0.0 \ --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via moocore -cma==4.4.1 \ - --hash=sha256:61177b54f12bfeeac307970f8caefd8210dfa1d43a2da34e9ef8b1416d930fd8 \ - --hash=sha256:bf0621d4f52cf3354be3d0a5cd439ffed52f24f429ab02c23cf0f8ca16d427d8 +cma==4.4.2 \ + --hash=sha256:75c45f201e689ffaf72e02dc1f2ba2f98fbb6cc4e89847f3173e9bf099d1f10c \ + --hash=sha256:edd1d1f22d11ebf7a2ccae713bc3838931e31002410d19910d9d7ca9c4911fe1 # via pymoo contourpy==1.3.2 \ --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ diff --git a/video/requirements.txt b/video/requirements.txt index 95809a6..a5a2fdd 100644 --- a/video/requirements.txt +++ b/video/requirements.txt @@ -254,9 +254,9 @@ fsspec==2026.1.0 \ --hash=sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc \ --hash=sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b # via torch -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==2.10 \ + --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 # via requests inotify==0.2.12 \ --hash=sha256:9aee407f92c7d51a2ce50f3b78291a9094e334e34bd68e82bf60020795fa2c94 \ @@ -953,8 +953,8 @@ pyyaml==6.0.3 \ # openvino-dev # ultralytics requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ + --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e # via # -r video/requirements.in # openvino-dev @@ -1015,9 +1015,54 @@ sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 # via torch -tomli==1.1.0 \ - --hash=sha256:33d7984738f8bb699c9b0a816eb646a8178a69eaa792d258486776a5d21b8ca5 \ - --hash=sha256:f4a182048010e89cbec0ae4686b21f550a7f2903f665e34a6de58ec15424f919 +tomli==2.4.0 \ + --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ + --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ + --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ + --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ + --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ + --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ + --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ + --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ + --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ + --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ + --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ + --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ + --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ + --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ + --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ + --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ + --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ + --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ + --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ + --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ + --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ + --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ + --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ + --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ + --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ + --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ + --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ + --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ + --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ + --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ + --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ + --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ + --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ + --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ + --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ + --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ + --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ + --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ + --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ + --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ + --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ + --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ + --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ + --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ + --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ + --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ + --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 # via build torch==2.9.1 \ --hash=sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73 \ @@ -1124,9 +1169,9 @@ ultralytics-thop==2.0.18 \ --hash=sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf \ --hash=sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec # via ultralytics -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 # via requests vdms==0.0.22 \ --hash=sha256:4d59fedd914a645fb8a42c504c9535131f9de9a435b5add00900a2abade50036 \ diff --git a/video/resources/models/download_yolo.py b/video/resources/models/download_yolo.py index 9dae0b5..1b5282e 100644 --- a/video/resources/models/download_yolo.py +++ b/video/resources/models/download_yolo.py @@ -24,30 +24,30 @@ def str2bool(in_val): DEVICE = os.environ.get("DEVICE", "CPU") MODEL_NAME = os.environ.get("MODEL_NAME", "yolo11n") if DEVICE == "GPU": - batch_size = int(os.environ.get("GPU_BATCH_SIZE", 1)) + EXPORT_BATCH_SIZE = int(os.environ.get("GPU_BATCH_SIZE", 1)) run_platform_name = "engine" os.environ["CUDA_VISIBLE_DEVICES"] = "0" print("[!] USING GPU & TENSORRT") else: - # batch_size = 8 - batch_size = int(os.environ.get("CPU_BATCH_SIZE", 1)) # 8 + EXPORT_BATCH_SIZE = int(os.environ.get("CPU_BATCH_SIZE", 1)) run_platform_name = "openvino" print("[!] USING CPU & OPENVINO") -def get_model(model_dir, run_platform, device_input, batch=1): +def get_model(model_dir, run_platform, device_input, batch=1, force_export=False): final_model_path = f"{model_dir}/{MODEL_NAME}.pt" pt_detection_model = YOLO(final_model_path, verbose=False, task="detect") if run_platform == "openvino": - pt_detection_model.export( - format="openvino", - half=half_flag, - dynamic=dynamic_flag, - device=device_input, - batch=batch, - ) - final_model_path = f"{model_dir}/{MODEL_NAME}_openvino_model/" + if not Path(final_model_path).exists() or force_export: + pt_detection_model.export( + format="openvino", + half=half_flag, + dynamic=dynamic_flag, + device=device_input, + batch=batch, + ) + object_detection_model = YOLO( final_model_path, verbose=False, @@ -62,17 +62,18 @@ def get_model(model_dir, run_platform, device_input, batch=1): # object_detection_model.predictor.model.ov_compiled_model = compiled_model elif run_platform == "engine": - pt_detection_model.export( - format="engine", - half=half_flag, - imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) - dynamic=dynamic_flag, - device=device_input, - simplify=True, - batch=batch, - ) - final_model_path = f"{model_dir}/{MODEL_NAME}.engine" + if not Path(final_model_path).exists() or force_export: + pt_detection_model.export( + format="engine", + half=half_flag, + imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) + dynamic=dynamic_flag, + device=device_input, + simplify=True, + batch=batch, + ) + object_detection_model = YOLO( final_model_path, verbose=False, @@ -90,14 +91,15 @@ def get_model(model_dir, run_platform, device_input, batch=1): ) final_model_path = f"{model_dir}/{MODEL_NAME}.onnx" - pt_detection_model.export( - format="onnx", - half=half_flag, - dynamic=dynamic_flag, - device=device_input, - simplify=True, - batch=batch, - ) + if not Path(final_model_path).exists() or force_export: + pt_detection_model.export( + format="onnx", + half=half_flag, + dynamic=dynamic_flag, + device=device_input, + simplify=True, + batch=batch, + ) object_detection_model = YOLO(final_model_path, verbose=False, task="detect") @@ -125,6 +127,12 @@ def get_model(model_dir, run_platform, device_input, batch=1): ydir = Path(dir_path) device_input = DEVICE.lower() if DEVICE == "CPU" else "cuda" - _, _ = get_model(ydir, run_platform_name, device_input, batch=batch_size) + _, _ = get_model( + ydir, + run_platform_name, + device_input, + batch=EXPORT_BATCH_SIZE, + force_export=False, + ) os.remove(f"{ydir}/{MODEL_NAME}.pt") diff --git a/video/segment.py b/video/segment.py index dc5a7d8..6cedf16 100755 --- a/video/segment.py +++ b/video/segment.py @@ -7,7 +7,14 @@ from tornado import gen, web from tornado.concurrent import run_on_executor -from utils import safely_join_path + + +def safely_join_path(base_dir, add_path): + safe_base = os.path.abspath(base_dir) + candidate_path = os.path.abspath(os.path.join(safe_base, add_path)) + if not candidate_path.startswith(safe_base + os.sep): + raise ValueError(f"Invalid path: {candidate_path}") + return candidate_path class SegmentHandler(web.RequestHandler): diff --git a/video/thumbnail.py b/video/thumbnail.py index 9a0d236..342e054 100755 --- a/video/thumbnail.py +++ b/video/thumbnail.py @@ -7,7 +7,14 @@ from tornado import gen, web from tornado.concurrent import run_on_executor -from utils import safely_join_path + + +def safely_join_path(base_dir, add_path): + safe_base = os.path.abspath(base_dir) + candidate_path = os.path.abspath(os.path.join(safe_base, add_path)) + if not candidate_path.startswith(safe_base + os.sep): + raise ValueError(f"Invalid path: {candidate_path}") + return candidate_path class ThumbnailHandler(web.RequestHandler): diff --git a/video/watch_and_send2vdms.py b/video/watch_and_send2vdms.py index 2bdc3b8..c9a176c 100644 --- a/video/watch_and_send2vdms.py +++ b/video/watch_and_send2vdms.py @@ -1,10 +1,11 @@ import multiprocessing as mp import os -import queue -import subprocess import sys import time +from multiprocessing.managers import BaseManager +from pathlib import Path +import requests import yaml from inotify.adapters import Inotify @@ -13,18 +14,65 @@ DEBUG = os.environ["DEBUG"] DEBUG_FLAG = True if DEBUG == "1" else False +BACKEND_URL = "http://fastapi-service:8000" -def run_processor(path_or_url, camera_name=None): - cmd = [ - sys.executable, - "/home/process_stream.py", - path_or_url, - # camera_name, - ] - if camera_name is not None: - cmd.append(camera_name) +# Exit program if queue is empty for 5 minutes +empty_timeout = 5 * 60 - subprocess.run(cmd, check=True) + +# 1. Define the Manager +class QueueManager(BaseManager): + pass + + +# ENABLE_STREAMLIT = os.environ["ENABLE_STREAMLIT"] +# if ENABLE_STREAMLIT: +# from frontend.detection_runner import LiveDetectionRunner + +# n_cols = 2 +# pipeline = LiveDetectionRunner(n_cols=n_cols) +# pipeline.setup_page() + + +# def run_processor(path_or_url, camera_name=None): +# cmd = [ +# sys.executable, +# "/home/process_stream.py", +# path_or_url, +# # camera_name, +# ] +# if camera_name is not None: +# cmd.append(camera_name) + +# subprocess.run(cmd, check=True) + + +def start_stream_processor(source, camera_name): + if camera_name is None: + camera_name = Path(source).stem + + for _ in range(10): + try: + # FastAPI stream_processor + payload = {"url": str(source), "name": camera_name} + # Data is hidden in the body, no URL-encoding (%2F) mess + res = requests.post(f"{BACKEND_URL}/stream", json=payload) + # res = requests.post( + # # f"{BACKEND_URL}/stream", + # f"{BACKEND_URL}/stream?url={str(source)}&name={camera_name}", + # # json={"url": str(source), "name": camera_name}, + # timeout=10 + # ) + if res.status_code == 200: + print(f"Started {source} process.") + return res + # except requests.exceptions.ConnectionError: + # print(f"Connection reset, retrying... {res.json}") + # time.sleep(1) # Wait for buffers to clear + except Exception: # as e: + # e = traceback.format_exc() + # print(f"Error: {e}") + time.sleep(1) def watch_video_files(queue, watch_dir): @@ -34,6 +82,7 @@ def watch_video_files(queue, watch_dir): source = os.path.join(watch_dir, filename) print(f"{source} added to queue: {time.time()}", flush=True) queue.put((source, None)) + start_stream_processor(source, None) # Watch watch_dir for new files i = Inotify() @@ -51,6 +100,7 @@ def watch_video_files(queue, watch_dir): source = os.path.join(path, filename) print(f"{source} added to queue: {time.time()}", flush=True) queue.put((source, None)) + start_stream_processor(source, None) def retrieve_camera_details(queue, config_path): @@ -64,14 +114,23 @@ def retrieve_camera_details(queue, config_path): source = camera_details["url"] print(f"{source} added to queue: {time.time()}", flush=True) queue.put((source, camera_name)) + start_stream_processor(source, camera_name) def main(watch_folder=os.getcwd()): if DEBUG_FLAG: print("[TIMING],start_watchandsend,," + str(time.time()), flush=True) + # 2. Setup the Shared Queue and Manager file_queue = mp.Queue() + QueueManager.register("get_file_queue", callable=lambda: file_queue) + + # Start the manager on port 5000 + manager = QueueManager(address=("0.0.0.0", 5005), authkey=b"password123") + manager.start() + print("Queue Server started at 0.0.0.0:5005") + # 3. Start worker processes # Create a process that monitors new files in watch_folder (added to file_queue) watcher_process = mp.Process( target=watch_video_files, args=(file_queue, watch_folder) @@ -85,24 +144,56 @@ def main(watch_folder=os.getcwd()): watcher_process.start() watcher_process_camera.start() - # Pool of workers to process video clips - with mp.Pool(processes=num_workers) as pool: + # if ENABLE_STREAMLIT: + # while True: + # if not file_queue.empty(): + # pipeline.setup_stream_section() + # break + + # 4. Keep the server alive + try: while True: - try: - path_or_url, camera_name = file_queue.get(timeout=0.5) - # pool.apply(run_processor, (path_or_url, camera_name,)) - pool.apply_async( - run_processor, - ( - path_or_url, - camera_name, - ), - ) - except queue.Empty: - pass - - watcher_process.join() - watcher_process_camera.join() + time.sleep(1) + except KeyboardInterrupt: + print("Stopping...") + finally: + watcher_process.terminate() + watcher_process_camera.terminate() + manager.shutdown() + + # # Pool of workers to process video clips + # # empty_queue_start = None + # empty_queue_start = time.time() + # i = 0 # stream_id + # while time.time() - empty_queue_start < empty_timeout: + # if not file_queue.empty(): + # path_or_url, camera_name = file_queue.get(timeout=0.5) + # empty_queue_start = None + # fastapi_processor = f"{BACKEND_URL}/video_feed/{camera_name}" + # if ENABLE_STREAMLIT: + # with pipeline.stream_cols[i % 2]: + # pipeline.st.subheader(f"Stream {i}: {camera_name}") + # pipeline.st.image(fastapi_processor + f"?path_or_url={path_or_url}") + # else: + # # pool.apply_async( + # # run_processor, + # # ( + # # path_or_url, + # # camera_name, + # # ), + # # ) + # _ = requests.get( + # fastapi_processor, + # data=[("path_or_url", path_or_url)], + # stream=True, + # ) + + # empty_queue_start = time.time() # Reset timer if item is processed + # else: + # time.sleep(0.1) # Sleep briefly to prevent high CPU usage + + # watcher_process.join() + # watcher_process_camera.join() if DEBUG_FLAG: print("[TIMING],end_watchandsend,," + str(time.time()), flush=True) From 2e192f8da07217a3e2c4032758661a5ae7c922ff Mon Sep 17 00:00:00 2001 From: "Lacewell, Chaunte W" Date: Thu, 19 Mar 2026 11:34:18 -0700 Subject: [PATCH 02/20] Add fastapi version to display detections as processed Signed-off-by: Lacewell, Chaunte W --- fastapi/Dockerfile | 2 +- fastapi/include/utils.py | 1402 ++++++++-------------------------- fastapi/main.py | 1090 ++++++++++++++------------ fastapi/templates/index.html | 184 +++++ video/nginx.conf | 28 +- 5 files changed, 1122 insertions(+), 1584 deletions(-) create mode 100644 fastapi/templates/index.html diff --git a/fastapi/Dockerfile b/fastapi/Dockerfile index af2a974..6571ac0 100644 --- a/fastapi/Dockerfile +++ b/fastapi/Dockerfile @@ -45,7 +45,7 @@ RUN python -m pip install pip --upgrade --no-cache-dir && \ COPY *.py /home/ COPY include /home/include -# COPY templates /home/templates +COPY templates /home/templates COPY nginx.conf /etc/nginx/nginx.conf # CMD /usr/local/sbin/nginx && uvicorn main:app --host 127.0.0.1 --port 8000 diff --git a/fastapi/include/utils.py b/fastapi/include/utils.py index 84668d5..5347f17 100644 --- a/fastapi/include/utils.py +++ b/fastapi/include/utils.py @@ -9,6 +9,7 @@ from datetime import datetime from math import ceil from pathlib import Path +from random import randint import cv2 import numpy as np @@ -64,7 +65,7 @@ def str2bool(in_val): MODEL_W, MODEL_H = (640, 640) NUM_USUABLE_CPUS = 2 TARGET_FPS = 15 -UDF_PORT = 5011 +FRAME_INTERVAL = 1.0 / TARGET_FPS # ~0.0667 seconds WRITER_FOURCC = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v CODE_DIR = os.getenv("CODE_DIR", "/home") @@ -79,12 +80,12 @@ def str2bool(in_val): MODEL_NAME = os.getenv("MODEL_NAME", "yolo11n") OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) RESIZE_FLAG = str2bool(os.getenv("RESIZE_FLAG", False)) -RESIZE_FLAG = str2bool(os.getenv("RESIZE_FLAG", False)) SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") -UDF_HOST = os.getenv("UDF_HOST", "fastapi-service") +UDF_HOST = os.getenv("UDF_HOST", "udf-service") +UDF_PORT = 5011 if DEVICE == "GPU": EXPORT_BATCH_SIZE = int(os.environ.get("GPU_BATCH_SIZE", 1)) @@ -107,6 +108,184 @@ def str2bool(in_val): ] +MASK_THRESHOLD_VALUE = 127 +MASK_MAX_VALUE = 255 +MAX_DETECTIONS = 100 + +# Plot variables +THICKNESS_SCALE_FACTOR = 1e-3 +FONT_SCALE_FACTOR = 1e-3 + + +YOLO_CLASS_NAMES = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", +] + +PLOT_HEXS = ( + "042AFF", + "0BDBEB", + "F3F3F3", + "00DFB7", + "111F68", + "FF6FDD", + "FF444F", + "CCED00", + "00F344", + "BD00FF", + "00B4FF", + "DD00BA", + "00FFFF", + "26C000", + "01FFB3", + "7D24FF", + "7B0068", + "FF1B6C", + "FC6D2F", + "A2FF0B", +) + +DETECTION_COLORS = [] +for h in PLOT_HEXS: + DETECTION_COLORS.append( + tuple(int(f"#{h}"[1 + i : 1 + i + 2], 16) for i in (0, 2, 4)) + ) + + +def get_detection_color(index, is_bgr=False): + ind = int(index) % len(PLOT_HEXS) + color = DETECTION_COLORS[ind] + if is_bgr: + return (color[2], color[1], color[0]) + else: + return color + + +def get_line_thickness(npixels, ref_pixels=(1280 * 720)): + ref_thickness = 1 + factor = npixels / ref_pixels + thickness = int(ref_thickness * factor) + if thickness < 1: + thickness = 1 + return thickness + + +def draw_label( + image, + label, + txt_bt_lft_corner, + font_face=cv2.FONT_HERSHEY_SIMPLEX, + color=(255, 255, 255), + padding=5, +): + height, width, _ = image.shape + + # Scale font and thickness based on the image's smaller dimension + scaled_font_scale = min(width, height) * FONT_SCALE_FACTOR + scaled_thickness = max(1, ceil(min(width, height) * THICKNESS_SCALE_FACTOR)) + + # Get text size and define position for the label background + (label_W, label_H), baseline = cv2.getTextSize( + label, font_face, scaled_font_scale, scaled_thickness + ) + label_y1 = (txt_bt_lft_corner[0], txt_bt_lft_corner[1] - label_H - padding) + label_y2 = ( + txt_bt_lft_corner[0] + label_W + padding, + txt_bt_lft_corner[1] + baseline, + ) + cv2.rectangle(image, label_y1, label_y2, color, -1) + + # Print label + cv2.putText( + image, + label, + (txt_bt_lft_corner[0] + padding // 2, txt_bt_lft_corner[1] - padding // 2), + font_face, + scaled_font_scale, + (0, 0, 0), # Black text + scaled_thickness, + cv2.LINE_AA, + ) + + def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): global db for ridx in range(num_retries + 1): @@ -133,52 +312,6 @@ def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int return response -# # This code is based on https://github.com/streamlit/demo-self-driving/blob/230245391f2dda0cb464008195a470751c01770b/streamlit_app.py#L48 # noqa: E501 -# def download_file(url, download_to: Path, expected_size=None): -# # Don't download the file twice. -# # (If possible, verify the download using the file length.) -# if download_to.exists(): -# if expected_size: -# if download_to.stat().st_size == expected_size: -# return -# else: -# st.info(f"{url} is already downloaded.") -# if not st.button("Download again?"): -# return - -# download_to.parent.mkdir(parents=True, exist_ok=True) - -# # These are handles to two visual elements to animate. -# weights_warning, progress_bar = None, None -# try: -# weights_warning = st.warning("Downloading %s..." % url) -# progress_bar = st.progress(0) -# with open(download_to, "wb") as output_file: -# with urllib.request.urlopen(url) as response: -# length = int(response.info()["Content-Length"]) -# counter = 0.0 -# MEGABYTES = 2.0**20.0 -# while True: -# data = response.read(8192) -# if not data: -# break -# counter += len(data) -# output_file.write(data) - -# # We perform animation by overwriting the elements. -# weights_warning.warning( -# "Downloading %s... (%6.2f/%6.2f MB)" -# % (url, counter / MEGABYTES, length / MEGABYTES) -# ) -# progress_bar.progress(min(counter / length, 1.0)) -# # Finally, we remove these visual elements by calling .empty(). -# finally: -# if weights_warning is not None: -# weights_warning.empty() -# if progress_bar is not None: -# progress_bar.empty() - - def format_df_value(value): if value is None: return value @@ -304,6 +437,25 @@ def get_models(model_tag: str, model_dir=PROJECT_PATH / "models"): # , _st_side return model, model_path, labels +# +def get_display_frame_in_bytes(foi, frame_width, display_size=(1280, 720), quality=50): + if frame_width > display_size[0]: + display_frame = cv2.resize(foi, display_size, interpolation=cv2.INTER_AREA) + ret, buffer = cv2.imencode( + ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + ) + else: + ret, buffer = cv2.imencode( + ".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + ) + if ret: + frame_bytes = buffer.tobytes() + else: + frame_bytes = None + + return frame_bytes + + # Manual FPS calculation if OpenCV reports 0 def manual_fps_calculation(src, num_frames=10): vid_obj = cv2.VideoCapture(src) @@ -330,30 +482,126 @@ def manual_fps_calculation(src, num_frames=10): return 0 -# def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): -# global db -# for ridx in range(num_retries + 1): -# response, _ = db.query(query, [[]]) -# if "FailedCommand" in response[0] and any( -# k in response[0]["info"].lower() for k in ERR_KEYWORDS -# ): -# err = response[0]["info"] -# if DEBUG == "1": -# query_type = list(query[0].keys())[0] -# print( -# f"DEBUG [process_stream Attempt #{ridx}] Received '{err}' for {query_type} query", -# flush=True, -# ) -# if sleep_timer > 0: -# time.sleep(sleep_timer) -# else: -# if DEBUG == "1": -# print( -# f"[DEBUG process_stream] Successful query response: {response}", -# flush=True, -# ) -# break # Continue -# return response +# Generate and run UDF query +def get_udf_query( + filename_path, + properties, + ingest_mode, + new_size, + id="udf_metadata", + metadata=None, + test_mode=TEST_MODE, +): + query = { + "AddVideo": { + "from_file_path": str(filename_path), # from_server_file + "is_local_file": True, + "properties": properties, + "operations": [ + { + "type": "syncremoteOp", # "remoteOp", + "url": f"http://{UDF_HOST}:{UDF_PORT}/video", + "options": { + "id": id, + "otype": ingest_mode, + "media_type": "video", + "input_sizeWH": new_size, + "filename": properties["Name"], + "ingestion": 1, + }, + } + ], + } + } + + if id == "udf_metadata" and metadata is not None: + query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata + + if test_mode: + return + + filename = str(Path(filename_path).name) + if DEBUG_FLAG: + print( + f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), + flush=True, + ) + try: + res = retry_query([query], sleep_timer=randint(1, 5)) + + if DEBUG_FLAG: + print( + f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), + flush=True, + ) + print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) + print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) + except Exception: + e = traceback.format_exc() + print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) + + +def _sort_dict_by_frame(in_dict): + def _by_int(key): + return tuple(int(k) for k in key.split("_")) + + return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) + + +# method to send metadata to VDMS once clip is saved +def metadata2vdms( + clip_key, + clip_filename, + clip_metadata, + width, + height, +): + if DEBUG == "1": + print( + f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) + + # Send metadata to UDF + properties = { + "Name": clip_key, # .split("/")[-1], + "category": "video_path_rop", + } + + combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} + if "face" in clip_metadata: + for face_frameidx_bbidx, value in clip_metadata["face"].items(): + face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") + max_obj_idx = 0 + for obj_frameidx_bbidx in combined_metadata: + if face_frameidx in obj_frameidx_bbidx: + _, obj_bbidx_ = obj_frameidx_bbidx.split("_") + max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) + + if max_obj_idx > 0: + new_face_bbidx = max_obj_idx + 1 + new_key = f"{face_frameidx}_{new_face_bbidx:04d}" + combined_metadata[new_key] = value + combined_metadata[new_key]["bbId"] = new_key + else: + combined_metadata[face_frameidx_bbidx] = value + + combined_metadata = _sort_dict_by_frame(combined_metadata) + get_udf_query( + clip_filename, + properties, + INGESTION.replace(",", "+"), + (width, height), + id="udf_metadata", + metadata=combined_metadata, + test_mode=TEST_MODE, + ) + + if DEBUG == "1": + print( + f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) # Extract metadata from object model results @@ -464,398 +712,6 @@ def release_clip_and_reencode(clip_key, _out_vid, clip_filename, tmp_file, targe return _out_vid -# """ -# VDMS RELATED FUNCTIONS -# """ - - -# def vdms_connection_status(dbhost: str = "localhost", dbport: int = 55555): -# db = vdms.vdms() -# availability_status = False -# try: -# availability_status = db.connect(dbhost, int(dbport)) -# except Exception: -# pass -# return availability_status - - -# def initialize_vdms_df(): -# st.session_state.vdms_instance_df = pd.DataFrame( -# columns=["Container Name", "Hostname", "Port", "Status"] -# ) - - -# def search_for_vdms_instances(): -# if st.button("Search for VDMS demo instances"): -# with st.spinner("Processing..."): -# info_str = get_vdms_instances() - -# st.info(info_str) - - -# def get_docker_status(dbhost="localhost", dbport=55555): -# try: -# get_containers_cmd = 'docker ps --filter name=vdms_log_pipeline --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"' -# output = subprocess.check_output(get_containers_cmd, shell=True) -# condition = "vdms_log_pipeline" in output.decode("utf-8") -# except Exception: -# db = vdms.vdms() -# condition = db.connect(dbhost, dbport) -# if condition: -# return "Deployed" -# else: -# return "Not Deployed" - - -# def get_vdms_instances(): -# get_containers_cmd = 'docker ps --filter name=vdms_.*_demo_test --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"' -# output = subprocess.check_output(get_containers_cmd, shell=True) - -# initialize_vdms_df() - -# lines = [line for line in output.decode("utf-8").split("\n")[1:] if line] -# line_names = ["ID", "Container Name", "Ports"] -# new_instances = 0 -# for line in lines: -# line_split = line.split() -# line_data = line_split[:2] + ["".join(line_split[2:])] - -# name = line_data[line_names.index("Container Name")] -# hostname = "localhost" -# port_str = line_data[line_names.index("Ports")] -# end_idx = port_str.find("->55555/tcp") -# port = port_str[port_str.find(":") + 1 : end_idx] -# availability_status = vdms_connection_status(hostname, int(port)) - -# new_data = pd.DataFrame( -# { -# "Container Name": [name], -# "Hostname": [hostname], -# "Port": [port], -# "Status": ["Connected" if availability_status else "Not Available"], -# } -# ) -# st.session_state.vdms_instance_df = pd.concat( -# [st.session_state.vdms_instance_df, new_data], ignore_index=True -# ) -# new_instances += 1 - -# info_str = f"{new_instances} VDMS instances found" - -# st.session_state.vdms_instance_df.sort_values( -# by=["Container Name"], inplace=True, ignore_index=True -# ) -# st.session_state.vdms_instance_df.drop_duplicates(inplace=True, ignore_index=True) - -# return info_str - - -# def add_vdms_instance_buttons(vdms_details): -# dbhost = vdms_details[1] -# dbport = vdms_details[2] - -# left_column, right_column = st.columns([0.5, 0.5]) - -# right_column.checkbox("Kill & Restart if DB exists", key="Kill_restart") - -# if left_column.form_submit_button("Add", use_container_width=True): -# if all([arg != "" for arg in [dbhost, dbport]]): -# matching_idx = st.session_state.vdms_instance_df.loc[ -# (st.session_state.vdms_instance_df["Hostname"] == dbhost) -# & (st.session_state.vdms_instance_df["Port"] == dbport) -# ].index.tolist() - -# if not st.session_state.Kill_restart and len(matching_idx) != 0: -# vdms_method = "Use existing DB" -# else: -# vdms_method = "Fresh DB" - -# if len(matching_idx) > 0: -# instance_num = matching_idx[0] -# container_name = st.session_state.vdms_instance_df.at[ -# instance_num, "Container Name" -# ] - -# # Start instance locally -# _ = start_vdms_docker( -# container_name=container_name, -# project_path=PROJECT_PATH, -# dbport=int(dbport), -# vdms_method=vdms_method, -# ) -# info_str = ( -# f"Instance #{instance_num} ({container_name}) already running" -# ) -# if vdms_method == "Fresh DB": -# info_str += " but redeploying" -# st.info(info_str) - -# else: -# instance_num = st.session_state.vdms_instance_df.shape[0] + 1 -# container_name = f"vdms_{instance_num}_demo_test" - -# # Start instance locally -# _ = start_vdms_docker( -# container_name=container_name, -# project_path=PROJECT_PATH, -# dbport=int(dbport), -# vdms_method=vdms_method, -# ) -# vdms_details[0] = container_name - -# # Check is available for connection -# availability_status = vdms_connection_status(dbhost, int(dbport)) -# vdms_details[-1] = ( -# "Connected" if availability_status else "Not Available" -# ) - -# if vdms_details[-1] == "Connected": -# st.session_state.vdms_instance_df.loc[instance_num] = vdms_details -# info_str = f"Instance #{instance_num} ({container_name}) added" -# st.info(info_str) -# else: -# st.error("Cannot connect to instance; Check server") - -# else: -# st.error("Must provide VDMS Port") - -# st.session_state.vdms_instance_df.sort_values( -# by=["Container Name"], inplace=True, ignore_index=True -# ) -# st.session_state.vdms_instance_df.drop_duplicates(inplace=True, ignore_index=True) - - -# def add_vdms_instance(): -# col_count = st.session_state.vdms_instance_df.shape[1] - -# with st.form(key="add form", clear_on_submit=True): -# cols = st.columns(1) -# vdms_details = [] - -# col_idx = 0 -# for i in range(col_count): -# value = "" -# if st.session_state.vdms_instance_df.columns[i] in ["Hostname", "Port"]: -# if st.session_state.vdms_instance_df.columns[i] == "Hostname": -# # Only local deployment supported for demo -# value = "localhost" - -# if st.session_state.vdms_instance_df.columns[i] == "Port": -# value = str( -# cols[col_idx].text_input( -# st.session_state.vdms_instance_df.columns[i] -# ) -# ) - -# vdms_details.append(value) - -# add_vdms_instance_buttons(vdms_details) - - -# def populate_vdms_instances(): -# if "vdms_instance_df" not in st.session_state: -# initialize_vdms_df() - -# st.markdown("1. If instances already deployed, search for them below.") -# search_for_vdms_instances() -# st.markdown("\n\n") - -# st.markdown("2. Provide local port to deploy instance.") -# add_vdms_instance() -# st.markdown("\n\n") - -# st.markdown("### VDMS Instances") -# st.dataframe(st.session_state.vdms_instance_df, use_container_width=True) - - -MASK_THRESHOLD_VALUE = 127 -MASK_MAX_VALUE = 255 -MAX_DETECTIONS = 100 - -# Plot variables -THICKNESS_SCALE_FACTOR = 1e-3 -FONT_SCALE_FACTOR = 1e-3 - - -YOLO_CLASS_NAMES = [ - "person", - "bicycle", - "car", - "motorcycle", - "airplane", - "bus", - "train", - "truck", - "boat", - "traffic light", - "fire hydrant", - "stop sign", - "parking meter", - "bench", - "bird", - "cat", - "dog", - "horse", - "sheep", - "cow", - "elephant", - "bear", - "zebra", - "giraffe", - "backpack", - "umbrella", - "handbag", - "tie", - "suitcase", - "frisbee", - "skis", - "snowboard", - "sports ball", - "kite", - "baseball bat", - "baseball glove", - "skateboard", - "surfboard", - "tennis racket", - "bottle", - "wine glass", - "cup", - "fork", - "knife", - "spoon", - "bowl", - "banana", - "apple", - "sandwich", - "orange", - "broccoli", - "carrot", - "hot dog", - "pizza", - "donut", - "cake", - "chair", - "couch", - "potted plant", - "bed", - "dining table", - "toilet", - "tv", - "laptop", - "mouse", - "remote", - "keyboard", - "cell phone", - "microwave", - "oven", - "toaster", - "sink", - "refrigerator", - "book", - "clock", - "vase", - "scissors", - "teddy bear", - "hair drier", - "toothbrush", -] - -PLOT_HEXS = ( - "042AFF", - "0BDBEB", - "F3F3F3", - "00DFB7", - "111F68", - "FF6FDD", - "FF444F", - "CCED00", - "00F344", - "BD00FF", - "00B4FF", - "DD00BA", - "00FFFF", - "26C000", - "01FFB3", - "7D24FF", - "7B0068", - "FF1B6C", - "FC6D2F", - "A2FF0B", -) - -DETECTION_COLORS = [] -for h in PLOT_HEXS: - DETECTION_COLORS.append( - tuple(int(f"#{h}"[1 + i : 1 + i + 2], 16) for i in (0, 2, 4)) - ) - - -def get_detection_color(index, is_bgr=False): - ind = int(index) % len(PLOT_HEXS) - color = DETECTION_COLORS[ind] - if is_bgr: - return (color[2], color[1], color[0]) - else: - return color - - -def get_line_thickness(npixels, ref_pixels=(1280 * 720)): - ref_thickness = 1 - factor = npixels / ref_pixels - thickness = int(ref_thickness * factor) - if thickness < 1: - thickness = 1 - return thickness - - -def draw_label( - image, - label, - txt_bt_lft_corner, - font_face=cv2.FONT_HERSHEY_SIMPLEX, - color=(255, 255, 255), - padding=5, -): - height, width, _ = image.shape - - # Scale font and thickness based on the image's smaller dimension - scaled_font_scale = min(width, height) * FONT_SCALE_FACTOR - scaled_thickness = max(1, ceil(min(width, height) * THICKNESS_SCALE_FACTOR)) - - # Get text size and define position for the label background - (label_W, label_H), baseline = cv2.getTextSize( - label, font_face, scaled_font_scale, scaled_thickness - ) - label_y1 = (txt_bt_lft_corner[0], txt_bt_lft_corner[1] - label_H - padding) - label_y2 = ( - txt_bt_lft_corner[0] + label_W + padding, - txt_bt_lft_corner[1] + baseline, - ) - cv2.rectangle(image, label_y1, label_y2, color, -1) - - # Print label - cv2.putText( - image, - label, - (txt_bt_lft_corner[0] + padding // 2, txt_bt_lft_corner[1] - padding // 2), - font_face, - scaled_font_scale, - (0, 0, 0), # Black text - scaled_thickness, - cv2.LINE_AA, - ) - - -@dataclass -class PipelineMapping: - resize_device: str.lower = "cpu" - bkgd_subtraction_device: str.lower = "cpu" - threshold_device: str.lower = "cpu" - erodeAndDilate_device: str.lower = "cpu" - contour_device: str.lower = "cpu" - detection_device: str.lower = "cpu" - - def merge_boxes_limit(bbs_full_res, dist_threshold=50, size_limit=640): """ boxes: list of [x1, y1, x2, y2] @@ -944,611 +800,11 @@ def filter_contained_boxes(boxes, containment_thresh=0.90): return keep -# from random import randint -# # Generate and run UDF query -# def get_udf_query( -# filename_path, -# properties, -# ingest_mode, -# new_size, -# id="udf_metadata", -# metadata=None, -# test_mode=TEST_MODE, -# ): -# query = { -# "AddVideo": { -# "from_file_path": str(filename_path), # from_server_file -# "is_local_file": True, -# "properties": properties, -# "operations": [ -# { -# "type": "syncremoteOp", # "remoteOp", -# "url": f"http://{UDF_HOST}:{UDF_PORT}/video", -# "options": { -# "id": id, -# "otype": ingest_mode, -# "media_type": "video", -# "input_sizeWH": new_size, -# "filename": properties["Name"], -# "ingestion": 1, -# }, -# } -# ], -# } -# } - -# if id == "udf_metadata" and metadata is not None: -# query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata - -# if test_mode: -# return - -# filename = str(Path(filename_path).name) -# if DEBUG_FLAG: -# print( -# f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), -# flush=True, -# ) -# try: -# res = retry_query([query], sleep_timer=randint(1, 5)) - -# if DEBUG_FLAG: -# print( -# f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), -# flush=True, -# ) -# print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) -# print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) -# except Exception: -# e = traceback.format_exc() -# print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) - -# # elapsed_time = time.time() - start_t - -# # db.disconnect() -# # del db - - -# def _sort_dict_by_frame(in_dict): -# def _by_int(key): -# return tuple(int(k) for k in key.split("_")) - -# return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) - - -# # method to send metadata to VDMS once clip is saved -# def metadata2vdms( -# clip_key, -# clip_filename, -# clip_metadata, -# width, -# height, -# ): -# if DEBUG == "1": -# print( -# f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", -# flush=True, -# ) - -# # Send metadata to UDF -# properties = { -# "Name": clip_key, # .split("/")[-1], -# "category": "video_path_rop", -# } - -# combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} -# if "face" in clip_metadata: -# for face_frameidx_bbidx, value in clip_metadata["face"].items(): -# face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") -# max_obj_idx = 0 -# for obj_frameidx_bbidx in combined_metadata: -# if face_frameidx in obj_frameidx_bbidx: -# _, obj_bbidx_ = obj_frameidx_bbidx.split("_") -# max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) - -# if max_obj_idx > 0: -# new_face_bbidx = max_obj_idx + 1 -# new_key = f"{face_frameidx}_{new_face_bbidx:04d}" -# combined_metadata[new_key] = value -# combined_metadata[new_key]["bbId"] = new_key -# else: -# combined_metadata[face_frameidx_bbidx] = value - -# combined_metadata = _sort_dict_by_frame(combined_metadata) -# get_udf_query( -# clip_filename, -# properties, -# INGESTION.replace(",", "+"), -# (width, height), -# id="udf_metadata", -# metadata=combined_metadata, -# test_mode=TEST_MODE, -# ) - -# if DEBUG == "1": -# print( -# f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", -# flush=True, -# ) - - -# # method to create clips (read frame write to file; add name to list) -# # def send_metadata(): -# # global all_metadata -# # clip_filename = "" -# # clip_key = "" -# # width = 0 -# # height = 0 -# # while True: -# # try: -# # queue_details = send_metadata_queue.get() -# # if queue_details is None: -# # break - -# # (clip_key, clip_filename, width, height) = queue_details - -# # metadata2vdms( -# # clip_key, -# # clip_filename, -# # all_metadata[clip_key], -# # width, -# # height, -# # ) -# # del all_metadata[clip_key] - -# # except queue.Empty: -# # pass - - -# class StreamProcessor: -# def __init__(self, model_path, source, name): -# self.model = YOLO(model_path, task="detect") -# self.cap = cv2.VideoCapture(source) -# self.name = name -# self.source =source - -# self.video_writer = None -# self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") -# self.clip_id = 0 -# self.clip_filename = "" -# self.clip_key = "" -# self.tmp_file = "" - -# self.resize_h, self.resize_w = [MODEL_H, MODEL_W] -# self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) -# self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) -# self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) -# self.fps = int(self.cap.get(cv2.CAP_PROP_FPS)) -# self.scale_x = self.frame_width / MODEL_W -# self.scale_y = self.frame_height / MODEL_H -# self.min_contour_area = int((0.005 * self.frame_width) * (0.005 * self.frame_height) ) # 207 - -# self.operation_device_map = PipelineMapping(detection_device="cpu") # No CUDA HERE -# self.device_input = ( -# self.operation_device_map.detection_device -# if self.operation_device_map.detection_device == "cpu" -# else "cuda" -# ) - -# self.cpu_resized_frame = None - -# # Subtraction -# history= 300 # int(5 * self.fps) -# background_thresh = 350 -# NSamples = 10 -# kNNSamples = 2 -# self.lr = -1 #.01 #-1 # 0.001 #1 / (5 * self.fps) # -1 # 0.01 # 1 / history -# bkgd_mask_queue_size = 3 -# self.backSub_cpu = cv2.createBackgroundSubtractorKNN( -# history=history, # default 500 -# dist2Threshold=background_thresh, # default 400 -# detectShadows=False, # default True -# ) -# self.backSub_cpu.setkNNSamples(kNNSamples) -# self.backSub_cpu.setNSamples(NSamples) - -# prev_bkgd = np.zeros((MODEL_H, MODEL_W), dtype="uint8") -# self.mask_history = deque(maxlen=bkgd_mask_queue_size) -# self.mask_history.append(prev_bkgd) - -# self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) -# self.dilate_kernel_for_enhanced_mask = np.ones((21,21), np.uint8) - -# # Create ThreadPoolExecutor -# self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) - -# def new_get_detections_for_contours_bbs( -# self, frameNum, foi, contours, thickness=2, device_input="cuda" -# ): -# global active_streams -# # source = self.source -# stream_name = self.name -# num_objs = 0 -# # predictions = [] -# metadata = dict() -# cropped_imgs, cropped_coords = [], [] -# H, W = foi.shape[:2] # Unpack once -# bbs_full_res = [] - -# # Filter and Sort in one go (Minimize Python-to-C++ crossings) -# raw_bbs=[] -# padding = 64 -# for c in contours: -# area = cv2.contourArea(c) -# x1,y1,w,h = cv2.boundingRect(c) -# if area > self.min_contour_area: # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect -# xx1 = max(0, int((x1 * self.scale_x)) - padding) -# yy1 = max(0, int((y1 * self.scale_y)) - padding) -# xx2 = min(W, int(((x1+w) * self.scale_x)) + padding) -# yy2 = min(H, int(((y1+h) * self.scale_y)) + padding) -# raw_bbs.append([area,[xx1,yy1,xx2,yy2]]) -# bbs_full_res = sorted( -# [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], -# key=lambda x: x[0], -# reverse=True, -# )[:MAX_DETECTIONS] - -# dist_thresh = min(0.05*W,0.05*H) -# merged = merge_boxes_limit(bbs_full_res, dist_threshold=dist_thresh, size_limit=640) - -# merged = filter_contained_boxes(merged, containment_thresh=0.9) - -# # for cnt, area in merged: -# for (x1, y1, x2, y2) in merged: - -# if x2 > x1 and y2 > y1 and (x2-x1) < self.frame_width and (y2-y1) < self.frame_height : -# crop = foi[y1:y2, x1:x2] -# if crop.size > 0: -# cropped_imgs.append(crop) -# cropped_coords.append((x1, y1)) - -# if not cropped_imgs: -# return metadata #num_objs, predictions - -# # 2. Inference (Keep stream=False as it is stable) -# results = self.model.predict( -# cropped_imgs, -# imgsz=MODEL_W, -# batch=len(cropped_imgs), -# device=device_input, -# verbose=False, -# stream=True, -# max_det=MAX_DETECTIONS, -# # classes=[0], # only "person", -# # conf=0.45, -# ) - -# label_source = ( -# self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES -# ) - -# for ridx, r in enumerate(results): -# if r.boxes is None or len(r.boxes) == 0: -# continue - -# # Move to CPU in one bulk operation per crop -# boxes = r.boxes.xyxy.cpu().numpy().astype(int) -# clss = r.boxes.cls.cpu().numpy().astype(int) -# confs = r.boxes.conf.cpu().numpy() -# off_x, off_y = cropped_coords[ridx] - -# for j in range(len(boxes)): -# num_objs += 1 -# bx1, by1, bx2, by2 = boxes[j] -# abs_x1, abs_y1 = off_x + bx1, off_y + by1 -# abs_x2, abs_y2 = off_x + bx2, off_y + by2 -# class_id = clss[j] -# class_name = label_source[class_id] -# confidence = confs[j] -# if confidence > DETECTION_THRESHOLD: -# if not OMIT_DETECTIONS_FLAG: -# timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] -# print( -# # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", -# f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", -# flush=True, -# ) - -# bb_color = get_detection_color(class_id, is_bgr=True) - -# cv2.rectangle( -# foi, -# (abs_x1, abs_y1), -# (abs_x2, abs_y2), -# bb_color, -# thickness, -# ) -# label = f"{class_name} {confidence:.2f}" -# draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) - -# height = min(abs_y2, H) - max(0, abs_y1) -# width = min(abs_x2, W) - max(0, abs_x1) -# object_res = [ -# abs_x1, -# abs_y1, -# height, -# width, -# class_name, -# confidence, -# H, -# W, -# ] - -# framenum_str = f"{frameNum:04d}_{j:04d}" -# if DEBUG_FLAG: -# meta_str = ",".join( -# [str(o) for o in object_res + [framenum_str]] -# ) -# print(f"[{stream_name} METADATA],{meta_str}", flush=True) - -# metadata[framenum_str] = { -# "frameId": frameNum, -# "bbId": framenum_str, -# "bbox": { -# "x": int(object_res[0]), -# "y": int(object_res[1]), -# "height": int(object_res[2]), -# "width": int(object_res[3]), -# "object": str(object_res[4]), -# "object_det": { -# "confidence": float(object_res[5]), -# "frameH": int(H), -# "frameW": int(W), -# }, -# }, -# } - -# # annotated_frame = r.plot() - -# # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) -# if self.frame_width > 1280: -# display_frame = cv2.resize(foi, (1280, 720), interpolation=cv2.INTER_AREA) -# _, buffer = cv2.imencode(".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) -# else: -# _, buffer = cv2.imencode(".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) -# frame_bytes = buffer.tobytes() - -# # Maintain only the freshest frame in the queue -# if active_streams[self.name].full(): -# try: -# active_streams[self.name].get_nowait() -# except Exception: -# pass -# active_streams[self.name].put(frame_bytes) - -# try: -# # Use block=False so the inference doesn't wait if the UI is slow -# active_streams[self.name].put(frame_bytes, block=False) -# except: -# pass # Skip frame if queue is full - -# # # Handle Video Writing (Cycle every 10 seconds) -# # clip_frameNum = (frameNum - 1) % MAX_FRAMES_PER_CLIP -# # print(f"frameNum: {frameNum} ({clip_frameNum})") -# # if clip_frameNum == 0: -# # if self.video_writer: -# # # video_writer.release() -# # self.video_writer = release_clip_and_reencode( -# # self.clip_key, self.video_writer, self.clip_filename, self.tmp_file, TARGET_FPS -# # ) - -# # send_metadata_queue.put( -# # ( -# # self.clip_key, -# # self.clip_filename, -# # self.frame_width, -# # self.frame_height, -# # ) -# # ) -# # self.clip_id += 1 - -# # if "://" not in str(source): -# # self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{self.clip_id}.mp4" -# # else: -# # self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{time.time()}.mp4" - -# # self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] -# # self.clip_key = Path(self.clip_filename).name - -# # # timestamp = int(time.time()) -# # # filename = f"clip_{timestamp}.mp4" -# # self.video_writer = cv2.VideoWriter(self.tmp_file, self.fourcc, TARGET_FPS, (width, height)) -# # main_app_logger.info(f"Started new clip: {self.tmp_file}") - -# # # 3. Write frame -# # self.video_writer.write(foi) -# # frame_counter += 1 -# return metadata - - -# # if not results: -# # return num_objs, predictions - -# # # 3. Post-processing -# # label_source = ( -# # self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES -# # ) -# # predictions = [] -# # for ridx, r in enumerate(results): -# # if r.boxes is None or len(r.boxes) == 0: -# # continue - -# # # Move to CPU in one bulk operation per crop -# # boxes = r.boxes.xyxy.cpu().numpy().astype(int) -# # clss = r.boxes.cls.cpu().numpy().astype(int) -# # confs = r.boxes.conf.cpu().numpy() -# # off_x, off_y = cropped_coords[ridx] - -# # for j in range(len(boxes)): -# # num_objs += 1 -# # bx1, by1, bx2, by2 = boxes[j] -# # abs_x1, abs_y1 = off_x + bx1, off_y + by1 -# # class_id = clss[j] -# # bb_color = get_detection_color(class_id, is_bgr=True) - -# # cv2.rectangle( -# # foi, -# # (abs_x1, abs_y1), -# # (off_x + bx2, off_y + by2), -# # bb_color, -# # thickness, -# # ) -# # label = f"{label_source[class_id]} {confs[j]:.2f}" -# # draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) -# # predictions.append( -# # [class_id, abs_x1, abs_y1, off_x + bx2, off_y + by2, confs[j]] -# # ) - -# # return num_objs, predictions - -# def new_contour2predictions(self, frameNum, mask, frame, device_input="cpu"): -# # global all_metadata -# manager, active_streams, all_metadata, send_metadata_queue = get_manager_stuff() -# source = self.source -# stream_name = self.name -# # Find movement areas -# # if cv2.countNonZero(mask) > (mask.size * 0.5): -# # return 0, [] -# contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - -# # Handle Video Writing (Cycle every 10 seconds) -# clip_frameNum = (frameNum - 1) % MAX_FRAMES_PER_CLIP -# print(f"frameNum: {frameNum} ({clip_frameNum})") -# if clip_frameNum == 0: -# if self.video_writer: -# # video_writer.release() -# self.video_writer = release_clip_and_reencode( -# self.clip_key, self.video_writer, self.clip_filename, self.tmp_file, TARGET_FPS -# ) - -# send_metadata_queue.put( -# ( -# self.clip_key, -# self.clip_filename, -# self.frame_width, -# self.frame_height, -# ) -# ) -# self.clip_id += 1 - -# if "://" not in str(source): -# self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{self.clip_id}.mp4" -# else: -# self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{time.time()}.mp4" - -# self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] -# self.clip_key = Path(self.clip_filename).name - -# # timestamp = int(time.time()) -# # filename = f"clip_{timestamp}.mp4" -# self.video_writer = cv2.VideoWriter(self.tmp_file, self.fourcc, TARGET_FPS, (self.frame_width, self.frame_height)) -# main_app_logger.info(f"Started new clip: {self.tmp_file}") - -# # 3. Write frame -# self.video_writer.write(frame) - -# # num_objs = 0 -# # predictions = [] -# metadata = dict() -# if contours: -# # Pass contours directly to the detection logic -# # Skip the 'get_bb_mask' and 'morphologyEx' on full frames -# # num_objs, predictions = -# # self.new_get_detections_for_contours_bbs( -# # queue, -# # frame, -# # contours, -# # device_input=device_input, -# # ) - -# metadata = self.new_get_detections_for_contours_bbs( -# frameNum, frame, contours, thickness=2, device_input=device_input -# ) - -# if metadata: -# all_metadata.setdefault( -# self.clip_key, -# { -# "object": {}, -# "face": {}, -# } -# ) -# all_metadata[self.clip_key]["object"].update(metadata) -# # all_metadata[clip_key]["face"].update(metadata_face) -# # return metadata -# # return num_objs, predictions - -# def test_full_cpu_detection_gpu(self, frame, frameNum): -# # Resize directly into the pre-allocated Pinned Memory -# # This avoids a temporary CPU allocation -# H, W = self.resize_h, self.resize_w -# self.cpu_resized_frame = cv2.resize(frame, (W, H)) - -# # if frameNum == 1: -# # # Do the same for CPU if needed (OpenCV does this internally, but seeding helps) -# # for _ in range(self.backSub_cpu.getNSamples()): -# # self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=1.0) - -# # Background Subtraction on CPU -# fgMask = self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=self.lr) - -# # Skip detection/prediction for the first 10-15 frames of a new stream -# # Just update the background, don't run the rest of the pipeline -# # if frameNum < self.fps/2: -# # return 0, [] - -# prev_bkgd = np.ones_like(fgMask) # AND -# for m in self.mask_history: -# # Dilate the historical mask -# dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) -# cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) -# self.mask_history.append(fgMask) - -# if prev_bkgd.max()!=prev_bkgd.min(): -# combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) - -# # Convert the boolean array back to uint8 with 0 and 255 values -# fgMask = combined_mask_bool.astype(np.uint8) * 255 - -# # Thresholding -# _, mask = cv2.threshold( -# fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY -# ) - -# mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - -# # Get Contours & Run Inference on detection_device -# device_input = ( -# self.operation_device_map.detection_device -# if self.operation_device_map.detection_device == "cpu" -# else "cuda" -# ) - -# # num_objs, predictions = -# self.new_contour2predictions(frameNum, mask, frame, device_input=device_input) - -# # method to start thread -# def start(self): -# self.stopped = False -# self.t = [] -# # self.t.append( -# # self.executor.submit( -# # self.get_frames, -# # ) -# # ) -# self.t.append( -# self.executor.submit( -# send_metadata, -# ) -# ) - -# # method to stop reading frames -# def stop(self): -# for t in as_completed(self.t): -# try: -# _ = t.result() -# except Exception as t_e: -# print(f"[DEBUG] Exception occurred in thread: {t_e}") - -# # self.stopped = True -# self.cap.release() +@dataclass +class PipelineMapping: + resize_device: str.lower = "cpu" + bkgd_subtraction_device: str.lower = "cpu" + threshold_device: str.lower = "cpu" + erodeAndDilate_device: str.lower = "cpu" + contour_device: str.lower = "cpu" + detection_device: str.lower = "cpu" diff --git a/fastapi/main.py b/fastapi/main.py index 8d6266c..6528868 100644 --- a/fastapi/main.py +++ b/fastapi/main.py @@ -9,12 +9,10 @@ # Force FFmpeg to use more threads for decoding import threading import time -import traceback from collections import deque -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path -from random import randint import cv2 import numpy as np @@ -22,8 +20,9 @@ from ultralytics import YOLO from ultralytics.utils.checks import check_imgsz -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request from fastapi.responses import StreamingResponse +from fastapi.templating import Jinja2Templates os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" @@ -32,15 +31,30 @@ # from process_stream import extract_metadata_from_results, release_clip_and_reencode, retry_query from include.utils import ( - UDF_HOST, - UDF_PORT, + CODE_DIR, + CUSTOM_MODEL_FLAG, + DEBUG, + DEBUG_FLAG, + DETECTION_THRESHOLD, + DEVICE, + MODEL_H, + MODEL_NAME, + MODEL_PRECISION, + MODEL_W, + NUM_USUABLE_CPUS, + OMIT_DETECTIONS_FLAG, + SHARED_OUTPUT, + TARGET_FPS, + TMP_LOCATION, YOLO_CLASS_NAMES, PipelineMapping, draw_label, filter_contained_boxes, get_detection_color, + get_display_frame_in_bytes, + manual_fps_calculation, merge_boxes_limit, - retry_query, + metadata2vdms, ) # ----- SETUP LOGGING ----- @@ -55,44 +69,20 @@ # ----- SPECIAL VARIABLES ----- -def str2bool(in_val): - if isinstance(in_val, bool): - return in_val - - if not isinstance(in_val, str): - raise ValueError(f"{in_val} is not a bool or string") - - if in_val.title() == "True": - return True - else: - return False - - CLIP_DURATION = 10 # seconds -CODE_DIR = os.getenv("CODE_DIR", "/home") -CUSTOM_MODEL_FLAG = str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)) -DBHOST = os.getenv("DBHOST", "vdms-service") -DEBUG = os.getenv("DEBUG", "0") -DEBUG_FLAG = True if DEBUG == "1" else False -DETECTION_THRESHOLD = 0.25 -DEVICE = os.getenv("DEVICE", "CPU") -TARGET_FPS = 15 -FRAME_INTERVAL = 1.0 / TARGET_FPS # ~0.0667 seconds -INGESTION = os.getenv("INGESTION", "object,face") KERNEL_RATIO = 0.05 # 0.03 # .05 # .025 MASK_MAX_VALUE = 255 MASK_THRESHOLD_VALUE = 127 MAX_DETECTIONS = 100 -MAX_FRAMES_PER_CLIP = int(TARGET_FPS * CLIP_DURATION) # 150 frames -MODEL_NAME = os.getenv("MODEL_NAME", "yolo11n") -MODEL_PRECISION = "FP16" -MODEL_W, MODEL_H = (640, 640) -NUM_USUABLE_CPUS = 2 -OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) -SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") -Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) -TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) -TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") +# MAX_FRAMES_PER_CLIP = int(TARGET_FPS * CLIP_DURATION) # 150 frames +# MODEL_PRECISION = "FP16" +# MODEL_W, MODEL_H = (640, 640) +# NUM_USUABLE_CPUS = 2 +# OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) +# SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") +# Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) +# TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) +# TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") if CUSTOM_MODEL_FLAG: model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" @@ -113,156 +103,6 @@ def str2bool(in_val): # ----- INGESTION FUNCTIONS ----- - - -# Manual FPS calculation if OpenCV reports 0 -def manual_fps_calculation(src, num_frames=10): - vid_obj = cv2.VideoCapture(src) - - frame_count = 0 - start_t = time.time() - - while frame_count < num_frames: - grabbed, frame = vid_obj.read() - - if not grabbed: - break - - frame_count += 1 - - end_t = time.time() - vid_obj.release() - - elapsed_t = end_t - start_t - - if elapsed_t > 0: - return frame_count / elapsed_t - else: - return 0 - - -# Generate and run UDF query -def get_udf_query( - filename_path, - properties, - ingest_mode, - new_size, - id="udf_metadata", - metadata=None, - test_mode=TEST_MODE, -): - query = { - "AddVideo": { - "from_file_path": str(filename_path), # from_server_file - "is_local_file": True, - "properties": properties, - "operations": [ - { - "type": "syncremoteOp", # "remoteOp", - "url": f"http://{UDF_HOST}:{UDF_PORT}/video", - "options": { - "id": id, - "otype": ingest_mode, - "media_type": "video", - "input_sizeWH": new_size, - "filename": properties["Name"], - "ingestion": 1, - }, - } - ], - } - } - - if id == "udf_metadata" and metadata is not None: - query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata - - if test_mode: - return - - filename = str(Path(filename_path).name) - if DEBUG_FLAG: - print( - f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), - flush=True, - ) - try: - res = retry_query([query], sleep_timer=randint(1, 5)) - - if DEBUG_FLAG: - print( - f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), - flush=True, - ) - print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) - print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) - except Exception: - e = traceback.format_exc() - print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) - - -def _sort_dict_by_frame(in_dict): - def _by_int(key): - return tuple(int(k) for k in key.split("_")) - - return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) - - -# method to send metadata to VDMS once clip is saved -def metadata2vdms( - clip_key, - clip_filename, - clip_metadata, - width, - height, -): - if DEBUG == "1": - print( - f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", - flush=True, - ) - - # Send metadata to UDF - properties = { - "Name": clip_key, # .split("/")[-1], - "category": "video_path_rop", - } - - combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} - if "face" in clip_metadata: - for face_frameidx_bbidx, value in clip_metadata["face"].items(): - face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") - max_obj_idx = 0 - for obj_frameidx_bbidx in combined_metadata: - if face_frameidx in obj_frameidx_bbidx: - _, obj_bbidx_ = obj_frameidx_bbidx.split("_") - max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) - - if max_obj_idx > 0: - new_face_bbidx = max_obj_idx + 1 - new_key = f"{face_frameidx}_{new_face_bbidx:04d}" - combined_metadata[new_key] = value - combined_metadata[new_key]["bbId"] = new_key - else: - combined_metadata[face_frameidx_bbidx] = value - - combined_metadata = _sort_dict_by_frame(combined_metadata) - get_udf_query( - clip_filename, - properties, - INGESTION.replace(",", "+"), - (width, height), - id="udf_metadata", - metadata=combined_metadata, - test_mode=TEST_MODE, - ) - - if DEBUG == "1": - print( - f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", - flush=True, - ) - - # method to create clips (read frame write to file; add name to list) def send_metadata(): global all_metadata @@ -291,21 +131,11 @@ def send_metadata(): pass -# --------------- APP ------------------- -from contextlib import asynccontextmanager - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # This is the ONLY place this should be initialized - if not hasattr(app.state, "active_streams"): - app.state.active_streams = {} - print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") - yield - # Cleanup logic here... - - -app = FastAPI(lifespan=lifespan) +def handle_done(future): + try: + future.result() + except Exception as e: + print(f"Task error: {e}") def save_and_finalize_clip( @@ -373,8 +203,12 @@ def __init__(self, source, name): # Set a timeout so it doesn't hang self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) + # self.stat_start_time = time.perf_counter() + self.stat_frame_count = 0 + self.stat_fps = 0 + self.video_writer = None - self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") + self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v self.clip_id = 0 self.clip_filename = "" self.clip_key = "" @@ -384,6 +218,8 @@ def __init__(self, source, name): self.frame = None self.latest_processed_frame = None self.last_write_time = time.time() + self.last_frame_id = 0 # Increment this in your DETECTION loop + self.sent_frame_id = -1 # Track what the BROWSER has already seen self.resize_h, self.resize_w = [MODEL_H, MODEL_W] self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) @@ -437,23 +273,117 @@ def __init__(self, source, name): # Create ThreadPoolExecutor self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) + self.metadata_thread = threading.Thread(target=self.send_metadata, daemon=True) self.thread = threading.Thread(target=self.update, daemon=True) self.process_thread = threading.Thread(target=self.run_inference, daemon=True) self.start() + def start(self): + # self.t = [] + # self.t.append( + # self.executor.submit( + # send_metadata, + # ) + # ) + self.metadata_thread.start() + self.thread.start() + self.process_thread.start() + + # def stop(self): + # self.active = False + # if self.video_writer: + # self.release_clip_and_reencode() + # # for t in as_completed(self.t): + # # try: + # # _ = t.result() + # # except Exception as t_e: + # # print(f"[DEBUG] Exception occurred in thread: {t_e}") + # send_metadata_queue.put(None) + # self.metadata_thread.join() + + # self.executor.shutdown(wait=True, cancel_futures=False) + # # self.thread.join() + # # self.process_thread.join() + # self.cap.release() + + def stop(self): + self.active = False # Signals the while loops to exit + + # Release the VideoWriter if it exists + if self.video_writer: + self.release_clip_and_reencode() + + # Close the OpenCV capture + if self.cap: + self.cap.release() + + # Join threads if you want to be 100% sure they are closed + # self.thread.join(timeout=1.0) + # self.process_thread.join(timeout=1.0) + + def update_frame(self): + self.stat_frame_count += 1 + elapsed = time.perf_counter() - self.stat_start_time + if elapsed > 0.0: # Update FPS every second + self.stat_fps = self.stat_frame_count / elapsed + # To keep it "real-time" and not a lifetime average, reset: + # self.stat_start_time = time.perf_counter() + # self.stat_frame_count = 0 + + def send_metadata(self): + # This loop runs in its own dedicated threading.Thread + while True: + try: + # Blocks until something is in the queue + queue_details = send_metadata_queue.get() + + if queue_details is None: # Sentinel to shut down + break + + (clip_key, clip_filename, width, height) = queue_details + + clip_data = all_metadata.get(clip_key) + + if clip_data: + # Use the EXECUTOR to fire off the heavy metadata sending + # This returns immediately so the loop can grab the next item + future = self.executor.submit( + metadata2vdms, + clip_key, + clip_filename, + clip_data, + width, + height, + ) + # Track success + future.add_done_callback(handle_done) + + # Clean up dict entry after submitting to the thread + # Note: If metadata2vdms needs the data, pass it in (as done above) + del all_metadata[clip_key] + + # Mark the task as done in the queue + send_metadata_queue.task_done() + + except Exception as e: + print(f"Queue Error: {e}") + # Gets video fps and framecount def get_fps_and_framecnt(self): self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps if self.input_fps == 0: # Case when FPs isn't available - self.input_fps = manual_fps_calculation(self.stream_id, num_frames=10) + self.input_fps = manual_fps_calculation(self.name, num_frames=10) self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps self.frame_skip = int(self.input_fps / self.target_fps) if self.frame_skip < 1: self.frame_skip = 1 + self.MAX_FRAMES_PER_CLIP = int(self.target_fps * CLIP_DURATION) + self.target_interval = 1.0 / self.target_fps # 0.0666s + print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) @@ -477,243 +407,307 @@ def get_frameWH(self): self.width = new_sizeWH[0] self.height = new_sizeWH[1] - # def run_inference(self): - # # # streamer = VideoStreamHandler(source_url, name) + def run_inference(self): + print(f"Inference thread for {self.name} started...") - # # # Skip frames if they aren't fresh - # last_frame_time = time.time() - # frame_counter = 0 - # print(f"Inference thread for {self.name} started...") + # 1. Initialize the start time and a counter for frames actually written + start_time = time.time() + self.stat_start_time = time.perf_counter() + total_frames_written = 0 - # while self.active: - # current_time = time.time() - # # Only process if 66ms has passed - # if current_time - last_frame_time < FRAME_INTERVAL: - # time.sleep(0.001) - # continue + while self.active: + # 2. Calculate how many frames SHOULD be in the file by now + elapsed_real_time = time.time() - start_time + expected_total_frames = int(elapsed_real_time * self.target_fps) - # # 1. Get frame from the update() thread - # frame = self.get_frame() - # if frame is None: - # time.sleep(0.01) - # continue + # 3. Determine the "Gap": How many frames do we need to write to stay in sync? + # If processing took 200ms, slots_to_fill will be ~3. + slots_to_fill = expected_total_frames - total_frames_written - # # if success: - # try: - # frame_bytes = self.test_full_cpu_detection_gpu(frame, frame_counter + 1) - # if frame_bytes is not None: - # frame_counter += 1 - # self.latest_processed_frame = frame_bytes - # else: - # print("DEBUG: test_full_cpu_detection_gpu returned None") + # if slots_to_fill > 0: + slots_to_fill = 1 + frame = self.get_frame() + if frame is None: + time.sleep(0.01) + continue + # Handle Video Writing (Cycle every 10 seconds) + # clip_frameNum = (expected_total_frames - 1) % self.MAX_FRAMES_PER_CLIP + clip_frameNum = self.stat_frame_count % self.MAX_FRAMES_PER_CLIP + if clip_frameNum == 0 or total_frames_written == 0: + print(f"frameNum: {expected_total_frames} ({clip_frameNum})") + if self.video_writer: + self.release_clip_and_reencode() + if "://" not in str(self.source): + self.clip_filename = ( + f"{SHARED_OUTPUT}/{self.name}_{self.clip_id}.mp4" + ) + else: + self.clip_filename = ( + f"{SHARED_OUTPUT}/{self.name}_{time.time()}.mp4" + ) - # # Reset frame to signal we are ready for the next one - # last_frame_time = current_time - # self.frame = None + self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] + self.clip_key = Path(self.clip_filename).name - # except Exception as e: - # print(f"Inference Error: {e}") + # timestamp = int(time.time()) + # filename = f"clip_{timestamp}.mp4" + self.video_writer = cv2.VideoWriter( + self.tmp_file, + self.fourcc, + self.target_fps, + (MODEL_W, MODEL_H), + # (self.width, self.height) + # (self.frame_width, self.frame_height), + ) + main_app_logger.info(f"Started new clip: {self.tmp_file}") - # time.sleep(0.001) + try: + # 4. PASS THE REPEAT COUNT to your function + # This ensures the video length matches the stopwatch + frame_bytes = self.test_full_cpu_detection_gpu( + frame, self.stat_frame_count + 1, repeat_count=slots_to_fill + ) + if frame_bytes is None: + print( + f"CRITICAL: {self.name} detection returned NULL bytes. Check OpenVINO logs." + ) + else: + print( + f"SUCCESS: {self.name} pushed {len(frame_bytes)} bytes to memory." + ) + self.latest_processed_frame = frame_bytes + self.last_heartbeat = time.time() + self.last_frame_id += 1 + total_frames_written = expected_total_frames + self.frame = None + self.update_frame() + except Exception as e: + print(f"Inference Error: {e}") + # else: + # # 6. We are ahead of the clock; wait for the next 66ms window + # # time.sleep(0.005) + # pass # def run_inference(self): - # frames_written = 0 - # frame_counter = 0 # print(f"Inference thread for {self.name} started...") - # last_frame_time = time.time() + # # This counter now perfectly represents a 15fps clock + # frame_counter = 0 + # while self.active: - # # elapsed_t = time.time() - last_frame_time - # # expected_frames = int(elapsed_t * TARGET_FPS) + # frame = self.get_frame() + # if frame is None: + # time.sleep(0.005) + # continue - # # Only process if 66ms has passed - # if frame_counter + 1 > frames_written: - # print(f"expected_frames > frames_written: {frame_counter + 1} > {frames_written}") - # frame = self.get_frame() - # if frame is None: - # time.sleep(0.01) - # continue + # # frame_counter will now hit exactly 150 every 10 seconds + # frame_counter += 1 + + # # Handle Video Writing (Cycle every 10 seconds) + # clip_frameNum = (frame_counter - 1) % self.MAX_FRAMES_PER_CLIP + # if clip_frameNum == 0: + # print(f"frameNum: {frame_counter} ({clip_frameNum})") + # if self.video_writer: + # self.release_clip_and_reencode() + # if "://" not in str(self.source): + # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{self.clip_id}.mp4" + # else: + # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{time.time()}.mp4" + + # self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] + # self.clip_key = Path(self.clip_filename).name + + # # timestamp = int(time.time()) + # # filename = f"clip_{timestamp}.mp4" + # self.video_writer = cv2.VideoWriter( + # self.tmp_file, + # self.fourcc, + # self.target_fps, + # (MODEL_W, MODEL_H), + # # (self.width, self.height) + # # (self.frame_width, self.frame_height), + # ) + # main_app_logger.info(f"Started new clip: {self.tmp_file}") # try: - # frameNum = frame_counter + 1 - # frame_bytes = self.test_full_cpu_detection_gpu(frame, frameNum ) - # frames_written = frameNum - # self.latest_processed_frame = frame_bytes - # self.frame = None - # frame_counter += 1 + + # # This triggers your 10s clip rotation (frameNum % 150 == 0) + # self.test_full_cpu_detection_gpu(frame, frame_counter) + + # # self.latest_processed_frame = frame_bytes # From your detection function + # self.frame = None # Signal Reader for next frame # except Exception as e: # print(f"Inference Error: {e}") - # time.sleep(0.001) - # def run_inference(self): + # print(f"Inference thread for {self.name} started...") + + # target_fps = 15.0 + # target_interval = 1.0 / target_fps # 0.0666s + # start_time = time.time() - # frames_accounted_for = 0 + # total_frames_written = 0 + + # # Store the last frame to use as a "filler" if we lag + # last_processed_annotated = None # while self.active: - # current_time = time.time() - # elapsed_real_time = current_time - start_time - # expected_total_frames = int(elapsed_real_time * self.target_fps) + # # 1. Calculate how many frames SHOULD be in the file by now + # elapsed_real_time = time.time() - start_time + # expected_total_frames = int(elapsed_real_time * target_fps) - # # How many 15fps 'slots' have passed since we last processed? - # # If processing took 133ms, this will be 2. - # frames_to_write = expected_total_frames - frames_accounted_for + # # 2. Are we behind the clock? + # if expected_total_frames > total_frames_written: + # # How many frames do we need to write to CATCH UP to the clock? + # frames_to_catch_up = expected_total_frames - total_frames_written - # if frames_to_write > 0: # frame = self.get_frame() - # if frame is None: + # if frame is not None: + + # # Handle Video Writing (Cycle every 10 seconds) + # clip_frameNum = (expected_total_frames - 1) % self.MAX_FRAMES_PER_CLIP + # if clip_frameNum == 0: + # print(f"frameNum: {expected_total_frames} ({clip_frameNum})") + # if self.video_writer: + # self.release_clip_and_reencode() + # if "://" not in str(self.source): + # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{self.clip_id}.mp4" + # else: + # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{time.time()}.mp4" + + # self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] + # self.clip_key = Path(self.clip_filename).name + + # # timestamp = int(time.time()) + # # filename = f"clip_{timestamp}.mp4" + # self.video_writer = cv2.VideoWriter( + # self.tmp_file, + # self.fourcc, + # self.target_fps, + # (MODEL_W, MODEL_H), + # # (self.width, self.height) + # # (self.frame_width, self.frame_height), + # ) + # main_app_logger.info(f"Started new clip: {self.tmp_file}") + # try: + # # 3. Process the latest frame (YOLO/KNN) + # # This might take 100ms (more than one 66ms tick) + # self.test_full_cpu_detection_gpu(frame, expected_total_frames) + # # self.latest_processed_frame = frame_bytes + + # # 4. WRITER SYNC: + # # If we missed 3 'ticks' while processing, write this frame 3 times. + # # This is the "Brake" that stops the fast-forward. + # if self.video_writer: + # for _ in range(frames_to_catch_up): + # self.video_writer.write(self.cpu_resized_frame) + + # total_frames_written = expected_total_frames + # self.frame = None + # except Exception as e: + # print(f"Inference Error: {e}") + # else: + # # No new frame from reader yet, but we MUST keep the clock moving + # # Write the previous frame again to maintain video duration + # if self.video_writer and total_frames_written > 0: + # for _ in range(frames_to_catch_up): + # self.video_writer.write(self.cpu_resized_frame) + # total_frames_written = expected_total_frames # time.sleep(0.01) - # continue - - # try: - # # PASS THE REPEAT COUNT to your function - # frame_bytes = self.test_full_cpu_detection_gpu( - # frame, - # expected_total_frames, - # repeat_count=frames_to_write - # ) - - # frames_accounted_for = expected_total_frames - # self.latest_processed_frame = frame_bytes - # self.frame = None - # except Exception as e: - # print(f"Inference Error: {e}") # else: - # time.sleep(0.005) # Wait for the next 66ms slot - - def run_inference(self): - print(f"Inference thread for {self.name} started...") - - # 1. Initialize the start time and a counter for frames actually written - start_time = time.time() - total_frames_written = 0 - target_fps = 15.0 # This must match your VideoWriter TARGET_FPS - - while self.active: - # 2. Calculate how many frames SHOULD be in the file by now - elapsed_real_time = time.time() - start_time - expected_total_frames = int(elapsed_real_time * target_fps) - - # 3. Determine the "Gap": How many frames do we need to write to stay in sync? - # If processing took 200ms, slots_to_fill will be ~3. - slots_to_fill = expected_total_frames - total_frames_written - - if slots_to_fill > 0: - frame = self.get_frame() - if frame is None: - time.sleep(0.01) - continue - - try: - # 4. PASS THE REPEAT COUNT to your function - # This ensures the video length matches the stopwatch - self.test_full_cpu_detection_gpu( - frame, expected_total_frames, repeat_count=slots_to_fill - ) - - # 5. Sync the counter - total_frames_written = expected_total_frames - self.frame = None - except Exception as e: - print(f"Inference Error: {e}") - else: - # 6. We are ahead of the clock; wait for the next 66ms window - time.sleep(0.005) + # # 5. We are ahead of the clock (CPU is fast); wait for the next 66ms tick + # time.sleep(0.005) # def update(self): - # while self.active: - # if not self.cap.isOpened(): - # print(f"[ERROR] Camera {self.name} lost connection.") - # self.active = False - # break - - # # # 1. Check if the frame was successfully grabbed - # # grabbed = self.cap.grab() - # # if not grabbed: - # # print(f"[INFO] Stream ended or disconnected: {self.name}") - # # self.active = False # <--- TRIGGER SHUTDOWN - # # break - - # # # 2. Only retrieve if main loop is ready - # # if self.frame is None: - # # success, frame = self.cap.retrieve() - # # if not success: - # # self.active = False - # # break - # # self.frame = frame - - # # time.sleep(0.001) - # success, frame = self.cap.read() - # if success: - # self.frame = frame - # # print("READER THREAD: Frame captured") # Silent once working - # else: - # # print("READER THREAD: Failed to read from file!") - # # time.sleep(1) - # self.active = False - # break - - # # 3. Clean up hardware handles automatically - # # self.stop() + # # Calculate exactly how much time should pass between 15fps frames + # # target_interval = 1.0 / self.target_fps # 0.0666s + # last_grab_time = time.time() - # def update(self): - # print(f"READER THREAD: Started for {self.name}") # while self.active: - # if not self.cap.isOpened(): - # self.active = False - # break - - # # 1. Calculate how many frames to SKIP - # # We want to skip 'self.frame_skip - 1' frames - # for _ in range(self.frame_skip - 1): - # # grab() is 5x faster than read() because it doesn't decode - # if not self.cap.grab(): - # self.active = False - # return - - # # 2. Only decode (read/retrieve) the ONE frame we actually want - # success, frame = self.cap.read() - + # # 1. Fast-forward the internal buffer using grab() + # # This clears out the 21fps 'junk' frames + # success = self.cap.grab() # if not success: - # print(f"READER THREAD: {self.name} reached end of file.") # self.active = False # break - # # 3. Hand the decoded frame to the inference thread - # # No 'if skip_frame_num' logic needed here anymore - # self.frame = frame + # # 2. Check if enough time has passed to 'retrieve' a 15fps frame + # current_time = time.time() + # if current_time - last_grab_time >= self.target_interval: + # success, frame = self.cap.retrieve() + # if success: + # self.frame = frame + # last_grab_time = current_time - # # 4. Tiny sleep to prevent this thread from starving the YOLO thread # time.sleep(0.001) def update(self): - # Calculate exactly how much time should pass between 15fps frames - target_interval = 1.0 / self.target_fps # 0.0666s - last_grab_time = time.time() + print(f"READER THREAD: Started for {self.name}") + # The 'step' is 1.4 (21 in / 15 out) + step = self.input_fps / self.target_fps + # This tracks where the next 'keeper' frame is in the 21fps timeline + next_keeper_idx = 0.0 + current_idx = 0 while self.active: - # 1. Fast-forward the internal buffer using grab() - # This clears out the 21fps 'junk' frames - success = self.cap.grab() + if not self.cap.isOpened(): + self.active = False + break + + # 1. Skip (grab) frames until we reach the next 15fps 'slot' + while current_idx < int(next_keeper_idx): + if not self.cap.grab(): + self.active = False + return + current_idx += 1 + + # 2. Retrieve (decode) the keeper frame + success, frame = self.cap.read() # read() includes grab + retrieve if not success: self.active = False break - # 2. Check if enough time has passed to 'retrieve' a 15fps frame - current_time = time.time() - if current_time - last_grab_time >= target_interval: - success, frame = self.cap.retrieve() - if success: - self.frame = frame - last_grab_time = current_time + current_idx += 1 + # Advance the 'keeper' mark by 1.4 + next_keeper_idx += step + + # 3. Hand off the frame to the inference thread + self.frame = frame + # time.sleep(0.001) - time.sleep(0.001) + # --- AUTO-CLEANUP --- + # Remove itself from the global dictionary so the dashboard knows it's gone + if self.name in app.state.active_streams: + app.state.active_streams[self.name].stop() + del app.state.active_streams[self.name] + + # def update(self): + # # 1/15 = 0.066s interval + # target_interval = 1.0 / self.target_fps + # last_yield_time = time.time() + + # while self.active: + # # 1. Grab (don't decode) as fast as possible to clear the buffer + # success = self.cap.grab() + # if not success: + # self.active = False + # break + + # # 2. Only retrieve (decode) if 66ms has passed + # current_time = time.time() + # if current_time - last_yield_time >= target_interval: + # success, frame = self.cap.retrieve() + # if success: + # self.frame = frame + # last_yield_time = current_time + + # time.sleep(0.001) def get_frame(self): return self.frame - def new_get_detections_for_contours_bbs( + def get_detections_for_contours_bbs( self, frameNum, foi, contours, thickness=2, device_input="cuda" ): # global active_streams @@ -768,18 +762,9 @@ def new_get_detections_for_contours_bbs( cropped_coords.append((x1, y1)) if not cropped_imgs: - if self.frame_width > 1280: - display_frame = cv2.resize( - foi, (1280, 720), interpolation=cv2.INTER_AREA - ) - _, buffer = cv2.imencode( - ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 50] - ) - else: - _, buffer = cv2.imencode( - ".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), 50] - ) - frame_bytes = buffer.tobytes() + frame_bytes = get_display_frame_in_bytes( + foi, self.frame_width, display_size=(1280, 720), quality=50 + ) return metadata, frame_bytes # num_objs, predictions # 2. Inference (Keep stream=False as it is stable) @@ -891,17 +876,9 @@ def new_get_detections_for_contours_bbs( } # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) - if self.frame_width > 1280: - display_frame = cv2.resize(foi, (1280, 720), interpolation=cv2.INTER_AREA) - ret, buffer = cv2.imencode( - ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 50] - ) - else: - ret, buffer = cv2.imencode(".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), 50]) - if ret: - frame_bytes = buffer.tobytes() - else: - frame_bytes = None + frame_bytes = get_display_frame_in_bytes( + foi, self.frame_width, display_size=(1280, 720), quality=50 + ) return metadata, frame_bytes @@ -926,50 +903,24 @@ def release_clip_and_reencode(self): self.video_writer = None self.clip_id += 1 - def new_contour2predictions( + def contour2predictions( self, frameNum, mask, frame, device_input="cpu", repeat_count=1 ): - source = self.source - stream_name = self.name + # source = self.source + # stream_name = self.name contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - # Handle Video Writing (Cycle every 10 seconds) - clip_frameNum = (frameNum - 1) % MAX_FRAMES_PER_CLIP - if clip_frameNum == 0: - print(f"frameNum: {frameNum} ({clip_frameNum})") - if self.video_writer: - self.release_clip_and_reencode() - if "://" not in str(source): - self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{self.clip_id}.mp4" - else: - self.clip_filename = f"{SHARED_OUTPUT}/{stream_name}_{time.time()}.mp4" - - self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] - self.clip_key = Path(self.clip_filename).name - - # timestamp = int(time.time()) - # filename = f"clip_{timestamp}.mp4" - self.video_writer = cv2.VideoWriter( - self.tmp_file, - self.fourcc, - self.target_fps, - (MODEL_W, MODEL_H), - # (self.width, self.height) - # (self.frame_width, self.frame_height), - ) - main_app_logger.info(f"Started new clip: {self.tmp_file}") - # 3. Write frame # self.video_writer.write(frame) - if self.video_writer: - for _ in range(repeat_count): - self.video_writer.write(self.cpu_resized_frame) + # if self.video_writer: + # for _ in range(repeat_count): + # self.video_writer.write(self.cpu_resized_frame) # num_objs = 0 # predictions = [] metadata = dict() if contours: - metadata, frame_bytes = self.new_get_detections_for_contours_bbs( + metadata, frame_bytes = self.get_detections_for_contours_bbs( frameNum, frame, contours, thickness=2, device_input=device_input ) @@ -990,6 +941,7 @@ def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): # This avoids a temporary CPU allocation H, W = self.resize_h, self.resize_w self.cpu_resized_frame = cv2.resize(frame, (W, H)) + self.video_writer.write(self.cpu_resized_frame) # Background Subtraction on CPU fgMask = self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=self.lr) @@ -1022,37 +974,54 @@ def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): ) # num_objs, predictions = - frame_bytes = self.new_contour2predictions( + frame_bytes = self.contour2predictions( frameNum, mask, frame, device_input=device_input, repeat_count=repeat_count ) return frame_bytes - def start(self): - self.t = [] - self.t.append( - self.executor.submit( - send_metadata, - ) - ) - self.thread.start() - self.process_thread.start() - - def stop(self): - self.active = False - for t in as_completed(self.t): - try: - _ = t.result() - except Exception as t_e: - print(f"[DEBUG] Exception occurred in thread: {t_e}") - - self.cap.release() - class StreamRequest(BaseModel): url: str name: str +# --------------- APP ------------------- +from contextlib import asynccontextmanager + + +async def auto_cleanup_janitor(app): + while True: + await asyncio.sleep(10) + now = time.time() + # Iterating over a list of keys to avoid "dictionary changed size" error + for name in list(app.state.active_streams.keys()): + streamer = app.state.active_streams[name] + + # Check if the stream is marked inactive OR timed out + # streamer.active should be False when the video source ends + if not streamer.active or (now - streamer.last_heartbeat > 30): + print(f"CLEANUP: Removing {name} from active_streams") + streamer.stop() + del app.state.active_streams[name] + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # This is the ONLY place this should be initialized + if not hasattr(app.state, "active_streams"): + app.state.active_streams = {} + asyncio.create_task(auto_cleanup_janitor(app)) + print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") + yield + # Cleanup logic here... + for s in app.state.active_streams.values(): + s.stop() + + +app = FastAPI(lifespan=lifespan) +templates = Jinja2Templates(directory="templates") + + @app.post("/stream") async def stream_video( # url: str = Query(..., description="RTSP URL or Local File Path"), @@ -1074,36 +1043,38 @@ async def stream_video( return {"status": "started", "keys": list(app.state.active_streams.keys())} -@app.get("/view_stream") -async def view_stream(name: str): - # DEBUG START - curr_keys = list(app.state.active_streams.keys()) - print( - f"view_stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" - ) - # DEBUG END - # This will now find 'test_vid' because it's the same memory! - streamer = app.state.active_streams.get(name) +# @app.get("/view_stream", name="view_stream") +# async def view_stream(name: str): +# # DEBUG START +# curr_keys = list(app.state.active_streams.keys()) +# print( +# f"view_stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" +# ) +# # DEBUG END +# # This will now find 'test_vid' because it's the same memory! +# streamer = app.state.active_streams.get(name) - if not streamer: - raise HTTPException(status_code=404, detail="Stream not found") +# if not streamer: +# raise HTTPException(status_code=404, detail="Stream not found") - async def get_frames(): - while streamer.active: - frame_bytes = streamer.latest_processed_frame - if frame_bytes is None: - print(f"DEBUG: {streamer.name} frame is still None...") - await asyncio.sleep(0.1) # Wait for first inference to finish - continue +# async def get_frames(): +# while streamer.active: +# frame_bytes = streamer.latest_processed_frame +# if frame_bytes is None: +# print(f"DEBUG: {streamer.name} frame is still None...") +# await asyncio.sleep(0.1) # Wait for first inference to finish +# continue - yield ( - b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n" - ) - await asyncio.sleep(0.06) +# streamer.update_frame() - return StreamingResponse( - get_frames(), media_type="multipart/x-mixed-replace; boundary=frame" - ) +# yield ( +# b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n" +# ) +# await asyncio.sleep(0.06) + +# return StreamingResponse( +# get_frames(), media_type="multipart/x-mixed-replace; boundary=frame" +# ) @app.get("/debug_frame/{name}") @@ -1126,6 +1097,133 @@ async def debug_frame(name: str): } +@app.get("/stream_list") +async def get_stream_list(request: Request): + """Returns a list of currently active stream names.""" + return list(request.app.state.active_streams.keys()) + + +# New endpoint to provide stats +@app.get("/stream_stats") +async def get_stats(request: Request): + # Return a dict mapping camera_id to its metrics + return { + cam_id: {"fps": round(state.stat_fps, 1), "frames": state.stat_frame_count} + for cam_id, state in request.app.state.active_streams.items() + } + + +@app.get("/") +async def index(request: Request): + """Renders the dashboard.""" + print(f"Active Streams: {app.state.active_streams.keys()}") # Check your terminal! + curr_keys = list(app.state.active_streams.keys()) + return templates.TemplateResponse( + "index.html", {"request": request, "cameras": curr_keys} + ) + + +# @app.get("/view_stream", name="view_stream") +# async def view_stream(name: str, request: Request): +# # Initialize state if it doesn't exist +# # if name not in app.state.active_streams: +# # app.state.active_streams[name] = CameraState() + +# async def frame_generator(): +# try: +# while True: +# # Check if client disconnected to stop processing immediately +# if await request.is_disconnected(): # +# break + +# frame_bytes = app.state.active_streams[name].latest_processed_frame +# if frame_bytes is None: +# print(f"DEBUG: {app.state.active_streams[name].name} frame is still None...") +# await asyncio.sleep(0.1) # Wait for first inference to finish +# continue + +# app.state.active_streams[name].update_frame() + +# # app.state.active_streams[name].frame_count += 1 +# yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n') +# finally: +# # THIS IS THE FIX: Remove the camera from the list when stream ends +# print(f"Stream ended: {name}. Cleaning up.") +# if name in app.state.active_streams: +# del app.state.active_streams[name] + +# return StreamingResponse(frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame") + + +@app.get("/view_stream", name="view_stream") +async def view_stream(name: str, request: Request): + if name not in request.app.state.active_streams: + raise HTTPException(status_code=404, detail="Stream not found") + streamer = request.app.state.active_streams.get(name) + if not streamer: + raise HTTPException(status_code=404) + + async def frame_generator(): + # try: + while streamer.active: + if await request.is_disconnected(): + break + # 2. Update Heartbeat for Auto-Cleanup + # streamer.last_heartbeat = time.time() + # 3. Only send a frame if a NEW one is ready + if streamer.latest_processed_frame: + # streamer.latest_processed_frame must be raw JPEG bytes + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n\r\n" + + streamer.latest_processed_frame + + b"\r\n" + ) + + # ONLY process if there is a NEW frame from the detector + # if streamer.last_frame_id > streamer.sent_frame_id: + # frame_bytes = streamer.latest_processed_frame + # if frame_bytes: + # # Sync IDs so we don't send this one again + # streamer.sent_frame_id = streamer.last_frame_id + + # # Now this count is HONEST: 1 count = 1 unique AI frame + # streamer.update_frame() + + # yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + + # frame_bytes + b"\r\n") + + # Tiny sleep (1ms) to prevent 100% CPU usage while waiting + # for the next unique frame to arrive from the detector. + await asyncio.sleep(0.001) + + # finally: + # if name in request.app.state.active_streams: + # del request.app.state.active_streams[name] + + return StreamingResponse( + frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame" + ) + + +@app.post("/stop_stream/{name}") # or @app.delete +async def stop_stream(name: str, request: Request): + """Gracefully stops a background stream and cleans up memory.""" + streamer = request.app.state.active_streams.get(name) + + if not streamer: + raise HTTPException(status_code=404, detail=f"Stream '{name}' not found.") + + # 1. Trigger the internal stop (releases CV2 cap and joins threads) + streamer.stop() + + # 2. Remove from the shared state + del request.app.state.active_streams[name] + + print(f"--- CLEANUP | Stream '{name}' stopped and removed. ---") + return {"status": "stopped", "camera": name} + + if __name__ == "__main__": import uvicorn diff --git a/fastapi/templates/index.html b/fastapi/templates/index.html new file mode 100644 index 0000000..57b942f --- /dev/null +++ b/fastapi/templates/index.html @@ -0,0 +1,184 @@ + + + + + + AI Detection Dashboard + + + + +

Live Detection Dashboard

+ + +
+ No Active Streams Detected +
+ +
+ {% for id in cameras %} +
+

Camera: {{ id }}

+
+
FPS: 0.0
+
Frames: 0
+
+
+ Camera Stream {{ id }} +
+
+ {% endfor %} +
+ + + + \ No newline at end of file diff --git a/video/nginx.conf b/video/nginx.conf index c9150d3..357d154 100644 --- a/video/nginx.conf +++ b/video/nginx.conf @@ -61,20 +61,20 @@ http { root /var/www; } - location / { - # proxy_http_version 1.1; - - # Mandatory for WebSocket support - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Standard proxy headers - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 86400; - } + # location / { + # # proxy_http_version 1.1; + + # # Mandatory for WebSocket support + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + + # # Standard proxy headers + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_read_timeout 86400; + # } } } From 4218dca2d7ded6a1457fccd206f741f3a4adfd66 Mon Sep 17 00:00:00 2001 From: "Lacewell, Chaunte W" Date: Tue, 24 Mar 2026 12:02:20 -0700 Subject: [PATCH 03/20] Latest for filtering demo; further optimizations needed to exceed 11 fps Signed-off-by: Lacewell, Chaunte W --- .gitignore | 5 +- deployment/docker-swarm/build.sh | 2 +- deployment/docker-swarm/video.m4 | 7 +- fastapi/Dockerfile | 160 ++- fastapi/build.sh | 5 - fastapi/entrypoint.sh | 13 + fastapi/include/handlers.py | 961 ++++++++++++++ fastapi/include/utils.py | 87 +- fastapi/main.py | 1150 +---------------- fastapi/manage.sh | 6 + fastapi/requirements.GPU.txt | 6 - fastapi/requirements.txt | 7 - .../resources/models/download_yolo.py | 12 +- .../resources/models/models.lst | 0 .../ultralytics/custom_models/.gitignore | 0 fastapi/templates/index.html | 222 +++- finetune/.env | 1 + finetune/Dockerfile | 102 ++ finetune/app/finetune.py | 205 +++ finetune/app/include/utils.py | 132 ++ finetune/app/requirements.txt | 25 + finetune/docker-compose.yml | 64 + start_app.sh | 2 +- video/Dockerfile | 32 +- video/manage.sh | 2 +- video/source_watcher.py | 317 +++++ video/watch_and_send2vdms.py | 206 --- 27 files changed, 2275 insertions(+), 1456 deletions(-) create mode 100644 fastapi/entrypoint.sh create mode 100644 fastapi/include/handlers.py create mode 100755 fastapi/manage.sh delete mode 100644 fastapi/requirements.GPU.txt delete mode 100644 fastapi/requirements.txt rename {video => fastapi}/resources/models/download_yolo.py (90%) rename {video => fastapi}/resources/models/models.lst (100%) rename {video => fastapi}/resources/models/ultralytics/custom_models/.gitignore (100%) create mode 100644 finetune/.env create mode 100644 finetune/Dockerfile create mode 100644 finetune/app/finetune.py create mode 100644 finetune/app/include/utils.py create mode 100644 finetune/app/requirements.txt create mode 100644 finetune/docker-compose.yml create mode 100644 video/source_watcher.py delete mode 100644 video/watch_and_send2vdms.py diff --git a/.gitignore b/.gitignore index 82cc81d..42c342d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ **/_* **/DockerImageTars/ build/* -video/resources/models/intel -video/resources/models/ultralytics +fastapi/resources/models/intel +fastapi/resources/models/ultralytics +finetune/.env \ No newline at end of file diff --git a/deployment/docker-swarm/build.sh b/deployment/docker-swarm/build.sh index 19abc0b..4d0249f 100755 --- a/deployment/docker-swarm/build.sh +++ b/deployment/docker-swarm/build.sh @@ -25,7 +25,7 @@ BDIR=$(dirname $(dirname $DIR)) # if video or fastapi in DIR; then # if [[ "$DIR" == *video* || "$DIR" == *fastapi* ]]; then # echo "docker build --network host --file=${DIR}/../video/Dockerfile.base $@ -t lcc_base_video_image:latest ${DIR}/../video $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') --build-arg DEVICE=${DEVICE}" -docker build --build-arg DEVICE=${DEVICE} --network host --file="${BDIR}/video/Dockerfile.base" -t "lcc_base_video_image:latest" "${BDIR}/video" $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') +# docker build --build-arg DEVICE=${DEVICE} --network host --file="${BDIR}/video/Dockerfile.base" -t "lcc_base_video_image:latest" "${BDIR}/video" $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') # fi if test -f "${DIR}/docker-compose.yml.m4"; then diff --git a/deployment/docker-swarm/video.m4 b/deployment/docker-swarm/video.m4 index a2c819a..cdff31e 100644 --- a/deployment/docker-swarm/video.m4 +++ b/deployment/docker-swarm/video.m4 @@ -14,6 +14,7 @@ define(`PROFILE_GPU', `runtime: nvidia CLEANUP_INTERVAL: "10m" DBHOST: "vdms-service" UDF_HOST: "udf-service" + BACKEND_URL: "http://fastapi-service:8000" `MODEL_NAME': "defn(`MODEL_NAME')" `CUSTOM_MODEL_FLAG': "defn(`CUSTOM_MODEL_FLAG')" `RESIZE_FLAG': "defn(`RESIZE_FLAG')" @@ -51,8 +52,6 @@ define(`PROFILE_GPU', `runtime: nvidia restart: always depends_on: - fastapi-service - - udf-service - - vdms-service fastapi-service: shm_size: '2gb' # Give it plenty of space for video frames @@ -77,6 +76,7 @@ define(`PROFILE_GPU', `runtime: nvidia HTTPS_PROXY: "${HTTPS_PROXY}" no_proxy: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${no_proxy}" NO_PROXY: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${NO_PROXY}" + NVIDIA_DRIVER_CAPABILITIES: "all" ports: - target: 80 published: 30077 @@ -95,8 +95,9 @@ define(`PROFILE_GPU', `runtime: nvidia mode: 0440 volumes: - /etc/localtime:/etc/localtime:ro - - app-content:/var/www + - app-content:/var/www:rw - ../../inputs:/watch_dir:ro + - ../../fastapi/resources:/home/resources:rw networks: - appnet restart: always diff --git a/fastapi/Dockerfile b/fastapi/Dockerfile index 6571ac0..42f9696 100644 --- a/fastapi/Dockerfile +++ b/fastapi/Dockerfile @@ -1,57 +1,171 @@ -FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a as build +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a AS build ARG DEBIAN_FRONTEND=noninteractive ENV VIRTUAL_ENV=/opt/venv -ARG DEBUG="0" -ENV DEBUG="${DEBUG}" +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 RUN apt-get update RUN apt-get install --only-upgrade libc-bin libc6 && \ - apt-get install -y -q --no-install-recommends python3 python3-venv curl libgl1-mesa-glx && \ + apt-get install -y -q --no-install-recommends python3-setuptools \ + python3-dev python3-pip python3-venv \ + curl libgl1-mesa-glx \ + # Update and install necessary build tools and dependencies for OpenCV + build-essential \ + cmake \ + git \ + wget \ + unzip \ + yasm \ + pkg-config \ + libgl1 \ + libglib2.0-0 \ + libgtk2.0-dev \ + libtbb-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libavcodec-dev \ + libavformat-dev \ + libswscale-dev \ + libxvidcore-dev \ + libxine2-dev \ + libv4l-dev \ + libdc1394-dev \ + libatlas-base-dev \ + gfortran && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean -RUN python3 -m venv ${VIRTUAL_ENV} -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -# make sure all messages always reach console -ENV PYTHONUNBUFFERED=1 +RUN curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb -o /tmp/cuda-keyring.deb && \ + dpkg -i /tmp/cuda-keyring.deb && \ + rm /tmp/cuda-keyring.deb +RUN apt-get update && apt-get install -y -q --no-install-recommends \ + cuda-toolkit-12-4 \ + libcudnn9-cuda-12 \ + libnvinfer10 \ + libnvonnxparsers10 \ + libnvinfer-plugin10 && \ + rm -rf /var/lib/apt/lists/* -COPY --from=lcc_base_video_image:latest ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --from=lcc_base_video_image:latest /home /home - -# activate virtual environment -ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN python3 -m venv ${VIRTUAL_ENV} +ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" +ENV LD_LIBRARY_PATH="$VIRTUAL_ENV/lib:/usr/local/cuda/lib64:${LD_LIBRARY_PATH}" ARG DEVICE="CPU" ENV DEVICE="${DEVICE}" -RUN if [ "${DEVICE}" = "CPU" ]; then \ +# COPY requirements.* /home/ +RUN python -m pip install pip --upgrade --no-cache-dir && \ + pip3 install --no-cache-dir "fastapi==0.135.1" "uvicorn==0.42.0" "openvino-dev==2024.6.0" "numpy==1.26.4" "ultralytics==8.4.7" "vdms==0.0.22" "wheel==0.46.3" && \ + if [ "${DEVICE}" = "CPU" ]; then \ pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ + pip3 install --no-cache-dir "nncf==2.19.0"; \ else \ + apt-get update; \ + apt-get install -y -q --no-install-recommends cuda-toolkit-12-4 libcudnn9-cuda-12 libnvinfer10 libnvonnxparsers10 ibnvinfer-plugin10; \ + rm -rf /var/lib/apt/lists/*; \ pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ + pip3 install --no-cache-dir "nvidia-cuda-runtime-cu12==12.8.*" "onnx==1.20.1" "onnxruntime-gpu==1.23.2" "onnxslim==0.1.82" "tensorrt_cu12==10.14.1.48.post1" "nvidia-cudnn-cu12==9.10.2.21"; \ fi; + +ARG PYTHON_VERSION=3.10 +ENV PYTHON_VERSION=${PYTHON_VERSION} +# OPENCV W/ CUDA SUPPORT +ENV OPENCV_VERSION="4.11.0" +# ENV PYTHON_VERSION="3.$(${VIRTUAL_ENV}/bin/python -V | cut -f 1 | cut -d '.' -f 2)" +ENV DEPENDENCY_DIR=/tmp/build_opencv +# ENV PYTHONPATH=${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages:${DEPENDENCY_DIR}/opencv/modules +WORKDIR ${DEPENDENCY_DIR} + +# Clone OpenCV and OpenCV Contrib repositories +RUN git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv.git ${DEPENDENCY_DIR}/opencv&& \ + git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv_contrib.git ${DEPENDENCY_DIR}/opencv_contrib +# Create build directory and run CMake +WORKDIR ${DEPENDENCY_DIR}/opencv/build +RUN NUMPY_PATH=$(${VIRTUAL_ENV}/bin/python -c "import numpy; print(numpy.get_include())") && \ + cmake -D CMAKE_BUILD_TYPE=RELEASE \ + -D BUILD_EXAMPLES=OFF \ + -D BUILD_JAVA=OFF \ + -D BUILD_opencv_python2=OFF \ + -D BUILD_opencv_python3=ON \ + -D BUILD_PERF_TESTS=OFF \ + -D BUILD_TESTS=OFF \ + -D CMAKE_INSTALL_PREFIX=${VIRTUAL_ENV} \ + -D CUDA_ARCH_BIN=70,75,80,86,89,90 \ + -D CUDA_FAST_MATH=ON \ + -D ENABLE_FAST_MATH=ON \ + -D INSTALL_PYTHON_EXAMPLES=OFF \ + -D INSTALL_C_EXAMPLES=OFF \ + -D OPENCV_EXTRA_MODULES_PATH=${DEPENDENCY_DIR}/opencv_contrib/modules \ + -D PYTHON_DEFAULT_EXECUTABLE=${VIRTUAL_ENV}/bin/python \ + -D PYTHON3_EXECUTABLE=${VIRTUAL_ENV}/bin/python \ + -D PYTHON3_NUMPY_INCLUDE_DIRS=${NUMPY_PATH} \ + -D PYTHON3_PACKAGES_PATH=${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages \ + -D PYTHON3_LIBRARY=${VIRTUAL_ENV}/lib/libpython${PYTHON_VERSION}.so \ + -D PYTHON3_INCLUDE_DIR=$(python3 -c "import sysconfig; print(sysconfig.get_path('include'))") \ + -D WITH_CUBLAS=ON \ + -D WITH_CUDA=ON \ + -D WITH_CUDNN=ON \ + -D WITH_LIBV4L=ON \ + -D WITH_NVCUVENC=ON \ + -D WITH_NVCUVID=ON \ + .. && \ + make -j$(nproc) && \ + make install && \ + ldconfig + +# Verify where it actually installed +RUN find ${VIRTUAL_ENV} -name "cv2*.so" +# Test the import immediately +RUN ${VIRTUAL_ENV}/bin/python -c "import cv2; print(cv2.__version__)" +# Clean up +RUN rm -rf ${DEPENDENCY_DIR} + + # Set the working directory in the container WORKDIR /home -COPY resources /home/resources -COPY requirements.* /home/ -RUN python -m pip install pip --upgrade --no-cache-dir && \ - python -m pip install --no-cache-dir -r requirements.txt && \ - python -m pip install --no-cache-dir -r requirements.GPU.txt +RUN if [ "${DEVICE}" != "CPU" ]; then \ + # Dynamically find the site-packages folder without hardcoding '3.10' + PY_SITEPACKAGES=$(find ${VIRTUAL_ENV}/lib/ -maxdepth 2 -name "site-packages") && \ + echo "Found site-packages at: ${PY_SITEPACKAGES}" && \ + \ + # Link TensorRT libs + ln -sf ${PY_SITEPACKAGES}/tensorrt_libs/lib* /usr/lib/ && \ + \ + # Link NVIDIA libs + find ${PY_SITEPACKAGES}/nvidia/ -name "lib*.so*" -exec ln -sf {} /usr/lib/ \; && \ + \ + ldconfig; \ + fi + +# RUN omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 + +ARG DEBUG="0" +ENV DEBUG="${DEBUG}" COPY *.py /home/ +COPY *.sh /home/ COPY include /home/include COPY templates /home/templates COPY nginx.conf /etc/nginx/nginx.conf -# CMD /usr/local/sbin/nginx && uvicorn main:app --host 127.0.0.1 --port 8000 -# EXPOSE 80 8000 8080 -# CMD /usr/local/sbin/nginx -g 'daemon on;' && python3 /home/resources/models/download_yolo.py && python3 main.py -CMD ["/bin/bash","-c","/usr/local/sbin/nginx -g 'daemon on;' && python /home/resources/models/download_yolo.py && python3 main.py"] +COPY entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Set the Entrypoint +ENTRYPOINT ["entrypoint.sh"] + +# Your simplified CMD +CMD ["/bin/bash", "-c", "/home/manage.sh"] +# CMD ["/bin/bash","-c","python /home/resources/models/download_yolo.py && /home/manage.sh"] + EXPOSE 80 8000 8080 #### diff --git a/fastapi/build.sh b/fastapi/build.sh index 6428e1b..aadc240 100755 --- a/fastapi/build.sh +++ b/fastapi/build.sh @@ -3,9 +3,4 @@ IMAGE="lcc_fastapi" DIR=$(dirname $(readlink -f "$0")) -# Make sure user provided models are available to FastAPI -cp -rp "$DIR/../video/resources" $DIR/ - . "$DIR/../script/build.sh" - -rm -rf "$DIR/resources" diff --git a/fastapi/entrypoint.sh b/fastapi/entrypoint.sh new file mode 100644 index 0000000..6aa60b9 --- /dev/null +++ b/fastapi/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +# Run the download script using container env vars +echo "Starting model download..." +FILE="/home/resources/models/intel/face-detection-adas-0001/FP16/face-detection-adas-0001.xml" +if [ ! -f "$FILE" ]; then + omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 +fi +python /home/resources/models/download_yolo.py + +# Execute the CMD (which will be /home/manage.sh) +exec "$@" \ No newline at end of file diff --git a/fastapi/include/handlers.py b/fastapi/include/handlers.py new file mode 100644 index 0000000..da79659 --- /dev/null +++ b/fastapi/include/handlers.py @@ -0,0 +1,961 @@ +import asyncio +import logging +import os +import queue +import shlex +import shutil +import subprocess +import sys + +# Force FFmpeg to use more threads for decoding +import threading +import time +import traceback +from collections import deque +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from datetime import datetime + +import cv2 +import numpy as np +from ultralytics import YOLO +from ultralytics.utils.checks import check_imgsz + +from fastapi import FastAPI + +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( + "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" +) + + +# from process_stream import extract_metadata_from_results, release_clip_and_reencode, retry_query +from include.utils import ( + CODE_DIR, + CUSTOM_MODEL_FLAG, + DEBUG, + DEBUG_FLAG, + DETECTION_THRESHOLD, + DEVICE, + MODEL_H, + MODEL_NAME, + MODEL_PRECISION, + MODEL_W, + OMIT_DETECTIONS_FLAG, + TARGET_FPS, + YOLO_CLASS_NAMES, + PipelineMapping, + draw_label, + filter_contained_boxes, + get_detection_color, + get_display_frame_in_bytes, + manual_fps_calculation, + merge_boxes_limit, + metadata2vdms, +) + +# ----- SETUP LOGGING ----- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +main_app_logger = logging.getLogger() + + +# ----- SPECIAL VARIABLES ----- +CLIP_DURATION = 10 # seconds +KERNEL_RATIO = 0.05 # 0.03 # .05 # .025 +MASK_MAX_VALUE = 255 +MASK_THRESHOLD_VALUE = 127 +MAX_DETECTIONS = 100 +MAX_WORKERS = 4 +# DISPLAY_FRAME_SIZE = (1280, 720) +# DISPLAY_FRAME_SIZE = (640, 360) +# DISPLAY_FRAME_SIZE = (854, 480) +DISPLAY_FRAME_SIZE = (960, 540) +DISPLAY_FRAME_QUALITY = 50 +ENABLE_QUERYING = False + +if CUSTOM_MODEL_FLAG: + model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" +else: + model_path = f"{CODE_DIR}/resources/models/ultralytics/{MODEL_NAME}/{MODEL_PRECISION}/{MODEL_NAME}" + # model_path = f"{CODE_DIR}/{MODEL_NAME}" + +if DEVICE == "GPU": + model_path += ".engine" + # 1. Force PyTorch to initialize the CUDA context + import torch + + if torch.cuda.is_available(): + torch.cuda.set_device(0) + torch.cuda.empty_cache() + print(f"Using GPU: {torch.cuda.get_device_name(0)}") +else: + model_path += "_openvino_model/" + +# ----- GLOBAL VARIABLES ----- +manager = None # Manager() +local_processes = {} +all_metadata = {} # manager.dict() +send_metadata_queue = queue.Queue() # manager.Queue() + + +# ----- INGESTION FUNCTIONS ----- +# method to create clips (read frame write to file; add name to list) +def send_metadata(): + global all_metadata + clip_filename = "" + clip_key = "" + width = 0 + height = 0 + while True: + try: + queue_details = send_metadata_queue.get() + if queue_details is None: + break + + (clip_key, clip_filename, width, height) = queue_details + + metadata2vdms( + clip_key, + clip_filename, + all_metadata[clip_key], + width, + height, + ) + del all_metadata[clip_key] + + except queue.Empty: + pass + + +def handle_done(future): + try: + future.result() + except Exception as e: + print(f"Task error: {e}") + + +def save_and_finalize_clip( + clip_key, + _out_vid, + clip_filename, + tmp_file, + target_fps, + frame_width, + frame_height, +): + if DEBUG == "1": + print( + f"[TIMING],start_release_clip,{clip_key},{time.time()}", + flush=True, + ) + _out_vid.release() + if DEBUG == "1": + print( + f"[TIMING],end_release_clip,{clip_key},{time.time()}", + flush=True, + ) + + # Re-encode video in order to seek via ffmpeg later + GENERAL_OPTS = "-flags -global_header -hide_banner -loglevel error -nostats -tune zerolatency -flush_packets 0" # -filter:v fps={target_fps} + CONVERSION = f"-c:v libx264 -preset ultrafast -filter:v fps=fps={target_fps}" # "-c:v libx264 -preset medium" + reencode_cmd = f"ffmpeg -y -i {tmp_file} {GENERAL_OPTS} {CONVERSION} -crf 23 -c:a copy {clip_filename}" + cmd_list = shlex.split(reencode_cmd) + if DEBUG == "1": + print( + f"[TIMING],start_reencode,{clip_key},{time.time()}", + flush=True, + ) + subprocess.run(cmd_list, check=True) + end_time = time.time() + # filename = str(Path(clip_filename).name) + if DEBUG == "1": + print( + f"[TIMING],end_reencode,{clip_key},{end_time}", + flush=True, + ) + print(f"[TIMING],Save clip,{clip_key},{end_time}", flush=True) + os.remove(tmp_file) + + send_metadata_queue.put( + ( + clip_key, + clip_filename, + frame_width, + frame_height, + ) + ) + + +async def auto_cleanup_janitor(app): + while True: + await asyncio.sleep(10) + now = time.time() + # Iterating over a list of keys to avoid "dictionary changed size" error + for name in list(app.state.active_streams.keys()): + streamer = app.state.active_streams[name] + + # Check if the stream is marked inactive OR timed out + # streamer.active should be False when the video source ends + if not streamer.active or (now - streamer.last_heartbeat > 30): + if DEBUG == "1": + print(f"CLEANUP: Removing {name} from active_streams") + streamer.stop() + del app.state.active_streams[name] + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # This is the ONLY place this should be initialized + if not hasattr(app.state, "active_streams"): + app.state.active_streams = {} + app.state.status = "Ready" + # app.state.model = YOLO(model_path, verbose=False, task="detect") + # app.state.model_lock = threading.Lock() + asyncio.create_task(auto_cleanup_janitor(app)) + if DEBUG == "1": + print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") + yield + # Cleanup logic here... + for s in app.state.active_streams.values(): + s.stop() + app.state.status = "Stopped" + + +class VideoStreamHandler: + def __init__(self, source, name, active_streams): + self.model = YOLO(model_path, verbose=False, task="detect") + self.name = name + self.source = source + self.active = True + self.active_streams = active_streams + + # 1. Capture setup + self.cap = cv2.VideoCapture(source, cv2.CAP_FFMPEG) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce latency + self.get_fps_and_framecnt() + self.get_frameWH() + + # 2. Performance Tracking + self.stat_start_time = time.perf_counter() + self.stat_frame_count = 0 + self.stat_fps = 0 + self.latest_processed_frame = None + self.last_heartbeat = time.time() + self.last_frame_id = 0 + + self.video_writer = None + self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v + self.clip_id = 0 + self.clip_filename = "" + self.clip_key = "" + self.tmp_file = "" + + self.resize_h, self.resize_w = [MODEL_H, MODEL_W] + self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + self.scale_x = self.frame_width / MODEL_W + self.scale_y = self.frame_height / MODEL_H + self.min_contour_area = int( + (0.005 * self.frame_width) * (0.005 * self.frame_height) + ) # 207 + + self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) + self.dilate_kernel_for_enhanced_mask = np.ones((21, 21), np.uint8) + + # Device based setup + if DEVICE == "GPU": + self.prepare_gpu_pipeline() + self.warmup() + else: + self.operation_device_map = PipelineMapping( + detection_device="cpu" + ) # No CUDA HERE + self.prepare_cpu_pipeline() + + # 3. Start dedicated inference thread + self.model_warmup() + self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) + self.process_thread = threading.Thread( + target=self.run_realtime_inference, daemon=True + ) + self.process_thread.start() + + def allocate_cpu(self, bkgd_mask_queue_size=3): + # pass + self.resized_frame = np.zeros((3, self.resize_h, self.resize_w), dtype="uint8") + # cv2.cuda.createContinuous( + # self.resize_h, self.resize_w, cv2.CV_8UC3 + # ) + + self.fgMask = np.zeros( + (self.resize_h, self.resize_w), dtype="uint8" + ) # For resize + self.prev_bkgd = np.ones((self.resize_h, self.resize_w), dtype="uint8") + + # bkgd_mask_queue_size = 3 + self.mask_history = deque(maxlen=bkgd_mask_queue_size) + self.mask_history.append(self.prev_bkgd) + + def prepare_cpu_pipeline(self, method="knn"): + self.operation_device_map = PipelineMapping() # "full_cpu" + self.device_input = self.operation_device_map.detection_device + + self.allocate_cpu() + + # Subtraction + if method == "knn": + history = 300 # int(5 * self.target_fps) + background_thresh = 350 + NSamples = 10 + kNNSamples = 2 + self.lr = -1 # .01 #-1 # 0.001 #1 / (5 * self.target_fps) # -1 # 0.01 # 1 / history + + self.backSub = cv2.createBackgroundSubtractorKNN( + history=history, # default 500 + dist2Threshold=background_thresh, # default 400 + detectShadows=False, # default True + ) + self.backSub.setkNNSamples(kNNSamples) + self.backSub.setNSamples(NSamples) + elif method == "mog2": + history = int(2 * self.target_fps) + background_thresh = 10 + self.lr = 0.001 + + self.backSub = cv2.createBackgroundSubtractorMOG2( + history=history, # default 500 + varThreshold=background_thresh, # default 16 + detectShadows=False, # default True + ) + else: + raise ValueError(f"Provided method ({method}) is not available.") + + def allocate_gpu(self, bkgd_mask_queue_size=3): + self.resized_frame = cv2.cuda_GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC3) + + self.stream = cv2.cuda.Stream() + + self.gpu_fullres_frame = cv2.cuda_GpuMat( + self.frame_height, self.frame_width, cv2.CV_8UC3 + ) + + self.pinned_downloaded_resizedframe_np = cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC3 + ) + + self.fgMask = cv2.cuda_GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) # For resize + + self.prev_bkgd = cv2.cuda_GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) # For resize + self.prev_bkgd.setTo((255,)) + + self.mask_history = deque(maxlen=bkgd_mask_queue_size) + self.mask_history.append(self.prev_bkgd) + + self.gpu_threshold_dst_frame = cv2.cuda_GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_threshold_dst_frame + ) + + self.gpu_morphed_frame = cv2.cuda_GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) # For resize + cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_morphed_frame + ) + + self.pinned_downloaded_frame_np = cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + + def prepare_gpu_pipeline(self): + self.operation_device_map = PipelineMapping( + resize_device="gpu", + bkgd_subtraction_device="gpu", + threshold_device="gpu", + erodeAndDilate_device="gpu", + detection_device="gpu", + ) # rbtd_detection_gpu + + self.device_input = "cuda" + + self.allocate_gpu() + + # Subtraction + history = int(2 * self.target_fps) # 300 # int(5 * self.target_fps) + self.lr = 0.001 + background_thresh = 10 # 350 + # self.lr = ( + # -1 + # ) # .01 #-1 # 0.001 #1 / (5 * self.target_fps) # -1 # 0.01 # 1 / history + # bkgd_mask_queue_size = 3 + self.backSub = cv2.cuda.createBackgroundSubtractorMOG2( + history=history, # Clear ghosts of fast drones in ~2 seconds (2*fps) + varThreshold=background_thresh, # High threshold to ignore "shimmer" and compression noise # default 16 + detectShadows=False, # default True + ) + + self.dilate_filter = cv2.cuda.createMorphologyFilter( + cv2.MORPH_DILATE, cv2.CV_8U, self.dilate_kernel + ) + self.dilate_filter_for_enhanced_mask = cv2.cuda.createMorphologyFilter( + cv2.MORPH_DILATE, cv2.CV_8UC1, self.dilate_kernel_for_enhanced_mask + ) + + def warmup(self): + # WARM UP (Crucial for first-run latency) + # JIT kernels are compiled on the first call + self.gpu_warmup_frame = cv2.cuda_GpuMat( + self.frame_height, self.frame_width, cv2.CV_8U + ) + self.gpu_warmup_input_frame = cv2.cuda_GpuMat( + self.frame_height, self.frame_width, cv2.CV_8U + ) + self.gpu_warmup_input_frame_np = cv2.cuda.createContinuous( + self.frame_height, self.frame_width, cv2.CV_8UC3 + ) + self.gpu_warmup_input_frame_np[:] = [255, 0, 0] + cv2.cuda.createContinuous( + self.frame_height, self.frame_width, cv2.CV_8U, self.gpu_warmup_frame + ) + cv2.cuda.createContinuous( + self.frame_height, self.frame_width, cv2.CV_8U, self.gpu_warmup_input_frame + ) + + self.gpu_warmup_input_frame.upload(self.gpu_warmup_input_frame_np) + cv2.cuda.cvtColor( + self.gpu_warmup_input_frame, + cv2.COLOR_BGR2GRAY, + stream=self.stream, + dst=self.gpu_warmup_frame, + ) + self.stream.waitForCompletion() + + def model_warmup(self): + print("Starting warmup...") + dummy_input = torch.zeros((1, 3, self.resize_h, self.resize_w)).to( + self.device_input + ) # Match your benchmark size + for _ in range(20): + _ = self.model(dummy_input, verbose=False) + + def get_executor_backlog(self): + """Returns the number of tasks currently waiting in the thread pool queue.""" + # access the internal queue of the executor + return self.executor._work_queue.qsize() + + def stop(self): + self.active = False # Signals the while loops to exit + + # Release the VideoWriter if it exists + # if ENABLE_QUERYING and self.video_writer: + # # self.release_clip_and_reencode() + # self.save_and_finalize_clip( + # self.clip_key, + # self.video_writer, + # self.clip_filename, + # self.tmp_file, + # self.target_fps, + # MODEL_W, + # MODEL_H, + # ) + + # Close the OpenCV capture + if self.cap: + self.cap.release() + + # Join threads if you want to be 100% sure they are closed + # self.update_thread.join(timeout=1.0) + # self.process_thread.join(timeout=1.0) + + def run_pipeline(self, frame, frameNume, repeat_count=1): + # if self.device_input == "cpu": + # return self.test_full_cpu_detection_gpu( + # frame, frameNume, repeat_count=repeat_count + # ) + # else: + return self.test_rbtd_detection_gpu(frame, frameNume, repeat_count=repeat_count) + + def async_yolo_task(self, data): + """Heavy lifting moved to ThreadPoolExecutor""" + try: + if self.device_input == "cuda": + self.pinned_downloaded_frame_np = data["mask"].download(self.stream) + frame_bytes = self.contour2predictions( + data["frameNum"], + self.pinned_downloaded_frame_np, + data["full_frame"], + device_input=self.device_input, + repeat_count=data["repeat_count"], + ) + else: + frame_bytes = self.contour2predictions( + data["frameNum"], + data["mask"], + data["full_frame"], + device_input=self.device_input, + repeat_count=data["repeat_count"], + ) + self.latest_processed_frame = frame_bytes + self.last_heartbeat = time.time() + self.last_frame_id += 1 + except Exception: + e = traceback.format_exc() + print(f"Async YOLO Error: {e}") + + def process_frame_async(self, frame, frame_num): + """ + Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) + in the background without blocking the video reader. + """ + try: + # Calls your existing Page 22 logic (run_pipeline) + inf_data = self.run_pipeline(frame, frame_num + 1) + + if inf_data: + # Calls your Page 20 async_yolo_task to handle mask download/inference + self.async_yolo_task(inf_data) + + except Exception: + e = traceback.format_exc() + print(f"ERROR: process_frame_async failed for {self.name}: {e}") + + def run_realtime_inference(self): + """ + Main loop: Initializes the model in this thread to fix CUDA context issues. + """ + print(f"Inference thread started for {self.name}...") + + # --- CRITICAL: Initialize model INSIDE the thread --- + # This binds the GPU context to this thread specifically. + # import torch + # self.model = YOLO(model_path, verbose=False, task="detect") + # self.model.to('cuda') # Explicitly move to GPU in this thread + + target_interval = 1.0 / self.target_fps + last_process_time = time.time() + + while self.active: + # 1. REAL-TIME SYNC: Clear stale frames from buffer + # while True: + grabbed = self.cap.grab() + if not grabbed: + self.active = False + break + + now = time.time() + if now - last_process_time < target_interval: + continue + + success, frame = self.cap.retrieve() + if not success or frame is None: + continue + + last_process_time = now + + # 3. DECOUPLED AI: Only submit to AI if the worker queue is not backed up + # This prevents 'lag' if the AI is slower than the video feed + if self.get_executor_backlog() < MAX_WORKERS: + # Move the heavy 'run_pipeline' call into a background worker + self.executor.submit( + self.process_frame_async, frame.copy(), self.stat_frame_count + ) + else: + # If AI is busy, still update the display with the raw frame + # so the dashboard video stays smooth and fluid + _, buffer = cv2.imencode( + ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] + ) + self.latest_processed_frame = buffer.tobytes() + self.last_frame_id += 1 # Ensure the generator sees this 'clean' frame + + self.update_frame() + self.last_heartbeat = time.time() + + self.stop() + # Add this line to remove it from the dashboard immediately: + if self.name in self.active_streams: # noqa: F821 + del self.active_streams[self.name] # noqa: F821 + + def test_rbtd_detection_gpu(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + H, W = self.resize_h, self.resize_w + # self.cpu_resized_frame = cv2.resize(frame, (W, H)) + # self.video_writer.write(self.cpu_resized_frame) + self.gpu_fullres_frame.upload(frame, self.stream) + cv2.cuda.resize( + self.gpu_fullres_frame, + (W, H), + stream=self.stream, + dst=self.resized_frame, + interpolation=cv2.INTER_NEAREST, + ) + if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): + self.pinned_downloaded_resizedframe_np = self.resized_frame.download( + self.stream + ) + # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) + for _ in range(repeat_count): + # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) + self.video_writer.write(self.pinned_downloaded_resizedframe_np) + + # Background Subtraction on GPU + self.fgMask = self.backSub.apply( + self.resized_frame, float(self.lr), stream=self.stream + ) + + for m in list(self.mask_history): + # Dilate the historical mask on GPU + dilated = self.dilate_filter_for_enhanced_mask.apply(m) + # Bitwise AND on GPU + cv2.cuda.bitwise_and(self.prev_bkgd, dilated, self.prev_bkgd) + # dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) + # cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) + self.mask_history.append(self.fgMask.clone()) + min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) + + if max_val != min_val: + self.fgMask = cv2.cuda.bitwise_or(self.fgMask, self.prev_bkgd) + + # Thresholding + cv2.cuda.threshold( + self.fgMask, + MASK_THRESHOLD_VALUE, + MASK_MAX_VALUE, + cv2.THRESH_BINARY, + self.gpu_threshold_dst_frame, + self.stream, + ) + + # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + self.dilate_filter.apply( + self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream + ) + + return { + "frameNum": frameNum, + "mask": self.gpu_morphed_frame, + "full_frame": frame, # Original for cropping + "repeat_count": repeat_count, + } + + def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + H, W = self.resize_h, self.resize_w + self.cpu_resized_frame = cv2.resize( + frame, (W, H), interpolation=cv2.INTER_NEAREST + ) + if ENABLE_QUERYING: + for _ in range(repeat_count): + self.video_writer.write(self.cpu_resized_frame) + + # Background Subtraction on CPU + fgMask = self.backSub.apply(self.cpu_resized_frame, learningRate=self.lr) + + prev_bkgd = np.ones_like(fgMask) # AND + for m in self.mask_history: + # Dilate the historical mask + dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) + cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) + self.mask_history.append(fgMask) + + if prev_bkgd.max() != prev_bkgd.min(): + combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) + + # Convert the boolean array back to uint8 with 0 and 255 values + fgMask = combined_mask_bool.astype(np.uint8) * 255 + + # Thresholding + _, mask = cv2.threshold( + fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY + ) + + mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + + return { + "frameNum": frameNum, + "mask": mask, + "full_frame": frame, # Original for cropping + "repeat_count": repeat_count, + } + + def update_frame(self): + self.stat_frame_count += 1 + elapsed = time.perf_counter() - self.stat_start_time + if elapsed > 1.0: + self.stat_fps = self.stat_frame_count / elapsed + + def check_disk_usage(self, path, min_gb=0.5): + """Returns True if there is at least min_gb available at path.""" + try: + total, used, free = shutil.disk_usage(path) + # Convert bytes to Gigabytes + free_gb = free / (2**30) + return free_gb > min_gb + except Exception as e: + print(f"Disk check error: {e}") + return False + + # Gets video fps and framecount + def get_fps_and_framecnt(self): + self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps + if self.input_fps == 0: # Case when FPS isn't available + self.input_fps = manual_fps_calculation(self.name, num_frames=10) + + self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps + self.frame_skip = int(self.input_fps / self.target_fps) + if self.frame_skip < 1: + self.frame_skip = 1 + + self.MAX_FRAMES_PER_CLIP = int(self.target_fps * CLIP_DURATION) + self.target_interval = 1.0 / self.target_fps # 0.0666s + + if DEBUG == "1": + print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) + print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) + + # Frame count for videos + self.frame_count = None + if "://" not in str(self.source): + self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + # Gets frame W and H details + def get_frameWH(self): + input_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + input_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + if (input_height * input_width) < (MODEL_H * MODEL_W): + new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw + else: + new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw + + new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) + + self.width = new_sizeWH[0] + self.height = new_sizeWH[1] + + def get_detections_for_contours_bbs( + self, frameNum, foi, contours, thickness=2, device_input="cuda" + ): + # global active_streams + # source = self.source + stream_name = self.name + num_objs = 0 + # predictions = [] + metadata = dict() + # frame_bytes = 'b' + cropped_imgs, cropped_coords = [], [] + H, W = foi.shape[:2] # Unpack once + bbs_full_res = [] + + # Filter and Sort in one go (Minimize Python-to-C++ crossings) + raw_bbs = [] + padding = 64 + for c in contours: + area = cv2.contourArea(c) + x1, y1, w, h = cv2.boundingRect(c) + if ( + area > self.min_contour_area + ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect + xx1 = max(0, int((x1 * self.scale_x)) - padding) + yy1 = max(0, int((y1 * self.scale_y)) - padding) + xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) + yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) + raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) + bbs_full_res = sorted( + [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], + key=lambda x: x[0], + reverse=True, + )[:MAX_DETECTIONS] + + dist_thresh = min(0.05 * W, 0.05 * H) + merged = merge_boxes_limit( + bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + ) + + merged = filter_contained_boxes(merged, containment_thresh=0.9) + + # for cnt, area in merged: + for x1, y1, x2, y2 in merged: + if ( + x2 > x1 + and y2 > y1 + and (x2 - x1) < self.frame_width + and (y2 - y1) < self.frame_height + ): + crop = foi[y1:y2, x1:x2] + if crop.size > 0: + cropped_imgs.append(crop) + cropped_coords.append((x1, y1)) + + if not cropped_imgs: + frame_bytes = get_display_frame_in_bytes( + foi, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + return metadata, frame_bytes # num_objs, predictions + + # 2. Inference (Keep stream=False as it is stable) + results = self.model.predict( + cropped_imgs, + imgsz=MODEL_W, + batch=len(cropped_imgs), + device=device_input, + verbose=False, + stream=True, + max_det=MAX_DETECTIONS, + # classes=[0], # only "person", + # conf=0.45, + ) + + label_source = ( + self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES + ) + + for ridx, r in enumerate(results): + if r.boxes is None or len(r.boxes) == 0: + continue + + # Move to CPU in one bulk operation per crop + boxes = r.boxes.xyxy.cpu().numpy().astype(int) + clss = r.boxes.cls.cpu().numpy().astype(int) + confs = r.boxes.conf.cpu().numpy() + off_x, off_y = cropped_coords[ridx] + + for j in range(len(boxes)): + num_objs += 1 + bx1, by1, bx2, by2 = boxes[j] + abs_x1, abs_y1 = off_x + bx1, off_y + by1 + abs_x2, abs_y2 = off_x + bx2, off_y + by2 + class_id = clss[j] + class_name = label_source[class_id] + confidence = confs[j] + if confidence > DETECTION_THRESHOLD: + if not OMIT_DETECTIONS_FLAG: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + print( + # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", + f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", + flush=True, + ) + + bb_color = get_detection_color(class_id, is_bgr=True) + + foi = cv2.rectangle( + foi, + (abs_x1, abs_y1), + (abs_x2, abs_y2), + bb_color, + thickness, + ) + label = f"{class_name} {confidence:.2f}" + draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) + + height = min(abs_y2, H) - max(0, abs_y1) + width = min(abs_x2, W) - max(0, abs_x1) + # object_res = [ + # abs_x1, + # abs_y1, + # height, + # width, + # class_name, + # confidence, + # H, + # W, + # ] + + # Resized + scale_x = self.resize_w / W + scale_y = self.resize_h / H + object_res = [ + int(abs_x1 * scale_x), + int(abs_y1 * scale_y), + int(height * scale_y), + int(width * scale_x), + class_name, + confidence, + int(self.resize_h), + int(self.resize_w), + ] + + framenum_str = f"{frameNum:04d}_{j:04d}" + if DEBUG_FLAG: + meta_str = ",".join( + [str(o) for o in object_res + [framenum_str]] + ) + print(f"[{stream_name} METADATA],{meta_str}", flush=True) + + # Full Res + metadata[framenum_str] = { + "frameId": frameNum, + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(object_res[6]), + "frameW": int(object_res[7]), + }, + }, + } + + # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) + frame_bytes = get_display_frame_in_bytes( + foi, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + + return metadata, frame_bytes + + def contour2predictions( + self, frameNum, mask, frame, device_input="cpu", repeat_count=1 + ): + # source = self.source + # stream_name = self.name + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 3. Write frame + # self.video_writer.write(frame) + # if self.video_writer: + # for _ in range(repeat_count): + # self.video_writer.write(self.cpu_resized_frame) + + # num_objs = 0 + # predictions = [] + metadata = dict() + if contours: + metadata, frame_bytes = self.get_detections_for_contours_bbs( + frameNum, frame, contours, thickness=2, device_input=device_input + ) + + if metadata: + all_metadata.setdefault( + self.clip_key, + { + "object": {}, + "face": {}, + }, + ) + all_metadata[self.clip_key]["object"].update(metadata) + # all_metadata[clip_key]["face"].update(metadata_face) + return frame_bytes diff --git a/fastapi/include/utils.py b/fastapi/include/utils.py index 5347f17..1be9ca2 100644 --- a/fastapi/include/utils.py +++ b/fastapi/include/utils.py @@ -13,6 +13,7 @@ import cv2 import numpy as np +from pydantic import BaseModel # import streamlit as st from ultralytics import YOLO @@ -83,7 +84,7 @@ def str2bool(in_val): SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) -TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") +TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache") UDF_HOST = os.getenv("UDF_HOST", "udf-service") UDF_PORT = 5011 @@ -95,9 +96,38 @@ def str2bool(in_val): EXPORT_BATCH_SIZE = int(os.environ.get("CPU_BATCH_SIZE", 1)) # 8 print("[!] USING CPU") -if not TEST_MODE: - db = vdms.vdms() - db.connect(DBHOST, DBPORT) +# if not TEST_MODE: +# db = vdms.vdms() +# db.connect(DBHOST, DBPORT) +import queue + + +class VDMSPool: + def __init__(self, host, port, size=5): + self.host = host + self.port = port + self.size = size + self.pool = queue.Queue(maxsize=size) + + # Pre-populate the pool with authenticated connections + for _ in range(size): + self.pool.put(self._create_connection()) + + def _create_connection(self): + client = vdms.vdms() + client.connect(self.host, self.port) + return client + + def get_connection(self): + # Borrow a connection (blocks if pool is empty) + return self.pool.get(block=True, timeout=10) + + def return_connection(self, conn): + # Put the connection back for reuse + self.pool.put(conn) + + +VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) LOCKTIMEOUT_RETRIES = 5 ERR_KEYWORDS = [ @@ -286,8 +316,11 @@ def draw_label( ) -def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): - global db +def retry_query( + query, local_db=None, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0 +): + # global db + db = local_db if local_db else vdms.vdms().connect(DBHOST, DBPORT) for ridx in range(num_retries + 1): response, _ = db.query(query, [[]]) if "FailedCommand" in response[0] and any( @@ -440,7 +473,7 @@ def get_models(model_tag: str, model_dir=PROJECT_PATH / "models"): # , _st_side # def get_display_frame_in_bytes(foi, frame_width, display_size=(1280, 720), quality=50): if frame_width > display_size[0]: - display_frame = cv2.resize(foi, display_size, interpolation=cv2.INTER_AREA) + display_frame = cv2.resize(foi, display_size, interpolation=cv2.INTER_NEAREST) ret, buffer = cv2.imencode( ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality] ) @@ -491,6 +524,7 @@ def get_udf_query( id="udf_metadata", metadata=None, test_mode=TEST_MODE, + local_db=None, ): query = { "AddVideo": { @@ -527,7 +561,7 @@ def get_udf_query( flush=True, ) try: - res = retry_query([query], sleep_timer=randint(1, 5)) + res = retry_query([query], local_db=local_db, sleep_timer=randint(1, 5)) if DEBUG_FLAG: print( @@ -587,22 +621,28 @@ def metadata2vdms( combined_metadata[face_frameidx_bbidx] = value combined_metadata = _sort_dict_by_frame(combined_metadata) - get_udf_query( - clip_filename, - properties, - INGESTION.replace(",", "+"), - (width, height), - id="udf_metadata", - metadata=combined_metadata, - test_mode=TEST_MODE, - ) - if DEBUG == "1": - print( - f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", - flush=True, + db = VDMS_POOL.get_connection() + try: + get_udf_query( + clip_filename, + properties, + INGESTION.replace(",", "+"), + (width, height), + id="udf_metadata", + metadata=combined_metadata, + test_mode=TEST_MODE, + local_db=db, ) + if DEBUG == "1": + print( + f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) + finally: + VDMS_POOL.return_connection(db) + # Extract metadata from object model results def extract_metadata_from_results( @@ -808,3 +848,8 @@ class PipelineMapping: erodeAndDilate_device: str.lower = "cpu" contour_device: str.lower = "cpu" detection_device: str.lower = "cpu" + + +class StreamRequest(BaseModel): + url: str + name: str diff --git a/fastapi/main.py b/fastapi/main.py index 6528868..13e45b1 100644 --- a/fastapi/main.py +++ b/fastapi/main.py @@ -1,61 +1,21 @@ import asyncio import logging import os -import queue -import shlex -import subprocess import sys # Force FFmpeg to use more threads for decoding -import threading import time -from collections import deque -from concurrent.futures import ThreadPoolExecutor -from datetime import datetime -from pathlib import Path - -import cv2 -import numpy as np -from pydantic import BaseModel -from ultralytics import YOLO -from ultralytics.utils.checks import check_imgsz from fastapi import FastAPI, HTTPException, Request from fastapi.responses import StreamingResponse from fastapi.templating import Jinja2Templates os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( - "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" + "rtsp_transport;tcp|hwaccel;cuda|threads;1|probesize;32|analyzeduration;0" ) - -# from process_stream import extract_metadata_from_results, release_clip_and_reencode, retry_query -from include.utils import ( - CODE_DIR, - CUSTOM_MODEL_FLAG, - DEBUG, - DEBUG_FLAG, - DETECTION_THRESHOLD, - DEVICE, - MODEL_H, - MODEL_NAME, - MODEL_PRECISION, - MODEL_W, - NUM_USUABLE_CPUS, - OMIT_DETECTIONS_FLAG, - SHARED_OUTPUT, - TARGET_FPS, - TMP_LOCATION, - YOLO_CLASS_NAMES, - PipelineMapping, - draw_label, - filter_contained_boxes, - get_detection_color, - get_display_frame_in_bytes, - manual_fps_calculation, - merge_boxes_limit, - metadata2vdms, -) +from include.handlers import VideoStreamHandler, lifespan +from include.utils import DEBUG, StreamRequest # ----- SETUP LOGGING ----- logging.basicConfig( @@ -68,1015 +28,45 @@ uvicorn_logger.setLevel(logging.INFO) -# ----- SPECIAL VARIABLES ----- -CLIP_DURATION = 10 # seconds -KERNEL_RATIO = 0.05 # 0.03 # .05 # .025 -MASK_MAX_VALUE = 255 -MASK_THRESHOLD_VALUE = 127 -MAX_DETECTIONS = 100 -# MAX_FRAMES_PER_CLIP = int(TARGET_FPS * CLIP_DURATION) # 150 frames -# MODEL_PRECISION = "FP16" -# MODEL_W, MODEL_H = (640, 640) -# NUM_USUABLE_CPUS = 2 -# OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) -# SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") -# Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) -# TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) -# TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") - -if CUSTOM_MODEL_FLAG: - model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" -else: - model_path = f"{CODE_DIR}/resources/models/ultralytics/{MODEL_NAME}/{MODEL_PRECISION}/{MODEL_NAME}" - # model_path = f"{CODE_DIR}/{MODEL_NAME}" - -if DEVICE == "GPU": - model_path += ".engine" -else: - model_path += "_openvino_model/" - -# ----- GLOBAL VARIABLES ----- -manager = None # Manager() -local_processes = {} -all_metadata = {} # manager.dict() -send_metadata_queue = queue.Queue() # manager.Queue() - - -# ----- INGESTION FUNCTIONS ----- -# method to create clips (read frame write to file; add name to list) -def send_metadata(): - global all_metadata - clip_filename = "" - clip_key = "" - width = 0 - height = 0 - while True: - try: - queue_details = send_metadata_queue.get() - if queue_details is None: - break - - (clip_key, clip_filename, width, height) = queue_details - - metadata2vdms( - clip_key, - clip_filename, - all_metadata[clip_key], - width, - height, - ) - del all_metadata[clip_key] - - except queue.Empty: - pass - - -def handle_done(future): - try: - future.result() - except Exception as e: - print(f"Task error: {e}") - +# --------------- APP ------------------- +app = FastAPI(lifespan=lifespan) +templates = Jinja2Templates(directory="templates") -def save_and_finalize_clip( - clip_key, - _out_vid, - clip_filename, - tmp_file, - target_fps, - frame_width, - frame_height, -): - if DEBUG == "1": - print( - f"[TIMING],start_release_clip,{clip_key},{time.time()}", - flush=True, - ) - _out_vid.release() - if DEBUG == "1": - print( - f"[TIMING],end_release_clip,{clip_key},{time.time()}", - flush=True, - ) - # Re-encode video in order to seek via ffmpeg later - GENERAL_OPTS = "-flags -global_header -hide_banner -loglevel error -nostats -tune zerolatency -flush_packets 0" # -filter:v fps={target_fps} - CONVERSION = f"-c:v libx264 -preset ultrafast -filter:v fps=fps={target_fps}" # "-c:v libx264 -preset medium" - reencode_cmd = f"ffmpeg -y -i {tmp_file} {GENERAL_OPTS} {CONVERSION} -crf 23 -c:a copy {clip_filename}" - cmd_list = shlex.split(reencode_cmd) - if DEBUG == "1": - print( - f"[TIMING],start_reencode,{clip_key},{time.time()}", - flush=True, - ) - subprocess.run(cmd_list, check=True) - end_time = time.time() - # filename = str(Path(clip_filename).name) +@app.get("/") +async def index(request: Request): + """Renders the dashboard.""" if DEBUG == "1": - print( - f"[TIMING],end_reencode,{clip_key},{end_time}", - flush=True, - ) - print(f"[TIMING],Save clip,{clip_key},{end_time}", flush=True) - os.remove(tmp_file) - - send_metadata_queue.put( - ( - clip_key, - clip_filename, - frame_width, - frame_height, - ) + print(f"Active Streams: {app.state.active_streams.keys()}") + curr_keys = list(app.state.active_streams.keys()) + # return templates.TemplateResponse( + # "index.html", {"request": request, "cameras": curr_keys} + # ) + return templates.TemplateResponse( + request=request, name="index.html", context={"cameras": curr_keys} ) -class VideoStreamHandler: - def __init__(self, source, name): - self.model = YOLO(model_path, verbose=False, task="detect") - self.source = source - self.name = name - # self.cap = cv2.VideoCapture(source) - # Use CAP_FFMPEG and increase internal buffer for RTSP - self.cap = cv2.VideoCapture(source, cv2.CAP_FFMPEG) - # Force the hardware buffer to 1 so we don't lag - # self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) - # Set a timeout so it doesn't hang - self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) - - # self.stat_start_time = time.perf_counter() - self.stat_frame_count = 0 - self.stat_fps = 0 - - self.video_writer = None - self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v - self.clip_id = 0 - self.clip_filename = "" - self.clip_key = "" - self.tmp_file = "" - - self.active = True - self.frame = None - self.latest_processed_frame = None - self.last_write_time = time.time() - self.last_frame_id = 0 # Increment this in your DETECTION loop - self.sent_frame_id = -1 # Track what the BROWSER has already seen - - self.resize_h, self.resize_w = [MODEL_H, MODEL_W] - self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) - - self.get_fps_and_framecnt() - - self.get_frameWH() - - self.scale_x = self.frame_width / MODEL_W - self.scale_y = self.frame_height / MODEL_H - self.min_contour_area = int( - (0.005 * self.frame_width) * (0.005 * self.frame_height) - ) # 207 - - self.operation_device_map = PipelineMapping( - detection_device="cpu" - ) # No CUDA HERE - self.device_input = ( - self.operation_device_map.detection_device - if self.operation_device_map.detection_device == "cpu" - else "cuda" - ) - - self.cpu_resized_frame = None - - # Subtraction - history = 300 # int(5 * self.fps) - background_thresh = 350 - NSamples = 10 - kNNSamples = 2 - self.lr = ( - -1 - ) # .01 #-1 # 0.001 #1 / (5 * self.fps) # -1 # 0.01 # 1 / history - bkgd_mask_queue_size = 3 - self.backSub_cpu = cv2.createBackgroundSubtractorKNN( - history=history, # default 500 - dist2Threshold=background_thresh, # default 400 - detectShadows=False, # default True - ) - self.backSub_cpu.setkNNSamples(kNNSamples) - self.backSub_cpu.setNSamples(NSamples) - - prev_bkgd = np.zeros((MODEL_H, MODEL_W), dtype="uint8") - self.mask_history = deque(maxlen=bkgd_mask_queue_size) - self.mask_history.append(prev_bkgd) - - self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) - self.dilate_kernel_for_enhanced_mask = np.ones((21, 21), np.uint8) - - # Create ThreadPoolExecutor - self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) - self.metadata_thread = threading.Thread(target=self.send_metadata, daemon=True) - - self.thread = threading.Thread(target=self.update, daemon=True) - - self.process_thread = threading.Thread(target=self.run_inference, daemon=True) - self.start() - - def start(self): - # self.t = [] - # self.t.append( - # self.executor.submit( - # send_metadata, - # ) - # ) - self.metadata_thread.start() - self.thread.start() - self.process_thread.start() - - # def stop(self): - # self.active = False - # if self.video_writer: - # self.release_clip_and_reencode() - # # for t in as_completed(self.t): - # # try: - # # _ = t.result() - # # except Exception as t_e: - # # print(f"[DEBUG] Exception occurred in thread: {t_e}") - # send_metadata_queue.put(None) - # self.metadata_thread.join() - - # self.executor.shutdown(wait=True, cancel_futures=False) - # # self.thread.join() - # # self.process_thread.join() - # self.cap.release() - - def stop(self): - self.active = False # Signals the while loops to exit - - # Release the VideoWriter if it exists - if self.video_writer: - self.release_clip_and_reencode() - - # Close the OpenCV capture - if self.cap: - self.cap.release() - - # Join threads if you want to be 100% sure they are closed - # self.thread.join(timeout=1.0) - # self.process_thread.join(timeout=1.0) - - def update_frame(self): - self.stat_frame_count += 1 - elapsed = time.perf_counter() - self.stat_start_time - if elapsed > 0.0: # Update FPS every second - self.stat_fps = self.stat_frame_count / elapsed - # To keep it "real-time" and not a lifetime average, reset: - # self.stat_start_time = time.perf_counter() - # self.stat_frame_count = 0 - - def send_metadata(self): - # This loop runs in its own dedicated threading.Thread - while True: - try: - # Blocks until something is in the queue - queue_details = send_metadata_queue.get() - - if queue_details is None: # Sentinel to shut down - break - - (clip_key, clip_filename, width, height) = queue_details - - clip_data = all_metadata.get(clip_key) - - if clip_data: - # Use the EXECUTOR to fire off the heavy metadata sending - # This returns immediately so the loop can grab the next item - future = self.executor.submit( - metadata2vdms, - clip_key, - clip_filename, - clip_data, - width, - height, - ) - # Track success - future.add_done_callback(handle_done) - - # Clean up dict entry after submitting to the thread - # Note: If metadata2vdms needs the data, pass it in (as done above) - del all_metadata[clip_key] - - # Mark the task as done in the queue - send_metadata_queue.task_done() - - except Exception as e: - print(f"Queue Error: {e}") - - # Gets video fps and framecount - def get_fps_and_framecnt(self): - self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps - if self.input_fps == 0: # Case when FPs isn't available - self.input_fps = manual_fps_calculation(self.name, num_frames=10) - - self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps - self.frame_skip = int(self.input_fps / self.target_fps) - if self.frame_skip < 1: - self.frame_skip = 1 - - self.MAX_FRAMES_PER_CLIP = int(self.target_fps * CLIP_DURATION) - self.target_interval = 1.0 / self.target_fps # 0.0666s - - print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) - print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) - - # Frame count for videos - self.frame_count = None - if "://" not in str(self.source): - self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) - - # Gets frame W and H details - def get_frameWH(self): - input_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - input_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - if (input_height * input_width) < (MODEL_H * MODEL_W): - new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw - else: - new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw - - new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) - - self.width = new_sizeWH[0] - self.height = new_sizeWH[1] - - def run_inference(self): - print(f"Inference thread for {self.name} started...") - - # 1. Initialize the start time and a counter for frames actually written - start_time = time.time() - self.stat_start_time = time.perf_counter() - total_frames_written = 0 - - while self.active: - # 2. Calculate how many frames SHOULD be in the file by now - elapsed_real_time = time.time() - start_time - expected_total_frames = int(elapsed_real_time * self.target_fps) - - # 3. Determine the "Gap": How many frames do we need to write to stay in sync? - # If processing took 200ms, slots_to_fill will be ~3. - slots_to_fill = expected_total_frames - total_frames_written - - # if slots_to_fill > 0: - slots_to_fill = 1 - frame = self.get_frame() - if frame is None: - time.sleep(0.01) - continue - # Handle Video Writing (Cycle every 10 seconds) - # clip_frameNum = (expected_total_frames - 1) % self.MAX_FRAMES_PER_CLIP - clip_frameNum = self.stat_frame_count % self.MAX_FRAMES_PER_CLIP - if clip_frameNum == 0 or total_frames_written == 0: - print(f"frameNum: {expected_total_frames} ({clip_frameNum})") - if self.video_writer: - self.release_clip_and_reencode() - if "://" not in str(self.source): - self.clip_filename = ( - f"{SHARED_OUTPUT}/{self.name}_{self.clip_id}.mp4" - ) - else: - self.clip_filename = ( - f"{SHARED_OUTPUT}/{self.name}_{time.time()}.mp4" - ) - - self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] - self.clip_key = Path(self.clip_filename).name - - # timestamp = int(time.time()) - # filename = f"clip_{timestamp}.mp4" - self.video_writer = cv2.VideoWriter( - self.tmp_file, - self.fourcc, - self.target_fps, - (MODEL_W, MODEL_H), - # (self.width, self.height) - # (self.frame_width, self.frame_height), - ) - main_app_logger.info(f"Started new clip: {self.tmp_file}") - - try: - # 4. PASS THE REPEAT COUNT to your function - # This ensures the video length matches the stopwatch - frame_bytes = self.test_full_cpu_detection_gpu( - frame, self.stat_frame_count + 1, repeat_count=slots_to_fill - ) - if frame_bytes is None: - print( - f"CRITICAL: {self.name} detection returned NULL bytes. Check OpenVINO logs." - ) - else: - print( - f"SUCCESS: {self.name} pushed {len(frame_bytes)} bytes to memory." - ) - self.latest_processed_frame = frame_bytes - self.last_heartbeat = time.time() - self.last_frame_id += 1 - total_frames_written = expected_total_frames - self.frame = None - self.update_frame() - except Exception as e: - print(f"Inference Error: {e}") - # else: - # # 6. We are ahead of the clock; wait for the next 66ms window - # # time.sleep(0.005) - # pass - - # def run_inference(self): - # print(f"Inference thread for {self.name} started...") - - # # This counter now perfectly represents a 15fps clock - # frame_counter = 0 - - # while self.active: - # frame = self.get_frame() - # if frame is None: - # time.sleep(0.005) - # continue - - # # frame_counter will now hit exactly 150 every 10 seconds - # frame_counter += 1 - - # # Handle Video Writing (Cycle every 10 seconds) - # clip_frameNum = (frame_counter - 1) % self.MAX_FRAMES_PER_CLIP - # if clip_frameNum == 0: - # print(f"frameNum: {frame_counter} ({clip_frameNum})") - # if self.video_writer: - # self.release_clip_and_reencode() - # if "://" not in str(self.source): - # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{self.clip_id}.mp4" - # else: - # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{time.time()}.mp4" - - # self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] - # self.clip_key = Path(self.clip_filename).name - - # # timestamp = int(time.time()) - # # filename = f"clip_{timestamp}.mp4" - # self.video_writer = cv2.VideoWriter( - # self.tmp_file, - # self.fourcc, - # self.target_fps, - # (MODEL_W, MODEL_H), - # # (self.width, self.height) - # # (self.frame_width, self.frame_height), - # ) - # main_app_logger.info(f"Started new clip: {self.tmp_file}") - - # try: - - # # This triggers your 10s clip rotation (frameNum % 150 == 0) - # self.test_full_cpu_detection_gpu(frame, frame_counter) - - # # self.latest_processed_frame = frame_bytes # From your detection function - # self.frame = None # Signal Reader for next frame - - # except Exception as e: - # print(f"Inference Error: {e}") - - # def run_inference(self): - # print(f"Inference thread for {self.name} started...") - - # target_fps = 15.0 - # target_interval = 1.0 / target_fps # 0.0666s - - # start_time = time.time() - # total_frames_written = 0 - - # # Store the last frame to use as a "filler" if we lag - # last_processed_annotated = None - - # while self.active: - # # 1. Calculate how many frames SHOULD be in the file by now - # elapsed_real_time = time.time() - start_time - # expected_total_frames = int(elapsed_real_time * target_fps) - - # # 2. Are we behind the clock? - # if expected_total_frames > total_frames_written: - # # How many frames do we need to write to CATCH UP to the clock? - # frames_to_catch_up = expected_total_frames - total_frames_written - - # frame = self.get_frame() - # if frame is not None: - - # # Handle Video Writing (Cycle every 10 seconds) - # clip_frameNum = (expected_total_frames - 1) % self.MAX_FRAMES_PER_CLIP - # if clip_frameNum == 0: - # print(f"frameNum: {expected_total_frames} ({clip_frameNum})") - # if self.video_writer: - # self.release_clip_and_reencode() - # if "://" not in str(self.source): - # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{self.clip_id}.mp4" - # else: - # self.clip_filename = f"{SHARED_OUTPUT}/{self.name}_{time.time()}.mp4" - - # self.tmp_file = TMP_LOCATION + self.clip_filename.split("/")[-1] - # self.clip_key = Path(self.clip_filename).name - - # # timestamp = int(time.time()) - # # filename = f"clip_{timestamp}.mp4" - # self.video_writer = cv2.VideoWriter( - # self.tmp_file, - # self.fourcc, - # self.target_fps, - # (MODEL_W, MODEL_H), - # # (self.width, self.height) - # # (self.frame_width, self.frame_height), - # ) - # main_app_logger.info(f"Started new clip: {self.tmp_file}") - # try: - # # 3. Process the latest frame (YOLO/KNN) - # # This might take 100ms (more than one 66ms tick) - # self.test_full_cpu_detection_gpu(frame, expected_total_frames) - # # self.latest_processed_frame = frame_bytes - - # # 4. WRITER SYNC: - # # If we missed 3 'ticks' while processing, write this frame 3 times. - # # This is the "Brake" that stops the fast-forward. - # if self.video_writer: - # for _ in range(frames_to_catch_up): - # self.video_writer.write(self.cpu_resized_frame) - - # total_frames_written = expected_total_frames - # self.frame = None - # except Exception as e: - # print(f"Inference Error: {e}") - # else: - # # No new frame from reader yet, but we MUST keep the clock moving - # # Write the previous frame again to maintain video duration - # if self.video_writer and total_frames_written > 0: - # for _ in range(frames_to_catch_up): - # self.video_writer.write(self.cpu_resized_frame) - # total_frames_written = expected_total_frames - # time.sleep(0.01) - # else: - # # 5. We are ahead of the clock (CPU is fast); wait for the next 66ms tick - # time.sleep(0.005) - - # def update(self): - # # Calculate exactly how much time should pass between 15fps frames - # # target_interval = 1.0 / self.target_fps # 0.0666s - # last_grab_time = time.time() - - # while self.active: - # # 1. Fast-forward the internal buffer using grab() - # # This clears out the 21fps 'junk' frames - # success = self.cap.grab() - # if not success: - # self.active = False - # break - - # # 2. Check if enough time has passed to 'retrieve' a 15fps frame - # current_time = time.time() - # if current_time - last_grab_time >= self.target_interval: - # success, frame = self.cap.retrieve() - # if success: - # self.frame = frame - # last_grab_time = current_time - - # time.sleep(0.001) - - def update(self): - print(f"READER THREAD: Started for {self.name}") - # The 'step' is 1.4 (21 in / 15 out) - step = self.input_fps / self.target_fps - # This tracks where the next 'keeper' frame is in the 21fps timeline - next_keeper_idx = 0.0 - current_idx = 0 - - while self.active: - if not self.cap.isOpened(): - self.active = False - break - - # 1. Skip (grab) frames until we reach the next 15fps 'slot' - while current_idx < int(next_keeper_idx): - if not self.cap.grab(): - self.active = False - return - current_idx += 1 - - # 2. Retrieve (decode) the keeper frame - success, frame = self.cap.read() # read() includes grab + retrieve - if not success: - self.active = False - break - - current_idx += 1 - # Advance the 'keeper' mark by 1.4 - next_keeper_idx += step - - # 3. Hand off the frame to the inference thread - self.frame = frame - # time.sleep(0.001) - - # --- AUTO-CLEANUP --- - # Remove itself from the global dictionary so the dashboard knows it's gone - if self.name in app.state.active_streams: - app.state.active_streams[self.name].stop() - del app.state.active_streams[self.name] - - # def update(self): - # # 1/15 = 0.066s interval - # target_interval = 1.0 / self.target_fps - # last_yield_time = time.time() - - # while self.active: - # # 1. Grab (don't decode) as fast as possible to clear the buffer - # success = self.cap.grab() - # if not success: - # self.active = False - # break - - # # 2. Only retrieve (decode) if 66ms has passed - # current_time = time.time() - # if current_time - last_yield_time >= target_interval: - # success, frame = self.cap.retrieve() - # if success: - # self.frame = frame - # last_yield_time = current_time - - # time.sleep(0.001) - - def get_frame(self): - return self.frame - - def get_detections_for_contours_bbs( - self, frameNum, foi, contours, thickness=2, device_input="cuda" - ): - # global active_streams - # source = self.source - stream_name = self.name - num_objs = 0 - # predictions = [] - metadata = dict() - # frame_bytes = 'b' - cropped_imgs, cropped_coords = [], [] - H, W = foi.shape[:2] # Unpack once - bbs_full_res = [] - - # Filter and Sort in one go (Minimize Python-to-C++ crossings) - raw_bbs = [] - padding = 64 - for c in contours: - area = cv2.contourArea(c) - x1, y1, w, h = cv2.boundingRect(c) - if ( - area > self.min_contour_area - ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect - xx1 = max(0, int((x1 * self.scale_x)) - padding) - yy1 = max(0, int((y1 * self.scale_y)) - padding) - xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) - yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) - raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) - bbs_full_res = sorted( - [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], - key=lambda x: x[0], - reverse=True, - )[:MAX_DETECTIONS] - - dist_thresh = min(0.05 * W, 0.05 * H) - merged = merge_boxes_limit( - bbs_full_res, dist_threshold=dist_thresh, size_limit=640 - ) - - merged = filter_contained_boxes(merged, containment_thresh=0.9) - - # for cnt, area in merged: - for x1, y1, x2, y2 in merged: - if ( - x2 > x1 - and y2 > y1 - and (x2 - x1) < self.frame_width - and (y2 - y1) < self.frame_height - ): - crop = foi[y1:y2, x1:x2] - if crop.size > 0: - cropped_imgs.append(crop) - cropped_coords.append((x1, y1)) - - if not cropped_imgs: - frame_bytes = get_display_frame_in_bytes( - foi, self.frame_width, display_size=(1280, 720), quality=50 - ) - return metadata, frame_bytes # num_objs, predictions - - # 2. Inference (Keep stream=False as it is stable) - results = self.model.predict( - cropped_imgs, - imgsz=MODEL_W, - batch=len(cropped_imgs), - device=device_input, - verbose=False, - stream=True, - max_det=MAX_DETECTIONS, - # classes=[0], # only "person", - # conf=0.45, - ) - - label_source = ( - self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES - ) - - for ridx, r in enumerate(results): - if r.boxes is None or len(r.boxes) == 0: - continue - - # Move to CPU in one bulk operation per crop - boxes = r.boxes.xyxy.cpu().numpy().astype(int) - clss = r.boxes.cls.cpu().numpy().astype(int) - confs = r.boxes.conf.cpu().numpy() - off_x, off_y = cropped_coords[ridx] - - for j in range(len(boxes)): - num_objs += 1 - bx1, by1, bx2, by2 = boxes[j] - abs_x1, abs_y1 = off_x + bx1, off_y + by1 - abs_x2, abs_y2 = off_x + bx2, off_y + by2 - class_id = clss[j] - class_name = label_source[class_id] - confidence = confs[j] - if confidence > DETECTION_THRESHOLD: - if not OMIT_DETECTIONS_FLAG: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - print( - # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", - f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", - flush=True, - ) - - bb_color = get_detection_color(class_id, is_bgr=True) - - cv2.rectangle( - foi, - (abs_x1, abs_y1), - (abs_x2, abs_y2), - bb_color, - thickness, - ) - label = f"{class_name} {confidence:.2f}" - draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) - - height = min(abs_y2, H) - max(0, abs_y1) - width = min(abs_x2, W) - max(0, abs_x1) - # object_res = [ - # abs_x1, - # abs_y1, - # height, - # width, - # class_name, - # confidence, - # H, - # W, - # ] - - # Resized - scale_x = self.resize_w / W - scale_y = self.resize_h / H - object_res = [ - int(abs_x1 * scale_x), - int(abs_y1 * scale_y), - int(height * scale_y), - int(width * scale_x), - class_name, - confidence, - int(self.resize_h), - int(self.resize_w), - ] - - framenum_str = f"{frameNum:04d}_{j:04d}" - if DEBUG_FLAG: - meta_str = ",".join( - [str(o) for o in object_res + [framenum_str]] - ) - print(f"[{stream_name} METADATA],{meta_str}", flush=True) - - # Full Res - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": { - "x": int(object_res[0]), - "y": int(object_res[1]), - "height": int(object_res[2]), - "width": int(object_res[3]), - "object": str(object_res[4]), - "object_det": { - "confidence": float(object_res[5]), - "frameH": int(object_res[6]), - "frameW": int(object_res[7]), - }, - }, - } - - # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) - frame_bytes = get_display_frame_in_bytes( - foi, self.frame_width, display_size=(1280, 720), quality=50 - ) - - return metadata, frame_bytes - - def release_clip_and_reencode(self): - if self.video_writer is not None: - threading.Thread( - target=save_and_finalize_clip, - args=( - self.clip_key, - self.video_writer, - self.clip_filename, - self.tmp_file, - self.target_fps, - MODEL_W, - MODEL_H, - # self.frame_width, - # self.frame_height, - ), - daemon=True, - ).start() - - self.video_writer = None - self.clip_id += 1 - - def contour2predictions( - self, frameNum, mask, frame, device_input="cpu", repeat_count=1 - ): - # source = self.source - # stream_name = self.name - contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - # 3. Write frame - # self.video_writer.write(frame) - # if self.video_writer: - # for _ in range(repeat_count): - # self.video_writer.write(self.cpu_resized_frame) - - # num_objs = 0 - # predictions = [] - metadata = dict() - if contours: - metadata, frame_bytes = self.get_detections_for_contours_bbs( - frameNum, frame, contours, thickness=2, device_input=device_input - ) - - if metadata: - all_metadata.setdefault( - self.clip_key, - { - "object": {}, - "face": {}, - }, - ) - all_metadata[self.clip_key]["object"].update(metadata) - # all_metadata[clip_key]["face"].update(metadata_face) - return frame_bytes - - def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation - H, W = self.resize_h, self.resize_w - self.cpu_resized_frame = cv2.resize(frame, (W, H)) - self.video_writer.write(self.cpu_resized_frame) - - # Background Subtraction on CPU - fgMask = self.backSub_cpu.apply(self.cpu_resized_frame, learningRate=self.lr) - - prev_bkgd = np.ones_like(fgMask) # AND - for m in self.mask_history: - # Dilate the historical mask - dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) - cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) - self.mask_history.append(fgMask) - - if prev_bkgd.max() != prev_bkgd.min(): - combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) - - # Convert the boolean array back to uint8 with 0 and 255 values - fgMask = combined_mask_bool.astype(np.uint8) * 255 - - # Thresholding - _, mask = cv2.threshold( - fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY - ) - - mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - - # Get Contours & Run Inference on detection_device - device_input = ( - self.operation_device_map.detection_device - if self.operation_device_map.detection_device == "cpu" - else "cuda" - ) - - # num_objs, predictions = - frame_bytes = self.contour2predictions( - frameNum, mask, frame, device_input=device_input, repeat_count=repeat_count - ) - return frame_bytes - - -class StreamRequest(BaseModel): - url: str - name: str - - -# --------------- APP ------------------- -from contextlib import asynccontextmanager - - -async def auto_cleanup_janitor(app): - while True: - await asyncio.sleep(10) - now = time.time() - # Iterating over a list of keys to avoid "dictionary changed size" error - for name in list(app.state.active_streams.keys()): - streamer = app.state.active_streams[name] - - # Check if the stream is marked inactive OR timed out - # streamer.active should be False when the video source ends - if not streamer.active or (now - streamer.last_heartbeat > 30): - print(f"CLEANUP: Removing {name} from active_streams") - streamer.stop() - del app.state.active_streams[name] - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # This is the ONLY place this should be initialized - if not hasattr(app.state, "active_streams"): - app.state.active_streams = {} - asyncio.create_task(auto_cleanup_janitor(app)) - print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") - yield - # Cleanup logic here... - for s in app.state.active_streams.values(): - s.stop() - - -app = FastAPI(lifespan=lifespan) -templates = Jinja2Templates(directory="templates") - - @app.post("/stream") -async def stream_video( - # url: str = Query(..., description="RTSP URL or Local File Path"), - # name: str = Query(..., description="Name of stream"), - data: StreamRequest, -): +async def stream_video(data: StreamRequest): url, name = data.url, data.name # Start background thread if name not in app.state.active_streams: print(f"Starting background worker for {name}...") - app.state.active_streams[name] = VideoStreamHandler(url, name) + app.state.active_streams[name] = VideoStreamHandler( + url, name, app.state.active_streams + ) # , model=app.state.model, lock=app.state.model_lock) # DEBUG START curr_keys = list(app.state.active_streams.keys()) - print( - f"stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" - ) + if DEBUG == "1": + print( + f"stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" + ) # DEBUG END return {"status": "started", "keys": list(app.state.active_streams.keys())} -# @app.get("/view_stream", name="view_stream") -# async def view_stream(name: str): -# # DEBUG START -# curr_keys = list(app.state.active_streams.keys()) -# print( -# f"view_stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" -# ) -# # DEBUG END -# # This will now find 'test_vid' because it's the same memory! -# streamer = app.state.active_streams.get(name) - -# if not streamer: -# raise HTTPException(status_code=404, detail="Stream not found") - -# async def get_frames(): -# while streamer.active: -# frame_bytes = streamer.latest_processed_frame -# if frame_bytes is None: -# print(f"DEBUG: {streamer.name} frame is still None...") -# await asyncio.sleep(0.1) # Wait for first inference to finish -# continue - -# streamer.update_frame() - -# yield ( -# b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n" -# ) -# await asyncio.sleep(0.06) - -# return StreamingResponse( -# get_frames(), media_type="multipart/x-mixed-replace; boundary=frame" -# ) - - @app.get("/debug_frame/{name}") async def debug_frame(name: str): streamer = app.state.active_streams.get(name) @@ -1084,9 +74,10 @@ async def debug_frame(name: str): return {"error": "not found"} # DEBUG START curr_keys = list(app.state.active_streams.keys()) - print( - f"debug_frame DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" - ) + if DEBUG == "1": + print( + f"debug_frame DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" + ) # DEBUG END return { "active": streamer.active, @@ -1103,56 +94,23 @@ async def get_stream_list(request: Request): return list(request.app.state.active_streams.keys()) -# New endpoint to provide stats @app.get("/stream_stats") async def get_stats(request: Request): # Return a dict mapping camera_id to its metrics return { - cam_id: {"fps": round(state.stat_fps, 1), "frames": state.stat_frame_count} - for cam_id, state in request.app.state.active_streams.items() + name: { + "fps": round(streamer.stat_fps, 1), + "frames": streamer.stat_frame_count, + # "status": + } + for name, streamer in request.app.state.active_streams.items() } -@app.get("/") -async def index(request: Request): - """Renders the dashboard.""" - print(f"Active Streams: {app.state.active_streams.keys()}") # Check your terminal! - curr_keys = list(app.state.active_streams.keys()) - return templates.TemplateResponse( - "index.html", {"request": request, "cameras": curr_keys} - ) - - -# @app.get("/view_stream", name="view_stream") -# async def view_stream(name: str, request: Request): -# # Initialize state if it doesn't exist -# # if name not in app.state.active_streams: -# # app.state.active_streams[name] = CameraState() - -# async def frame_generator(): -# try: -# while True: -# # Check if client disconnected to stop processing immediately -# if await request.is_disconnected(): # -# break - -# frame_bytes = app.state.active_streams[name].latest_processed_frame -# if frame_bytes is None: -# print(f"DEBUG: {app.state.active_streams[name].name} frame is still None...") -# await asyncio.sleep(0.1) # Wait for first inference to finish -# continue - -# app.state.active_streams[name].update_frame() - -# # app.state.active_streams[name].frame_count += 1 -# yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n') -# finally: -# # THIS IS THE FIX: Remove the camera from the list when stream ends -# print(f"Stream ended: {name}. Cleaning up.") -# if name in app.state.active_streams: -# del app.state.active_streams[name] - -# return StreamingResponse(frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame") +@app.get("/status") +async def get_status(request: Request): + # Return a dict mapping camera_id to its metrics + return {"status": app.state.status if hasattr(app.state, "status") else "Loading"} @app.get("/view_stream", name="view_stream") @@ -1169,7 +127,7 @@ async def frame_generator(): if await request.is_disconnected(): break # 2. Update Heartbeat for Auto-Cleanup - # streamer.last_heartbeat = time.time() + streamer.last_heartbeat = time.time() # 3. Only send a frame if a NEW one is ready if streamer.latest_processed_frame: # streamer.latest_processed_frame must be raw JPEG bytes @@ -1180,22 +138,9 @@ async def frame_generator(): + b"\r\n" ) - # ONLY process if there is a NEW frame from the detector - # if streamer.last_frame_id > streamer.sent_frame_id: - # frame_bytes = streamer.latest_processed_frame - # if frame_bytes: - # # Sync IDs so we don't send this one again - # streamer.sent_frame_id = streamer.last_frame_id - - # # Now this count is HONEST: 1 count = 1 unique AI frame - # streamer.update_frame() - - # yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + - # frame_bytes + b"\r\n") - # Tiny sleep (1ms) to prevent 100% CPU usage while waiting # for the next unique frame to arrive from the detector. - await asyncio.sleep(0.001) + await asyncio.sleep(0.01) # finally: # if name in request.app.state.active_streams: @@ -1220,10 +165,23 @@ async def stop_stream(name: str, request: Request): # 2. Remove from the shared state del request.app.state.active_streams[name] - print(f"--- CLEANUP | Stream '{name}' stopped and removed. ---") + if DEBUG == "1": + print(f"--- CLEANUP | Stream '{name}' stopped and removed. ---") return {"status": "stopped", "camera": name} +@app.get("/dashboard_stats") +async def dashboard_stats(request: Request): + stats = {} + for name, streamer in request.app.state.active_streams.items(): + stats[name] = { + "current_fps": round(streamer.stat_fps, 2), + "reencode_backlog": streamer.get_executor_backlog(), + "total_frames": streamer.stat_frame_count, + } + return stats + + if __name__ == "__main__": import uvicorn diff --git a/fastapi/manage.sh b/fastapi/manage.sh new file mode 100755 index 0000000..f0ee94d --- /dev/null +++ b/fastapi/manage.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +/usr/local/sbin/nginx -g 'daemon on;' + +# run fastapi +exec ${VIRTUAL_ENV}/bin/python /home/main.py diff --git a/fastapi/requirements.GPU.txt b/fastapi/requirements.GPU.txt deleted file mode 100644 index 113f8d9..0000000 --- a/fastapi/requirements.GPU.txt +++ /dev/null @@ -1,6 +0,0 @@ -cuda-python #==12.2.0 -onnx>=1.12.0,<=1.19.1 -onnxruntime-gpu -onnxslim>=0.1.71 -tensorrt==10.9.0.34 #>=10.12.0.36 -# cupy==14.0.1 diff --git a/fastapi/requirements.txt b/fastapi/requirements.txt deleted file mode 100644 index 39ab158..0000000 --- a/fastapi/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -fastapi -uvicorn -opencv-python-headless #==4.12.0.88 # Newer versions (4.13.x) have packaging dependency on XCB libraries -openvino-dev==2024.6.0 -ultralytics -numpy -vdms \ No newline at end of file diff --git a/video/resources/models/download_yolo.py b/fastapi/resources/models/download_yolo.py similarity index 90% rename from video/resources/models/download_yolo.py rename to fastapi/resources/models/download_yolo.py index 1b5282e..349554f 100644 --- a/video/resources/models/download_yolo.py +++ b/fastapi/resources/models/download_yolo.py @@ -24,7 +24,14 @@ def str2bool(in_val): DEVICE = os.environ.get("DEVICE", "CPU") MODEL_NAME = os.environ.get("MODEL_NAME", "yolo11n") if DEVICE == "GPU": - EXPORT_BATCH_SIZE = int(os.environ.get("GPU_BATCH_SIZE", 1)) + # 1. Force PyTorch to initialize the CUDA context + import torch + + if torch.cuda.is_available(): + torch.cuda.set_device(0) + torch.cuda.empty_cache() + print(f"Using GPU: {torch.cuda.get_device_name(0)}") + EXPORT_BATCH_SIZE = 64 # 32, 50 # int(os.environ.get("GPU_BATCH_SIZE", 1)) run_platform_name = "engine" os.environ["CUDA_VISIBLE_DEVICES"] = "0" print("[!] USING GPU & TENSORRT") @@ -67,7 +74,8 @@ def get_model(model_dir, run_platform, device_input, batch=1, force_export=False pt_detection_model.export( format="engine", half=half_flag, - imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) + imgsz=[640, 640], + # imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) dynamic=dynamic_flag, device=device_input, simplify=True, diff --git a/video/resources/models/models.lst b/fastapi/resources/models/models.lst similarity index 100% rename from video/resources/models/models.lst rename to fastapi/resources/models/models.lst diff --git a/video/resources/models/ultralytics/custom_models/.gitignore b/fastapi/resources/models/ultralytics/custom_models/.gitignore similarity index 100% rename from video/resources/models/ultralytics/custom_models/.gitignore rename to fastapi/resources/models/ultralytics/custom_models/.gitignore diff --git a/fastapi/templates/index.html b/fastapi/templates/index.html index 57b942f..4613955 100644 --- a/fastapi/templates/index.html +++ b/fastapi/templates/index.html @@ -57,6 +57,9 @@ } .stats-bar span { color: #00ffcc; font-weight: bold; } + .stats-bar span.warning { color: #ff3333 !important; animation: blink 1.5s infinite; } + .stats-bar span.pending { color: #ffcc00; } + @keyframes blink { 50% { opacity: 0.5; } } .video-container { position: relative; @@ -64,9 +67,17 @@ border-radius: 6px; overflow: hidden; min-height: 300px; + will-change: transform; /* Hints to the browser to prepare the GPU */ } - img { width: 100%; display: block; } + img { + width: 100%; + display: block; + image-rendering: -webkit-optimize-contrast; /* Chrome/Safari */ + image-rendering: crisp-edges; /* Firefox */ + backface-visibility: hidden; /* Prevents flickers during fast updates */ + transform: translateZ(0); /* Forces Hardware Acceleration */ + } #no-streams-msg { text-align: center; @@ -82,20 +93,26 @@

Live Detection Dashboard

-
+ +
+ +
+
{% for id in cameras %}

Camera: {{ id }}

FPS: 0.0
-
Frames: 0
+
- Camera Stream {{ id }} + Camera Stream {{ id }}
{% endfor %} @@ -104,81 +121,146 @@

Camera: {{ id }}

\ No newline at end of file diff --git a/finetune/.env b/finetune/.env new file mode 100644 index 0000000..2cbb6f7 --- /dev/null +++ b/finetune/.env @@ -0,0 +1 @@ +LOCAL_DATA_DIR=/path/to/your/actual/data \ No newline at end of file diff --git a/finetune/Dockerfile b/finetune/Dockerfile new file mode 100644 index 0000000..9862abf --- /dev/null +++ b/finetune/Dockerfile @@ -0,0 +1,102 @@ +FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04 + +ENV VIRTUAL_ENV=/opt/venv +ARG DEBIAN_FRONTEND=noninteractive + +# Update and install necessary build tools and dependencies for OpenCV +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + wget \ + unzip \ + yasm \ + pkg-config \ + libgl1 \ + libglib2.0-0 \ + libgtk2.0-dev \ + libtbb-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libavcodec-dev \ + libavformat-dev \ + libswscale-dev \ + libxvidcore-dev \ + libxine2-dev \ + libv4l-dev \ + libdc1394-dev \ + libatlas-base-dev \ + gfortran \ + python3-dev \ + python3-numpy \ + python3-pip \ + python3-venv && \ + rm -rf /var/lib/apt/lists/* +RUN python3 -m venv ${VIRTUAL_ENV} +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +RUN pip3 install pip --no-cache-dir --upgrade && \ + pip3 install --no-cache-dir torch torchvision "numpy<2.0" + +# Define environment variables for OpenCV version and installation path +ENV OPENCV_VERSION="4.10.0" +ENV PYTHON_VERSION="3.$(${VIRTUAL_ENV}/bin/python -V | cut -f 1 | cut -d '.' -f 2)" +ENV DEPENDENCY_DIR=/tmp/build_opencv +ENV PYTHONPATH=${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages:${DEPENDENCY_DIR}/opencv/modules + +# Working directory for source code +WORKDIR ${DEPENDENCY_DIR} + +# Clone OpenCV and OpenCV Contrib repositories +RUN git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv.git ${DEPENDENCY_DIR}/opencv&& \ + git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv_contrib.git ${DEPENDENCY_DIR}/opencv_contrib + +# Create build directory and run CMake +WORKDIR ${DEPENDENCY_DIR}/opencv/build +RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ + -D BUILD_EXAMPLES=OFF \ + -D BUILD_JAVA=OFF \ + -D BUILD_opencv_python2=OFF \ + -D BUILD_opencv_python3=ON \ + -D BUILD_PERF_TESTS=OFF \ + -D BUILD_TESTS=OFF \ + -D CMAKE_INSTALL_PREFIX=${VIRTUAL_ENV} \ + -D CUDA_ARCH_BIN=70,75,80,86,89,90 \ + -D CUDA_FAST_MATH=ON \ + -D ENABLE_FAST_MATH=ON \ + -D INSTALL_PYTHON_EXAMPLES=OFF \ + -D INSTALL_C_EXAMPLES=OFF \ + -D OPENCV_EXTRA_MODULES_PATH=${DEPENDENCY_DIR}/opencv_contrib/modules \ + -D PYTHON_DEFAULT_EXECUTABLE=${VIRTUAL_ENV}/bin/python \ + -D PYTHON3_EXECUTABLE=${VIRTUAL_ENV}/bin/python \ + -D PYTHON3_NUMPY_INCLUDE_DIRS=$(${VIRTUAL_ENV}/bin/python -c "import numpy; print(numpy.get_include())") \ + -D WITH_CUBLAS=ON \ + -D WITH_CUDA=ON \ + -D WITH_CUDNN=ON \ + -D WITH_LIBV4L=ON \ + -D WITH_NVCUVENC=ON \ + -D WITH_NVCUVID=ON \ + .. && \ + make -j$(nproc) && \ + make install && \ + ldconfig + +# Clean up +RUN rm -rf ${DEPENDENCY_DIR} + +# Set environment variables for installed OpenCV +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${VIRTUAL_ENV}/lib + +WORKDIR /home +# COPY .vscode /home/.vscode +# COPY configs /app/configs +# COPY include /app/include +# COPY inputs /app/inputs +# COPY models /app/models +# COPY *.py /app/ +COPY requirements.txt /home/ + +RUN pip3 install --no-cache-dir -r /home/requirements.txt + +ENTRYPOINT python3 /home/finetune_test.py 2>&1 | tee /home/finetune.log \ No newline at end of file diff --git a/finetune/app/finetune.py b/finetune/app/finetune.py new file mode 100644 index 0000000..0879004 --- /dev/null +++ b/finetune/app/finetune.py @@ -0,0 +1,205 @@ +################################################################################# +# SCRIPT IS TEST FOR FINETUNING YOLO MODEL +# This script uses ultralytics for training +# Next step is to modify without Ultralytics FW (possible?) +# +# NOTE: To keep original classes, they must be included in dataset; (2nd attempt) +# Otherwise only new classes will be detected (1st attempt) +################################################################################# + +import gc +import os +import time +from pathlib import Path + +from include.utils import ( + DETECTION_THRESHOLD, + IOU_THRESHOLD, + convert_SynDroneVision_2_Train_Structure, + get_logger, +) +from torch.cuda import empty_cache +from ultralytics import YOLO + +# WORKSPACE = Path(__file__).parent +TRAIN_MODEL = True +WORKSPACE = Path("/workspace/app") +SYSTEM_DATA_DIR = Path( + # "/data1/dataset" + "/workspace/dataset" +) # Path(__file__).parent # Where to store data +LOCAL_DATA_DIR = Path(__file__).parent / "data" # Where data info yamls are stored +ORIGINAL_DATA_DIR = SYSTEM_DATA_DIR / "SynDroneVision" +DATA_DIR = SYSTEM_DATA_DIR / "SynDroneVision_yolo" +YAML_PATH = DATA_DIR / "drones.yaml" +RESULT_DIR = WORKSPACE / "SynDroneVision-Results" +PROJECT_NAME = str(RESULT_DIR / "finetune_revised_3.9") + +TRAIN_RUN_NAME = "train_output" +DEVICES = [4, 5] # [0, 1] +os.environ["CUDA_VISIBLE_DEVICES"] = ",".join([str(d) for d in DEVICES]) +BATCH_SIZE = 16 # 8 # 16 +NUM_EPOCHS = 100 # 60 +IMGZ_SHAPE = 1280 # 1024 # 640 #Image shape: 2560x1489 too large, using 1280 +LEARNING_RATE = 0.001 # 0.001 +OPTIMIZER_NAME = "AdamW" +RECT_FLAG = False # True # Enables minimum padding strategy; cannot use with multi-gpu training +WARMUP_EPOCHS = ( + 3 # Set to 0 to prevent the learning rate from starting too low [Default: 3] +) +PATIENCE = 20 # 5 # Automatically stops training if no improvement after P epochs [Default: 100] +MULTI_SCALE = 0 # .75 #True # Change imgsz by up to a factor of 0.5 during training to be more accurate with multiple imgsz during inference +SCALE = 0.8 # Default:0.5 This tells YOLO to zoom in significantly on your 2560px images during training, effectively creating "crops" on the fly that keep the drone closer to its original size + +VAL_RUN_NAME = "val_output" +NUM_WORKER_THREADS = 2 # 4 + +DETECT_RUN_NAME = "predict_output" +TEST_VIDEO = "./inputs/anduril_swarm.mp4" +# TEST_VIDEO = "inputs/pexels-joseph-redfield-8459631 (1080p).mp4" + +Path(PROJECT_NAME).mkdir(parents=True, exist_ok=True) + +from datetime import datetime + +timestamp = datetime.now().strftime("%Y%m%d") +# log_filename = LOGS_DIR / f"{operation}_{timestamp}.log" +log_filename = Path(PROJECT_NAME) / f"finetune_test_{timestamp}.log" +logger = get_logger(log_filename) +logger.info( + f"🚀 Logging initialized. Writing to screen and {log_filename.relative_to(WORKSPACE)}" +) + +# Convert Data Structure to Structure of Training +if not (DATA_DIR / "train/images").exists(): + convert_SynDroneVision_2_Train_Structure(ORIGINAL_DATA_DIR, DATA_DIR) + + +# Generate dataset configuration for dataset +if not YAML_PATH.exists(): + data_info_content = f""" +# Dataset root directory +path: {DATA_DIR} # dataset root dir +train: train # train images relative path +val: val # validation images relative path +test: test # test images relative path (optional) + + +# Num. of classes +nc: 1 + +# Classes +names: ["drone"] + """ + + with open(YAML_PATH, "w") as f: + f.write(data_info_content) + + +# Create Results directory +if not RESULT_DIR.exists(): + RESULT_DIR.mkdir(parents=True, exist_ok=True) + + +""" TRAIN """ +if TRAIN_MODEL: + model = YOLO(RESULT_DIR / "yolo11n.pt") + start_train = time.time() + results = model.train( + batch=BATCH_SIZE, + data=YAML_PATH, + epochs=NUM_EPOCHS, + imgsz=IMGZ_SHAPE, + lr0=LEARNING_RATE, + optimizer=OPTIMIZER_NAME, + project=PROJECT_NAME, + name=TRAIN_RUN_NAME, + device=DEVICES, + patience=PATIENCE, + multi_scale=MULTI_SCALE, + workers=NUM_WORKER_THREADS, + rect=RECT_FLAG, + warmup_epochs=WARMUP_EPOCHS, + close_mosaic=10, # Turn off mosaic earlier to stabilize + scale=SCALE, # set scale=0.8 or higher (the default is usually 0.5) + ) + train_time = time.time() - start_train + empty_cache() # Frees memory no longer used + gc.collect() # Forces garbage collector +else: + if ( + Path(f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/results.csv").exists() + and not Path(f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/results.png").exists() + ): + from ultralytics.utils.plotting import plot_results + + plot_results(file=f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/results.csv") + + +""" VALIDATION """ +# Check latest directory in case of multiple training runs +idx_run = 0 +original_TRAIN_RUN_NAME = TRAIN_RUN_NAME +for train_runs in Path(PROJECT_NAME).glob(f"{original_TRAIN_RUN_NAME}*"): + name_idx_str = train_runs.name.replace(original_TRAIN_RUN_NAME, "") + if name_idx_str != "" and int(name_idx_str) > idx_run: + TRAIN_RUN_NAME = train_runs.name + idx_run = int(name_idx_str) + +model = YOLO(f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/weights/best.pt") +start_val = time.time() +val_result = model.val( + batch=BATCH_SIZE, + data=YAML_PATH, + imgsz=IMGZ_SHAPE, + conf=DETECTION_THRESHOLD, + iou=IOU_THRESHOLD, + split="test", + project=PROJECT_NAME, + name=VAL_RUN_NAME, + workers=NUM_WORKER_THREADS, + device=",".join([f"cuda:{d}" for d in DEVICES]), +) +val_time = time.time() - start_val +empty_cache() # Frees memory no longer used +gc.collect() # Forces garbage collector + + +""" DETECT """ +start_detect = time.time() +result = model.predict( + source=TEST_VIDEO, + conf=DETECTION_THRESHOLD, + iou=IOU_THRESHOLD, + show=False, + imgsz=IMGZ_SHAPE, + save=True, + project=PROJECT_NAME, + name=DETECT_RUN_NAME, + exist_ok=False, # overwrite if folder exists + device=DEVICES[0], +) +detect_time = time.time() - start_detect +empty_cache() # Frees memory no longer used +gc.collect() # Forces garbage collector + + +""" SUMMARY """ +info_file = Path(PROJECT_NAME) / "Summary.txt" +with open(info_file, "w") as f: + if TRAIN_MODEL: + print( + f"Training took {train_time:0.3f} secs for bs {BATCH_SIZE} and {NUM_EPOCHS} epochs", + file=f, + ) + + print( + f"\n\nValidation took {val_time:0.3f} secs", + file=f, + ) + print("mAP50-95:", val_result.box.map, file=f) + print("mAP50:", val_result.box.map50, file=f) + print("mAP75:", val_result.box.map75, file=f) + print("mAP:", val_result.box.maps, file=f) + + print(f"\n\nDetection took {detect_time:0.3f} secs", file=f) diff --git a/finetune/app/include/utils.py b/finetune/app/include/utils.py new file mode 100644 index 0000000..1e93cb3 --- /dev/null +++ b/finetune/app/include/utils.py @@ -0,0 +1,132 @@ +import logging +import shutil +import sys +from pathlib import Path + +from colorlog import ColoredFormatter + +# Model Variables +DETECTION_THRESHOLD = 0.25 +DYNAMIC_FLAG = True +HALF_FLAG = True +IOU_THRESHOLD = 0.5 # 0.7 +MAX_DETECTIONS = 300 +MODEL_W, MODEL_H = (640, 640) + +from ultralytics.utils import LOGGER as ULTRALYTICS_LOGGER + + +class LoggerWriter: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): + self.logger.log(self.level, message.strip()) + + def flush(self): + pass + + def isatty(self): + return False + + +def get_logger(log_filename): + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + # 2. Configure specific library levels + # (This controls how much detail you want from each) + ULTRALYTICS_LOGGER.propagate = True + ultra_logger = logging.getLogger("ultralytics") + ultra_logger.propagate = True + ultra_logger.setLevel(logging.INFO) + logging.getLogger("openvino").setLevel(logging.INFO) + + # Define the format (added date to file, kept succinct for screen) + # We color the name of the logger (e.g., kiss, stdout, main) differently + console_formatter = ColoredFormatter( + "%(log_color)s%(levelname)-8s%(reset)s | %(name_log_color)s%(name)-12s%(reset)s | %(message)s", + log_colors={ + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "bold_red", + }, + secondary_log_colors={ + "name": { + "stdout": "purple", # Prints will be purple + "stderr": "red", # Stderr will be red + "ultralytics": "blue", # ultralytics logs will be blue + "openvino": "cyan", # GEPA logs will be cyan + "main": "white", # Main program is white + } + }, + style="%", + ) + file_formatter = logging.Formatter( + "%(asctime)s | %(name)s | %(levelname)s | %(message)s" + ) + # console_formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") + + # Create StreamHandler (Screen) + console_handler = logging.StreamHandler(sys.__stdout__) + console_handler.setFormatter(console_formatter) + + # Create FileHandler (File) + file_handler = logging.FileHandler(log_filename, mode="w") + file_handler.setFormatter(file_formatter) + + # 5. Add handlers to ROOT instead of specific loggers + if not root_logger.handlers: + root_logger.addHandler(console_handler) + root_logger.addHandler(file_handler) + + # 3. Redirect STDOUT and STDERR + # Assigning to 'main' logger is fine, but root is often easier + sys.stdout = LoggerWriter(logging.getLogger("stdout"), logging.INFO) + sys.stderr = LoggerWriter(logging.getLogger("stderr"), logging.ERROR) + + return root_logger + + +def copy_file(src: Path, dst: Path): + if not dst.exists(): + try: + shutil.copy2(src, dst) + except Exception as e: + raise ValueError(f"Error occurred during copy: {e}") + else: + # pass + raise FileExistsError(f"File exists: {dst}") + + +def convert_SynDroneVision_2_Train_Structure(ORIGINAL_DATA_DIR, DATA_DIR): + for stage in ["train", "val", "test"]: + (DATA_DIR / f"{stage}/images").mkdir(parents=True, exist_ok=True) + (DATA_DIR / f"{stage}/labels").mkdir(parents=True, exist_ok=True) + + # Copy files to new folder + for src_file in (ORIGINAL_DATA_DIR / f"images/{stage}").rglob("*.png"): + src_label_str = str(src_file.with_suffix(".txt")) + src_label_file = Path(src_label_str.replace("/images/", "/labels/")) + + # if src_label_file.exists(): + dest = DATA_DIR / f"{stage}/images/{src_file.parent.name}_{src_file.name}" + copy_file(src_file, dest) + + if src_label_file.exists(): + dest_label = ( + DATA_DIR + / f"{stage}/labels/{src_label_file.parent.name}_{src_label_file.name}" + ) + copy_file(src_label_file, dest_label) + elif Path(str(src_label_file).replace(".txt", "")).exists(): + src_label_file = Path(str(src_label_file).replace(".txt", "")) + dest_label = ( + DATA_DIR + / f"{stage}/labels/{src_label_file.parent.name}_{src_label_file.name}.txt" + ) + copy_file(src_label_file, dest_label) diff --git a/finetune/app/requirements.txt b/finetune/app/requirements.txt new file mode 100644 index 0000000..d1993c8 --- /dev/null +++ b/finetune/app/requirements.txt @@ -0,0 +1,25 @@ +# fastapi +# uvicorn +# opencv-python-headless==4.11.0.86 #==4.12.0.88 # Newer versions (4.13.x) have packaging dependency on XCB libraries +# openvino-dev==2024.6.0 +pip==25.3 +numpy<2.0 +ultralytics==8.4.7 +# vdms==0.0.22 +wheel==0.46.3 +# nncf==2.19.0 +torch +torchvision + +# cuda-python==12.2.0 +# onnx>=1.12.0,<=1.19.1 +# onnxruntime-gpu +# onnxslim>=0.1.71 +# tensorrt==10.9.0.34 #>=10.12.0.36 + +nvidia-cuda-runtime-cu12==12.8.* +onnx==1.20.1 +onnxruntime-gpu==1.23.2 +onnxslim==0.1.82 +tensorrt_cu12==10.14.1.48.post1 +nvidia-cudnn-cu12 \ No newline at end of file diff --git a/finetune/docker-compose.yml b/finetune/docker-compose.yml new file mode 100644 index 0000000..9e29108 --- /dev/null +++ b/finetune/docker-compose.yml @@ -0,0 +1,64 @@ + +services: + finetune-service: + # image: lcc_finetune:latest + build: ./ + container_name: finetune_model + privileged: true + network_mode: "host" + ipc: "host" + shm_size: '2gb' # Give it plenty of space for video frames + environment: + YOLO_CONFIG_DIR: "/tmp" + DBHOST: "vdms-service" + UDF_HOST: "udf-service" + MODEL_NAME: "yolo11n" + CUSTOM_MODEL_FLAG: "False" + RESIZE_FLAG: "True" + OMIT_DETECTIONS_FLAG: "False" + CPU_BATCH_SIZE: 1 + GPU_BATCH_SIZE: 1 + DEBUG: "1" + DEVICE: "GPU" + INGESTION: "object,face" + WATCH_DIR: "/watch_dir" + http_proxy: "${http_proxy}" + HTTP_PROXY: "${HTTP_PROXY}" + https_proxy: "${https_proxy}" + HTTPS_PROXY: "${HTTPS_PROXY}" + no_proxy: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${no_proxy}" + NO_PROXY: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${NO_PROXY}" + volumes: + - /etc/localtime:/etc/localtime:ro + - ../../inputs:/watch_dir:ro + - ./app:/home + - ${LOCAL_DATA_DIR}:/dataset + networks: + - appnet + restart: always + runtime: nvidia + deploy: + resources: + reservations: + devices: + - driver: nvidia + capabilities: [gpu] + count: all + + +# secrets: +# self_key: +# file: ../certificate/self.key +# self_crt: +# file: ../certificate/self.crt + + +# networks: +# appnet: +# driver: overlay +# attachable: true + + +# volumes: +# app-content: +# vdms-content: diff --git a/start_app.sh b/start_app.sh index 3f03e56..a8b6a0a 100755 --- a/start_app.sh +++ b/start_app.sh @@ -3,7 +3,7 @@ # This script runs the Curation application ####################################################################################################################### # DEFAULT VARIABLES -INGESTION="object,face" +INGESTION="object" #,face" EXP_TYPE=compose DEBUG="0" DEVICE="CPU" diff --git a/video/Dockerfile b/video/Dockerfile index 77ee34a..bfc4310 100644 --- a/video/Dockerfile +++ b/video/Dockerfile @@ -9,12 +9,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 RUN apt-get update +# RUN apt-get install --only-upgrade libc-bin libc6 && \ +# apt-get install -y -q --no-install-recommends python3-setuptools \ +# python3-tornado curl libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* RUN apt-get install --only-upgrade libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-setuptools \ - python3-tornado curl libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* + python3-tornado python3-ply python3-pip python3-venv \ + && rm -rf /var/lib/apt/lists/* -COPY --from=lcc_base_video_image:latest ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --from=lcc_base_video_image:latest /home /home +RUN python3 -m venv ${VIRTUAL_ENV} + +# COPY --from=lcc_base_video_image:latest ${VIRTUAL_ENV} ${VIRTUAL_ENV} +# COPY --from=lcc_base_video_image:latest /home /home # activate virtual environment ENV PATH="$VIRTUAL_ENV/bin:$PATH" @@ -27,24 +33,26 @@ COPY requirements.* /home/ ARG DEVICE="CPU" ENV DEVICE="${DEVICE}" -RUN if [ "${DEVICE}" = "CPU" ]; then \ - pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ - else \ - pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ - fi; +# RUN if [ "${DEVICE}" = "CPU" ]; then \ +# pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ +# else \ +# pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ +# fi; # RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ # pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt -RUN pip3 install --no-cache-dir -r /home/requirements.in && \ - pip3 install --no-cache-dir -r /home/requirements.${DEVICE}.txt +RUN pip3 install --no-cache-dir -r /home/requirements.in +# && \ +# pip3 install --no-cache-dir -r /home/requirements.${DEVICE}.txt COPY *.py /home/ COPY *.yaml /home/ COPY *.conf /etc/nginx/ COPY cleanup.sh /home/ COPY manage.sh /home/ -COPY frontend /home/frontend +# COPY frontend /home/frontend # COPY include /home/include -CMD ["/bin/bash","-c","python /home/resources/models/download_yolo.py && /home/manage.sh"] +# CMD ["/bin/bash","-c","python /home/resources/models/download_yolo.py && /home/manage.sh"] +CMD ["/bin/bash","-c","/home/manage.sh"] #### ARG USER diff --git a/video/manage.sh b/video/manage.sh index 59f0c02..9d1ffc1 100755 --- a/video/manage.sh +++ b/video/manage.sh @@ -2,7 +2,7 @@ # Watch directory echo "WATCH_DIR: ${WATCH_DIR}" -python3 /home/watch_and_send2vdms.py ${WATCH_DIR} & +python3 /home/source_watcher.py ${WATCH_DIR} & # run tornado exec ${VIRTUAL_ENV}/bin/python /home/manage.py diff --git a/video/source_watcher.py b/video/source_watcher.py new file mode 100644 index 0000000..9abcac3 --- /dev/null +++ b/video/source_watcher.py @@ -0,0 +1,317 @@ +import logging +import multiprocessing as mp +import os +import sys +import time +import traceback +from multiprocessing.managers import BaseManager +from pathlib import Path + +import requests +import yaml +from inotify.adapters import Inotify + +logger = logging.getLogger(__name__) + +BACKEND_URL = os.getenv("BACKEND_URL", "http://fastapi-service:8000") +DEBUG = os.environ["DEBUG"] +DEBUG_FLAG = True if DEBUG == "1" else False +ACCEPTED_VIDEO_FORMATS = [".mp4", ".mkv", ".avi"] +num_workers = mp.cpu_count() // 2 +empty_timeout = 5 * 60 + + +# --- HELPER FUNCTIONS --- +def is_file_ready(filepath, retries=5, delay=1): + """ + Checks if a file is fully written and accessible. + Uses exponential backoff for retries. + """ + for _ in range(retries): + try: + # Try to open the file in append mode to check for locks + with open(filepath, "r"): + return True + except (IOError, OSError): + logger.warning(f"File {filepath} is locked. Retrying in {delay}s...") + time.sleep(delay) + delay *= 2 # Exponential backoff + return False + + +def connect_to_app(): + # is_ready = False + # while not is_ready: + with requests.Session() as session: + while True: + try: + res = session.get(f"{BACKEND_URL}/status") + + if res.status_code == 200: + # Expected format: {"cam_1": {"status": "Ready"}, ...} + data = res.json() + + is_ready = any(status == "Ready" for status in data.values()) + if is_ready: + break + except requests.exceptions.RequestException: + logger.info("BACKEND: Waiting for fastapi-service ...") + + time.sleep(1) + + +# --- WATCHERS & MANAGERS --- +class QueueManager(BaseManager): + pass + + +def worker_process(queue): + """Consumes tasks from the queue and sends them to the backend.""" + while True: + try: + # Wait for a task; timeout prevents hanging on shutdown + task = queue.get(timeout=10) + if task is None: + break # Sentinel value to stop worker + + source, camera_name = task + start_stream_processor(source, camera_name) + except mp.queues.Empty: + continue + except Exception as e: + logger.error(f"Worker error: {e}") + + +def watch_video_files(queue, watch_dir): + # Initial scan of files + with os.scandir(watch_dir) as entries: + for entry in entries: + # entry.is_file() and entry.name are retrieved in one go + if entry.is_file() and any( + entry.name.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS + ): + source = entry.path # Full path is already available + + if is_file_ready(source): + queue.put((source, None)) + logger.info(f"{source} added to queue: {time.time()}") + # start_stream_processor(source, None) + + # Watch watch_dir for new files + i = Inotify() + i.add_watch(watch_dir) + logger.info("START ADDING VIDEO FILES TO WATCHED DIRECTORY") + + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + + # New file created in watched directory + # IN_CLOSE_WRITE is better than IN_CREATE because it triggers + # only after the writing process finishes and closes the file. + if "IN_CLOSE_WRITE" in type_names and any( + filename.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS + ): + source = os.path.join(path, filename) + + if is_file_ready(source): + queue.put((source, None)) + logger.info(f"{source} added to queue: {time.time()}") + # start_stream_processor(source, None) + else: + logger.error(f"Failed to access {source} after retries. Skipping.") + + +def retrieve_camera_details(queue, config_path): + """ + Watches config file and adds only new streams (via camera_name) to queue + """ + unique_camera_names = set() + + def read_config_and_queue(): + try: + with open(config_path, "r") as inFile: + config = yaml.safe_load(inFile) + + if config: + for camera_name, camera_details in config.items(): + # run_processor(camera_details["url"], camera_name=camera_name) + if isinstance(camera_details, dict) and "url" in camera_details: + source = camera_details["url"] + if source not in unique_camera_names: + logger.info(f"{source} added to queue: {time.time()}") + queue.put((source, camera_name)) + unique_camera_names.add(source) + # start_stream_processor(source, camera_name) + else: + logger.warning(f"Invalid entry: {camera_name}") + except Exception: + e = traceback.format_exc() + logger.info(f"Unexpected error processing config file: {e}") + + # Initial scan of config file + read_config_and_queue() + + # Watch for changes to file + i = Inotify() + config_dir = os.path.dirname(os.path.abspath(config_path)) + i.add_watch(config_dir) + + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + + if filename == os.path.basename(config_path) and "IN_CLOSE_WRITE" in type_names: + time.sleep(0.5) + read_config_and_queue() + + +def unified_watcher(queue, watch_dir, config_path): + """ + Watches directory for both videos and config changes. + """ + unique_camera_names = set() + config_filename = os.path.basename(config_path) + + def read_config(): + try: + with open(config_path, "r") as f: + config = yaml.safe_load(f) + if config: + for name, details in config.items(): + url = details.get("url") + if url and url not in unique_camera_names: + logger.info(f"CAMERA: {name} added to queue: {time.time()}") + queue.put((url, name)) + unique_camera_names.add(url) + except Exception as e: + logger.error(f"CONFIG ERROR: {e}") + + # Initial scan of config file + read_config() + + # Initial scan of video files + with os.scandir(watch_dir) as entries: + for entry in entries: + if entry.is_file() and any( + entry.name.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS + ): + if is_file_ready(entry.path): + queue.put((entry.path, None)) + + # Unified Event Loop + i = Inotify() + i.add_watch(watch_dir) + logger.info(f"UNIFIED WATCHER: Monitoring {watch_dir} for videos and config...") + + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + full_path = os.path.join(path, filename) + + # Handle Config Update (Supports Atomic Saves/Moves) + if filename == config_filename: + if any(ev in type_names for ev in ["IN_CLOSE_WRITE", "IN_MOVED_TO"]): + time.sleep(0.2) + read_config() + + # Handle New Video Files + elif any(filename.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS): + if "IN_CLOSE_WRITE" in type_names: + if is_file_ready(full_path): + queue.put((full_path, None)) + logger.info(f"VIDEO: {full_path} added to queue: {time.time()}") + + +# --- STREAMER --- +def start_stream_processor(source, camera_name): + if camera_name is None: + camera_name = Path(source).stem + + payload = {"url": str(source), "name": camera_name} + + for attempt in range(1, 11): + try: + # FastAPI stream_processor + res = requests.post(f"{BACKEND_URL}/stream", json=payload) + + # Raise an exception for 4xx or 5xx status codes + res.raise_for_status() + + if res.status_code == 200: + logger.info(f"Started {source} process on attempt {attempt}.") + return res + + except requests.exceptions.HTTPError as e: + # Handle specific API errors (e.g., 400 Bad Request, 500 Internal Server Error) + logger.info(f"HTTP Error: {e.response.status_code} - {e.response.text}") + + # If it's a client error (4xx), retrying likely won't help + if 400 <= e.response.status_code < 500: + break + + except requests.exceptions.RequestException as e: + # Handle connection issues, timeouts, and DNS errors + logger.info(f"Connection attempt {attempt} failed: {e}") + + except Exception: + e = traceback.format_exc() + logger.info(f"Unexpected error processing {source}: {e}") + + time.sleep(1) + + +# --- MAIN FUNCTION --- +def main(watch_folder=os.getcwd()): + # Wait until App is ready + connect_to_app() + + if DEBUG_FLAG: + logger.info("[TIMING],start_watchandsend,," + str(time.time())) + + # Setup the Shared Queue and Manager + file_queue = mp.Queue() + QueueManager.register("get_file_queue", callable=lambda: file_queue) + + # Start the manager on port 5005 + manager = QueueManager(address=("0.0.0.0", 5005), authkey=b"password123") + manager.start() + logger.info("Queue Server started at 0.0.0.0:5005") + + # Define worker processes + processes = [ + mp.Process( # Process that retrieves camera info from config file (added to file_queue) + target=unified_watcher, + args=(file_queue, watch_folder, "/home/camera_config.yaml"), + name="UnifiedWatcher", + daemon=True, + ), + ] + + for _ in range(num_workers): + p = mp.Process(target=worker_process, args=(file_queue,), daemon=True) + processes.append(p) + + # 4. Start processed and Keep the server alive + try: + # Start all processes + for p in processes: + p.start() + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Stopping services ...") + finally: + # Cleanup + for p in processes: + if p.is_alive(): + p.terminate() + p.join() # Ensure fully closed + + manager.shutdown() + if DEBUG_FLAG: + logger.info("[TIMING],end_watchandsend,," + str(time.time())) + + +if __name__ == "__main__": + if len(sys.argv) == 2: + main(sys.argv[1]) + else: + raise ValueError("Invalid input. Please provide watch directory.") diff --git a/video/watch_and_send2vdms.py b/video/watch_and_send2vdms.py deleted file mode 100644 index c9a176c..0000000 --- a/video/watch_and_send2vdms.py +++ /dev/null @@ -1,206 +0,0 @@ -import multiprocessing as mp -import os -import sys -import time -from multiprocessing.managers import BaseManager -from pathlib import Path - -import requests -import yaml -from inotify.adapters import Inotify - -num_workers = mp.cpu_count() // 2 - -DEBUG = os.environ["DEBUG"] -DEBUG_FLAG = True if DEBUG == "1" else False - -BACKEND_URL = "http://fastapi-service:8000" - -# Exit program if queue is empty for 5 minutes -empty_timeout = 5 * 60 - - -# 1. Define the Manager -class QueueManager(BaseManager): - pass - - -# ENABLE_STREAMLIT = os.environ["ENABLE_STREAMLIT"] -# if ENABLE_STREAMLIT: -# from frontend.detection_runner import LiveDetectionRunner - -# n_cols = 2 -# pipeline = LiveDetectionRunner(n_cols=n_cols) -# pipeline.setup_page() - - -# def run_processor(path_or_url, camera_name=None): -# cmd = [ -# sys.executable, -# "/home/process_stream.py", -# path_or_url, -# # camera_name, -# ] -# if camera_name is not None: -# cmd.append(camera_name) - -# subprocess.run(cmd, check=True) - - -def start_stream_processor(source, camera_name): - if camera_name is None: - camera_name = Path(source).stem - - for _ in range(10): - try: - # FastAPI stream_processor - payload = {"url": str(source), "name": camera_name} - # Data is hidden in the body, no URL-encoding (%2F) mess - res = requests.post(f"{BACKEND_URL}/stream", json=payload) - # res = requests.post( - # # f"{BACKEND_URL}/stream", - # f"{BACKEND_URL}/stream?url={str(source)}&name={camera_name}", - # # json={"url": str(source), "name": camera_name}, - # timeout=10 - # ) - if res.status_code == 200: - print(f"Started {source} process.") - return res - # except requests.exceptions.ConnectionError: - # print(f"Connection reset, retrying... {res.json}") - # time.sleep(1) # Wait for buffers to clear - except Exception: # as e: - # e = traceback.format_exc() - # print(f"Error: {e}") - time.sleep(1) - - -def watch_video_files(queue, watch_dir): - # Get files already in watch_dir - for filename in os.listdir(watch_dir): - if any(filename.endswith(ext) for ext in [".mp4", ".mkv", ".avi"]): - source = os.path.join(watch_dir, filename) - print(f"{source} added to queue: {time.time()}", flush=True) - queue.put((source, None)) - start_stream_processor(source, None) - - # Watch watch_dir for new files - i = Inotify() - i.add_watch(watch_dir) - print("START ADDING VIDEO FILES TO WATCHED DIRECTORY", flush=True) - - for event in i.event_gen(yield_nones=False): - (_, type_names, path, filename) = event - - # New file created in watched directory - # if "IN_CREATE" in type_names: - if "IN_CLOSE_WRITE" in type_names and any( - filename.endswith(ext) for ext in [".mp4", ".mkv", ".avi"] - ): - source = os.path.join(path, filename) - print(f"{source} added to queue: {time.time()}", flush=True) - queue.put((source, None)) - start_stream_processor(source, None) - - -def retrieve_camera_details(queue, config_path): - config = None - with open(config_path, "r") as inFile: - config = yaml.safe_load(inFile) - - if config is not None: - for camera_name, camera_details in config.items(): - # run_processor(camera_details["url"], camera_name=camera_name) - source = camera_details["url"] - print(f"{source} added to queue: {time.time()}", flush=True) - queue.put((source, camera_name)) - start_stream_processor(source, camera_name) - - -def main(watch_folder=os.getcwd()): - if DEBUG_FLAG: - print("[TIMING],start_watchandsend,," + str(time.time()), flush=True) - - # 2. Setup the Shared Queue and Manager - file_queue = mp.Queue() - QueueManager.register("get_file_queue", callable=lambda: file_queue) - - # Start the manager on port 5000 - manager = QueueManager(address=("0.0.0.0", 5005), authkey=b"password123") - manager.start() - print("Queue Server started at 0.0.0.0:5005") - - # 3. Start worker processes - # Create a process that monitors new files in watch_folder (added to file_queue) - watcher_process = mp.Process( - target=watch_video_files, args=(file_queue, watch_folder) - ) - - # Create a process that retrieves camera info from config file (added to file_queue) - watcher_process_camera = mp.Process( - target=retrieve_camera_details, args=(file_queue, "/home/camera_config.yaml") - ) - - watcher_process.start() - watcher_process_camera.start() - - # if ENABLE_STREAMLIT: - # while True: - # if not file_queue.empty(): - # pipeline.setup_stream_section() - # break - - # 4. Keep the server alive - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - print("Stopping...") - finally: - watcher_process.terminate() - watcher_process_camera.terminate() - manager.shutdown() - - # # Pool of workers to process video clips - # # empty_queue_start = None - # empty_queue_start = time.time() - # i = 0 # stream_id - # while time.time() - empty_queue_start < empty_timeout: - # if not file_queue.empty(): - # path_or_url, camera_name = file_queue.get(timeout=0.5) - # empty_queue_start = None - # fastapi_processor = f"{BACKEND_URL}/video_feed/{camera_name}" - # if ENABLE_STREAMLIT: - # with pipeline.stream_cols[i % 2]: - # pipeline.st.subheader(f"Stream {i}: {camera_name}") - # pipeline.st.image(fastapi_processor + f"?path_or_url={path_or_url}") - # else: - # # pool.apply_async( - # # run_processor, - # # ( - # # path_or_url, - # # camera_name, - # # ), - # # ) - # _ = requests.get( - # fastapi_processor, - # data=[("path_or_url", path_or_url)], - # stream=True, - # ) - - # empty_queue_start = time.time() # Reset timer if item is processed - # else: - # time.sleep(0.1) # Sleep briefly to prevent high CPU usage - - # watcher_process.join() - # watcher_process_camera.join() - - if DEBUG_FLAG: - print("[TIMING],end_watchandsend,," + str(time.time()), flush=True) - - -if __name__ == "__main__": - if len(sys.argv) == 2: - main(sys.argv[1]) - else: - raise ValueError("Invalid input. Please provide watch directory.") From 6fad9a45f11224110acb17754167594b71e7c799 Mon Sep 17 00:00:00 2001 From: "Lacewell, Chaunte W" Date: Mon, 30 Mar 2026 09:36:14 -0700 Subject: [PATCH 04/20] demo cleanup Signed-off-by: Lacewell, Chaunte W --- .github/scripts/get_py_hashes.sh | 22 + fastapi/Dockerfile | 41 +- fastapi/include/handlers.py | 1318 ++++++++++++++++++++++- fastapi/include/utils.py | 109 ++ fastapi/main.py | 6 + {video => fastapi}/requirements.CPU.txt | 974 +++++++++-------- fastapi/requirements.GPU.txt | 673 ++++++++++++ fastapi/requirements.txt | 1307 ++++++++++++++++++++++ fastapi/templates/index.html | 12 +- finetune/Dockerfile | 12 +- finetune/app/requirements.txt | 25 - finetune/requirements.txt | 1213 +++++++++++++++++++++ frontend/Dockerfile | 7 +- frontend/requirements.txt | 19 +- udf/requirements.txt | 5 - video/Dockerfile | 12 +- video/Dockerfile.base | 34 - video/requirements.GPU.txt | 228 ---- video/requirements.txt | 1169 +++----------------- 19 files changed, 5300 insertions(+), 1886 deletions(-) create mode 100755 .github/scripts/get_py_hashes.sh rename {video => fastapi}/requirements.CPU.txt (56%) create mode 100644 fastapi/requirements.GPU.txt create mode 100644 fastapi/requirements.txt delete mode 100644 finetune/app/requirements.txt create mode 100644 finetune/requirements.txt delete mode 100644 video/Dockerfile.base delete mode 100644 video/requirements.GPU.txt diff --git a/.github/scripts/get_py_hashes.sh b/.github/scripts/get_py_hashes.sh new file mode 100755 index 0000000..9302c83 --- /dev/null +++ b/.github/scripts/get_py_hashes.sh @@ -0,0 +1,22 @@ +# pip-compile is part of pip-tools pypi package + +# FASTAPI +pip-compile -o fastapi/requirements.txt --generate-hashes fastapi/requirements.in + +# pip-compile -o fastapi/requirements.CPU.txt --generate-hashes fastapi/requirements.CPU.in +# curl -LsSf https://astral.sh/uv/install.sh | sh +uv pip compile fastapi/requirements.CPU.in -o fastapi/requirements.CPU.txt --generate-hashes + +pip-compile -o fastapi/requirements.GPU.txt --generate-hashes fastapi/requirements.GPU.in + +# FINETUNE +pip-compile -o finetune/requirements.txt --generate-hashes finetune/requirements.in + +# FRONTEND +pip-compile -o frontend/requirements.txt --generate-hashes frontend/requirements.in + +# UDF +pip-compile -o udf/requirements.txt --generate-hashes udf/requirements.in + +# VIDEO +pip-compile -o video/requirements.txt --generate-hashes video/requirements.in \ No newline at end of file diff --git a/fastapi/Dockerfile b/fastapi/Dockerfile index 42f9696..bd319c8 100644 --- a/fastapi/Dockerfile +++ b/fastapi/Dockerfile @@ -56,22 +56,9 @@ RUN apt-get update && apt-get install -y -q --no-install-recommends \ RUN python3 -m venv ${VIRTUAL_ENV} ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" ENV LD_LIBRARY_PATH="$VIRTUAL_ENV/lib:/usr/local/cuda/lib64:${LD_LIBRARY_PATH}" -ARG DEVICE="CPU" -ENV DEVICE="${DEVICE}" -# COPY requirements.* /home/ -RUN python -m pip install pip --upgrade --no-cache-dir && \ - pip3 install --no-cache-dir "fastapi==0.135.1" "uvicorn==0.42.0" "openvino-dev==2024.6.0" "numpy==1.26.4" "ultralytics==8.4.7" "vdms==0.0.22" "wheel==0.46.3" && \ - if [ "${DEVICE}" = "CPU" ]; then \ - pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ - pip3 install --no-cache-dir "nncf==2.19.0"; \ - else \ - apt-get update; \ - apt-get install -y -q --no-install-recommends cuda-toolkit-12-4 libcudnn9-cuda-12 libnvinfer10 libnvonnxparsers10 ibnvinfer-plugin10; \ - rm -rf /var/lib/apt/lists/*; \ - pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ - pip3 install --no-cache-dir "nvidia-cuda-runtime-cu12==12.8.*" "onnx==1.20.1" "onnxruntime-gpu==1.23.2" "onnxslim==0.1.82" "tensorrt_cu12==10.14.1.48.post1" "nvidia-cudnn-cu12==9.10.2.21"; \ - fi; +RUN pip3 install pip --no-cache-dir --upgrade && \ + pip3 install --no-cache-dir "torch>=2.9.1" "torchvision>=0.24.1" "numpy<2.0" ARG PYTHON_VERSION=3.10 @@ -127,6 +114,8 @@ RUN ${VIRTUAL_ENV}/bin/python -c "import cv2; print(cv2.__version__)" # Clean up RUN rm -rf ${DEPENDENCY_DIR} +ARG DEVICE="CPU" +ENV DEVICE="${DEVICE}" # Set the working directory in the container WORKDIR /home @@ -145,6 +134,28 @@ RUN if [ "${DEVICE}" != "CPU" ]; then \ ldconfig; \ fi +COPY requirements.* /home/ +# RUN python -m pip install pip --upgrade --no-cache-dir && \ +# pip3 install --no-cache-dir -r requirements.in && \ +# if [ "${DEVICE}" = "CPU" ]; then \ +# pip3 install --no-cache-dir -r requirements.CPU.in; \ +# else \ +# apt-get update; \ +# apt-get install -y -q --no-install-recommends cuda-toolkit-12-4 libcudnn9-cuda-12 libnvinfer10 libnvonnxparsers10 ibnvinfer-plugin10; \ +# rm -rf /var/lib/apt/lists/*; \ +# pip3 install --no-cache-dir -r requirements.GPU.in; \ +# fi; +RUN python -m pip install pip --upgrade --no-cache-dir && \ + pip3 install --no-cache-dir --require-hashes -r requirements.txt && \ + if [ "${DEVICE}" = "CPU" ]; then \ + pip3 install --no-cache-dir --require-hashes -r requirements.CPU.txt; \ + else \ + apt-get update; \ + apt-get install -y -q --no-install-recommends cuda-toolkit-12-4 libcudnn9-cuda-12 libnvinfer10 libnvonnxparsers10 ibnvinfer-plugin10; \ + rm -rf /var/lib/apt/lists/*; \ + pip3 install --no-cache-dir --require-hashes -r requirements.GPU.txt; \ + fi; + # RUN omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 ARG DEBUG="0" diff --git a/fastapi/include/handlers.py b/fastapi/include/handlers.py index da79659..41b378c 100644 --- a/fastapi/include/handlers.py +++ b/fastapi/include/handlers.py @@ -16,8 +16,10 @@ from contextlib import asynccontextmanager from datetime import datetime +import cupy import cv2 import numpy as np +from cucim.skimage.measure import label as cucim_label from ultralytics import YOLO from ultralytics.utils.checks import check_imgsz @@ -44,10 +46,12 @@ TARGET_FPS, YOLO_CLASS_NAMES, PipelineMapping, + bbox_kernel, draw_label, filter_contained_boxes, get_detection_color, get_display_frame_in_bytes, + gpumat2cupy, manual_fps_calculation, merge_boxes_limit, metadata2vdms, @@ -224,7 +228,132 @@ async def lifespan(app: FastAPI): app.state.status = "Stopped" -class VideoStreamHandler: +class FastPinnedReader: + def __init__(self, source, h, w, maxlen=10, target_fps=TARGET_FPS): + self.cap = cv2.VideoCapture(str(source), cv2.CAP_FFMPEG) + # Enable multi-threaded CPU decoding (1 thread per 2K pixels approx) + # self.cap.set(cv2.CAP_PROP_HW_ACCEL, 0) + # os.environ["OPENCV_FFMPEG_THREADS"] = "4" # Force 16 CPU threads + self.h, self.w = h, w + self.maxlen = maxlen + self.target_fps = target_fps + self.frame_interval = 1.0 / target_fps # 0.0666s for 15 FPS + self.frame_queue = deque( + maxlen=self.maxlen + ) # Small queue to prevent VRAM bloat + self.stopped = False + # Pre-allocate Pinned (Page-Locked) Memory for the reader thread + # self.pinned_buf_1 = cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) + # self.pinned_buf_2 = cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) + # self.buffers = [self.pinned_buf_1, self.pinned_buf_2] + self.buffers = [ + cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) + for _ in range(self.maxlen) + ] + self.thread = None + + def start(self): + self.thread = threading.Thread(target=self.update, daemon=True) + self.thread.start() + return self + + def stop(self): + """Cleanly stop the reader and release resources.""" + self.stopped = True + if self.cap.isOpened(): + self.cap.release() + self.frame_queue.clear() + # Optionally join if you want to ensure the thread is dead + # self.thread.join(timeout=1.0) + + def update(self): + idx = 0 + while not self.stopped: + ret, frame = self.cap.read() + if not ret: + self.stopped = True + break + + # Rapid copy into pinned memory for DMA upload + target_buf = self.buffers[idx % self.maxlen] + target_buf[:] = frame + self.frame_queue.append((target_buf, idx)) + idx += 1 + + # # 2. SKIP frames (Light Bitstream Parsing) + # # This is the "Compute Saver" - it bypasses the decoder for N frames + # for _ in range(self.skip_count): + # if not self.cap.grab(): + # self.stopped = True + # break + + def read(self): + return self.frame_queue.popleft() if self.frame_queue else (None, None) + + +# class FastPinnedReader: +# def __init__(self, source, h, w, maxlen=10, target_fps=15): +# self.cap = cv2.VideoCapture(str(source), cv2.CAP_FFMPEG) +# # Optimized for RTSP latency +# self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) +# self.maxlen = maxlen +# self.buffers = [cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) for _ in range(self.maxlen)] +# self.h, self.w = h, w +# self.stopped = False + +# # Pre-allocate two Pinned Buffers to prevent write-during-read +# self.buffers = [ +# cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3), +# cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) +# ] + +# self.latest_idx = 0 +# self.frame_ready = threading.Event() +# self.lock = threading.Lock() +# self.f_cnt = 0 + +# def start(self): +# self.thread = threading.Thread(target=self.update, daemon=True) +# self.thread.start() +# return self + +# def update(self): +# write_idx = 0 +# while not self.stopped: +# ret, frame = self.cap.read() +# if not ret: +# self.stopped = True +# break + +# # Switch between the two pinned buffers (Double Buffering) +# target_buf = self.buffers[write_idx % self.maxlen] +# target_buf[:] = frame + +# with self.lock: +# self.latest_idx = write_idx % self.maxlen +# self.f_cnt += 1 + +# self.frame_ready.set() # Signal inference thread that new data is here +# write_idx += 1 + +# def read(self): +# """Returns the absolute newest frame available.""" +# if not self.frame_ready.is_set(): +# return None, None + +# with self.lock: +# idx = self.latest_idx +# count = self.f_cnt +# self.frame_ready.clear() +# return self.buffers[idx], count + +# def stop(self): +# self.stopped = True +# if self.cap.isOpened(): +# self.cap.release() + + +class BaseHandler: def __init__(self, source, name, active_streams): self.model = YOLO(model_path, verbose=False, task="detect") self.name = name @@ -239,11 +368,9 @@ def __init__(self, source, name, active_streams): self.get_frameWH() # 2. Performance Tracking - self.stat_start_time = time.perf_counter() self.stat_frame_count = 0 self.stat_fps = 0 self.latest_processed_frame = None - self.last_heartbeat = time.time() self.last_frame_id = 0 self.video_writer = None @@ -279,10 +406,17 @@ def __init__(self, source, name, active_streams): # 3. Start dedicated inference thread self.model_warmup() + self.stat_start_time = time.perf_counter() + self.last_heartbeat = time.time() + self.setup_threads() + + def setup_threads(self): self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) self.process_thread = threading.Thread( target=self.run_realtime_inference, daemon=True ) + + def start(self): self.process_thread.start() def allocate_cpu(self, bkgd_mask_queue_size=3): @@ -478,14 +612,103 @@ def stop(self): # self.update_thread.join(timeout=1.0) # self.process_thread.join(timeout=1.0) - def run_pipeline(self, frame, frameNume, repeat_count=1): - # if self.device_input == "cpu": - # return self.test_full_cpu_detection_gpu( - # frame, frameNume, repeat_count=repeat_count - # ) - # else: - return self.test_rbtd_detection_gpu(frame, frameNume, repeat_count=repeat_count) + def apply_background_subtraction_gpu(self, include_history=True, method="and"): + self.fgMask = self.backSub.apply( + self.resized_frame, float(self.lr), stream=self.stream + ) + + if include_history: + for m in list(self.mask_history): + # Dilate the historical mask on GPU + dilated = self.dilate_filter_for_enhanced_mask.apply(m) + + if method == "or": + # Bitwise OR on GPU + cv2.cuda.bitwise_or(self.prev_bkgd, dilated, self.prev_bkgd) + else: + # Bitwise AND on GPU + cv2.cuda.bitwise_and(self.prev_bkgd, dilated, self.prev_bkgd) + + self.mask_history.append(self.fgMask.clone()) + min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) + + if max_val != min_val: + self.fgMask = cv2.cuda.bitwise_or(self.fgMask, self.prev_bkgd) + + def check_disk_usage(self, path, min_gb=0.5): + """Returns True if there is at least min_gb available at path.""" + try: + total, used, free = shutil.disk_usage(path) + # Convert bytes to Gigabytes + free_gb = free / (2**30) + return free_gb > min_gb + except Exception as e: + print(f"Disk check error: {e}") + return False + + # Gets video fps and framecount + def get_fps_and_framecnt(self): + self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps + if self.input_fps == 0: # Case when FPS isn't available + self.input_fps = manual_fps_calculation(self.name, num_frames=10) + + self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps + self.frame_skip = int(self.input_fps / self.target_fps) + if self.frame_skip < 1: + self.frame_skip = 1 + self.skip_count = self.frame_skip - 1 + + self.MAX_FRAMES_PER_CLIP = int(self.target_fps * CLIP_DURATION) + self.target_interval = 1.0 / self.target_fps # 0.0666s + + if DEBUG == "1": + print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) + print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) + + # Frame count for videos + self.frame_count = None + if "://" not in str(self.source): + self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + # Gets frame W and H details + def get_frameWH(self): + input_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + input_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + if (input_height * input_width) < (MODEL_H * MODEL_W): + new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw + else: + new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw + + new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) + + self.width = new_sizeWH[0] + self.height = new_sizeWH[1] + + def update_frame(self): + self.stat_frame_count += 1 + elapsed = time.perf_counter() - self.stat_start_time + if elapsed > 1.0: + self.stat_fps = self.stat_frame_count / elapsed + + def run_model(self, frame, batch=1, device_input="cuda", stream=True): + results = self.model.predict( + frame, + imgsz=(self.resize_h, self.resize_w), + batch=batch, + device=device_input, + verbose=False, + stream=stream, + max_det=MAX_DETECTIONS, + ) + return results + # Main inference loop + def run_realtime_inference(self): + pass + + +class VideoStreamHandler(BaseHandler): def async_yolo_task(self, data): """Heavy lifting moved to ThreadPoolExecutor""" try: @@ -513,14 +736,22 @@ def async_yolo_task(self, data): e = traceback.format_exc() print(f"Async YOLO Error: {e}") - def process_frame_async(self, frame, frame_num): + def process_frame_async(self, frame, frame_num, repeat_count=1): """ Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) in the background without blocking the video reader. """ try: # Calls your existing Page 22 logic (run_pipeline) - inf_data = self.run_pipeline(frame, frame_num + 1) + # inf_data = self.run_pipeline(frame, frame_num + 1) + if self.device_input == "cpu": + inf_data = self.test_full_cpu_detection_gpu( + frame, frame_num + 1, repeat_count=repeat_count + ) + else: + inf_data = self.test_rbtdc_detection_gpu_optimized3( + frame, frame_num + 1, repeat_count=repeat_count + ) if inf_data: # Calls your Page 20 async_yolo_task to handle mask download/inference @@ -650,6 +881,55 @@ def test_rbtd_detection_gpu(self, frame, frameNum, repeat_count=1): "repeat_count": repeat_count, } + def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + # H, W = self.resize_h, self.resize_w + # self.cpu_resized_frame = cv2.resize(frame, (W, H)) + # self.video_writer.write(self.cpu_resized_frame) + self.gpu_fullres_frame.upload(frame, self.stream) + + cv2.cuda.resize( + self.gpu_fullres_frame, + (self.resize_w, self.resize_h), + stream=self.stream, + dst=self.resized_frame, + interpolation=cv2.INTER_NEAREST, + ) + if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): + self.pinned_downloaded_resizedframe_np = self.resized_frame.download( + self.stream + ) + # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) + for _ in range(repeat_count): + # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) + self.video_writer.write(self.pinned_downloaded_resizedframe_np) + + # Background Subtraction on GPU + self.apply_background_subtraction_gpu(include_history=True, method="and") + + # Thresholding + cv2.cuda.threshold( + self.fgMask, + MASK_THRESHOLD_VALUE, + MASK_MAX_VALUE, + cv2.THRESH_BINARY, + self.gpu_threshold_dst_frame, + self.stream, + ) + + # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + self.dilate_filter.apply( + self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream + ) + + return { + "frameNum": frameNum, + "mask": self.gpu_morphed_frame, + "full_frame": frame, # Original for cropping + "repeat_count": repeat_count, + } + def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): # Resize directly into the pre-allocated Pinned Memory # This avoids a temporary CPU allocation @@ -691,60 +971,986 @@ def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): "repeat_count": repeat_count, } - def update_frame(self): - self.stat_frame_count += 1 - elapsed = time.perf_counter() - self.stat_start_time - if elapsed > 1.0: - self.stat_fps = self.stat_frame_count / elapsed + def get_detections_for_contours_bbs( + self, frameNum, foi, contours, thickness=2, device_input="cuda" + ): + # global active_streams + # source = self.source + stream_name = self.name + num_objs = 0 + # predictions = [] + metadata = dict() + # frame_bytes = 'b' + cropped_imgs, cropped_coords = [], [] + H, W = foi.shape[:2] # Unpack once + bbs_full_res = [] - def check_disk_usage(self, path, min_gb=0.5): - """Returns True if there is at least min_gb available at path.""" - try: - total, used, free = shutil.disk_usage(path) - # Convert bytes to Gigabytes - free_gb = free / (2**30) - return free_gb > min_gb - except Exception as e: - print(f"Disk check error: {e}") - return False + # Filter and Sort in one go (Minimize Python-to-C++ crossings) + raw_bbs = [] + padding = 64 + for c in contours: + area = cv2.contourArea(c) + x1, y1, w, h = cv2.boundingRect(c) + if ( + area > self.min_contour_area + ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect + xx1 = max(0, int((x1 * self.scale_x)) - padding) + yy1 = max(0, int((y1 * self.scale_y)) - padding) + xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) + yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) + raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) + bbs_full_res = sorted( + [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], + key=lambda x: x[0], + reverse=True, + )[:MAX_DETECTIONS] - # Gets video fps and framecount - def get_fps_and_framecnt(self): - self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps - if self.input_fps == 0: # Case when FPS isn't available - self.input_fps = manual_fps_calculation(self.name, num_frames=10) + dist_thresh = min(0.05 * W, 0.05 * H) + merged = merge_boxes_limit( + bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + ) - self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps - self.frame_skip = int(self.input_fps / self.target_fps) - if self.frame_skip < 1: - self.frame_skip = 1 + merged = filter_contained_boxes(merged, containment_thresh=0.9) - self.MAX_FRAMES_PER_CLIP = int(self.target_fps * CLIP_DURATION) - self.target_interval = 1.0 / self.target_fps # 0.0666s + # for cnt, area in merged: + for x1, y1, x2, y2 in merged: + if ( + x2 > x1 + and y2 > y1 + and (x2 - x1) < self.frame_width + and (y2 - y1) < self.frame_height + ): + crop = foi[y1:y2, x1:x2] + if crop.size > 0: + cropped_imgs.append(crop) + cropped_coords.append((x1, y1)) - if DEBUG == "1": - print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) - print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) + if not cropped_imgs: + frame_bytes = get_display_frame_in_bytes( + foi, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + return metadata, frame_bytes # num_objs, predictions - # Frame count for videos - self.frame_count = None - if "://" not in str(self.source): - self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + # 2. Inference (Keep stream=False as it is stable) + results = self.model.predict( + cropped_imgs, + imgsz=MODEL_W, + batch=len(cropped_imgs), + device=device_input, + verbose=False, + stream=True, + max_det=MAX_DETECTIONS, + # classes=[0], # only "person", + # conf=0.45, + ) - # Gets frame W and H details - def get_frameWH(self): - input_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - input_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + label_source = ( + self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES + ) - if (input_height * input_width) < (MODEL_H * MODEL_W): - new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw - else: - new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw + for ridx, r in enumerate(results): + if r.boxes is None or len(r.boxes) == 0: + continue - new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) + # Move to CPU in one bulk operation per crop + boxes = r.boxes.xyxy.cpu().numpy().astype(int) + clss = r.boxes.cls.cpu().numpy().astype(int) + confs = r.boxes.conf.cpu().numpy() + off_x, off_y = cropped_coords[ridx] - self.width = new_sizeWH[0] - self.height = new_sizeWH[1] + for j in range(len(boxes)): + num_objs += 1 + bx1, by1, bx2, by2 = boxes[j] + abs_x1, abs_y1 = off_x + bx1, off_y + by1 + abs_x2, abs_y2 = off_x + bx2, off_y + by2 + class_id = clss[j] + class_name = label_source[class_id] + confidence = confs[j] + if confidence > DETECTION_THRESHOLD: + if not OMIT_DETECTIONS_FLAG: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + print( + # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", + f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", + flush=True, + ) + + bb_color = get_detection_color(class_id, is_bgr=True) + + foi = cv2.rectangle( + foi, + (abs_x1, abs_y1), + (abs_x2, abs_y2), + bb_color, + thickness, + ) + label = f"{class_name} {confidence:.2f}" + draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) + + height = min(abs_y2, H) - max(0, abs_y1) + width = min(abs_x2, W) - max(0, abs_x1) + # object_res = [ + # abs_x1, + # abs_y1, + # height, + # width, + # class_name, + # confidence, + # H, + # W, + # ] + + # Resized + scale_x = self.resize_w / W + scale_y = self.resize_h / H + object_res = [ + int(abs_x1 * scale_x), + int(abs_y1 * scale_y), + int(height * scale_y), + int(width * scale_x), + class_name, + confidence, + int(self.resize_h), + int(self.resize_w), + ] + + framenum_str = f"{frameNum:04d}_{j:04d}" + if DEBUG_FLAG: + meta_str = ",".join( + [str(o) for o in object_res + [framenum_str]] + ) + print(f"[{stream_name} METADATA],{meta_str}", flush=True) + + # Full Res + metadata[framenum_str] = { + "frameId": frameNum, + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(object_res[6]), + "frameW": int(object_res[7]), + }, + }, + } + + # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) + frame_bytes = get_display_frame_in_bytes( + foi, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + + return metadata, frame_bytes + + def contour2predictions( + self, frameNum, mask, frame, device_input="cpu", repeat_count=1 + ): + # source = self.source + # stream_name = self.name + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 3. Write frame + # self.video_writer.write(frame) + # if self.video_writer: + # for _ in range(repeat_count): + # self.video_writer.write(self.cpu_resized_frame) + + # num_objs = 0 + # predictions = [] + metadata = dict() + if contours: + metadata, frame_bytes = self.get_detections_for_contours_bbs( + frameNum, frame, contours, thickness=2, device_input=device_input + ) + + if metadata: + all_metadata.setdefault( + self.clip_key, + { + "object": {}, + "face": {}, + }, + ) + all_metadata[self.clip_key]["object"].update(metadata) + # all_metadata[clip_key]["face"].update(metadata_face) + return frame_bytes + + +class VideoStreamHandler2(BaseHandler): + def __init__(self, source, name, active_streams): + super().__init__(source, name, active_streams) + self.cap.release() + + def setup_threads(self): + self.torch_stream = torch.cuda.ExternalStream(self.stream.cudaPtr()) + self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) + self.reader = FastPinnedReader( + self.source, self.frame_height, self.frame_width, maxlen=2 + ) # .start() + self.process_thread = threading.Thread( + target=self.run_realtime_inference, daemon=True + ) + # self.process_thread.start() + + def start(self): + self.reader.start() + self.process_thread.start() + + def stop(self): + self.active = False # Signals the while loops to exit + self.reader.stop() + # self.process_thread.join() + + # Close the OpenCV capture + if self.cap: + self.cap.release() + + def run_realtime_inference(self): + print(f"Inference thread started for {self.name}...") + + # --- CRITICAL: Initialize model INSIDE the thread --- + # This binds the GPU context to this thread specifically. + # import torch + # self.model = YOLO(model_path, verbose=False, task="detect") + # self.model.to('cuda') # Explicitly move to GPU in this thread + + target_interval = 1.0 / self.target_fps + last_process_time = time.time() + + while self.active: # and (not self.reader.stopped or self.reader.frame_queue): + # 1. REAL-TIME SYNC: Clear stale frames from buffer + # while True: + # grabbed = self.cap.grab() + # if not grabbed: + # self.active = False + # break + + if self.device_input == "cuda": + now = time.time() + # if now - last_process_time < target_interval: + # continue + frame, f_idx = self.reader.read() + if frame is None: + # time.sleep(0.001) # Wait for reader thread + continue + last_process_time = now + else: + grabbed = self.cap.grab() + if not grabbed: + self.active = False + + now = time.time() + if now - last_process_time < target_interval: + continue + + success, frame = self.cap.retrieve() + if not success or frame is None: + continue + + last_process_time = now + + # 3. DECOUPLED AI: Only submit to AI if the worker queue is not backed up + # This prevents 'lag' if the AI is slower than the video feed + if self.get_executor_backlog() < MAX_WORKERS: + # Move the heavy 'run_pipeline' call into a background worker + self.executor.submit( + self.process_frame_async, frame.copy(), self.stat_frame_count + 1 + ) + else: + # If AI is busy, still update the display with the raw frame + # so the dashboard video stays smooth and fluid + _, buffer = cv2.imencode( + ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] + ) + self.latest_processed_frame = buffer.tobytes() + self.last_frame_id += 1 # Ensure the generator sees this 'clean' frame + + self.update_frame() + self.last_heartbeat = time.time() + + # 2. SKIP frames (Light Bitstream Parsing) + # This is the "Compute Saver" - it bypasses the decoder for N frames + for _ in range(self.skip_count): + if self.reader.read()[0] is None: + self.reader.stopped = True + break + + self.stop() + # Add this line to remove it from the dashboard immediately: + if self.name in self.active_streams: # noqa: F821 + del self.active_streams[self.name] # noqa: F821 + + def process_frame_async(self, frame, frame_num, repeat_count=1): + """ + Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) + in the background without blocking the video reader. + """ + try: + repeat_count = 1 + # Calls your existing Page 22 logic (run_pipeline) + # inf_data = self.run_pipeline(frame, frame_num + 1) + if self.device_input == "cpu": + raise ValueError("CPU pipeline not added") + # inf_data = self.test_full_cpu_detection_gpu( + # frame, frame_num + 1, repeat_count=repeat_count + # ) + else: + # inf_data = self.test_rbtdc_detection_gpu_optimized3(frame, frame_num + 1, repeat_count=repeat_count) + # print(f"inf_data: {inf_data}") + self.test_rbtdc_detection_gpu_optimized3( + frame, frame_num + 1, repeat_count=repeat_count + ) + # if inf_data: + # # Calls your Page 20 async_yolo_task to handle mask download/inference + # self.async_yolo_task(inf_data) + + except Exception: + e = traceback.format_exc() + print(f"ERROR: process_frame_async failed for {self.name}: {e}") + + def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + # H, W = self.resize_h, self.resize_w + # self.cpu_resized_frame = cv2.resize(frame, (W, H)) + # self.video_writer.write(self.cpu_resized_frame) + self.gpu_fullres_frame.upload(frame, self.stream) + + cv2.cuda.resize( + self.gpu_fullres_frame, + (self.resize_w, self.resize_h), + stream=self.stream, + dst=self.resized_frame, + interpolation=cv2.INTER_NEAREST, + ) + if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): + self.pinned_downloaded_resizedframe_np = self.resized_frame.download( + self.stream + ) + # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) + for _ in range(repeat_count): + # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) + self.video_writer.write(self.pinned_downloaded_resizedframe_np) + + # Background Subtraction on GPU + self.apply_background_subtraction_gpu(include_history=True, method="and") + + # Thresholding + cv2.cuda.threshold( + self.fgMask, + MASK_THRESHOLD_VALUE, + MASK_MAX_VALUE, + cv2.THRESH_BINARY, + self.gpu_threshold_dst_frame, + self.stream, + ) + + # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + self.dilate_filter.apply( + self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream + ) + + # return { + # "frameNum": frameNum, + # "mask": self.gpu_morphed_frame, + # "full_frame": frame, # Original for cropping + # "repeat_count": repeat_count, + # } + cropped_imgs, cropped_coords = [], [] + H, W = frame.shape[:2] # Unpack once + bbs_full_res = self.get_sorted_contours_gpu(self.gpu_morphed_frame, H, W) + # if not bbs_full_res: + # return num_objs + dist_thresh = min(0.05 * W, 0.05 * H) + merged = merge_boxes_limit( + bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + ) + + merged = filter_contained_boxes(merged, containment_thresh=0.9) + for x1, y1, x2, y2 in merged: + if ( + x2 > x1 + and y2 > y1 + and (x2 - x1) < self.frame_width + and (y2 - y1) < self.frame_height + ): + crop = frame[y1:y2, x1:x2] + if crop.size > 0: + cropped_imgs.append(crop) + cropped_coords.append((x1, y1)) + + if cropped_imgs: + with torch.cuda.stream(self.torch_stream): + results = self.run_model( + cropped_imgs, batch=len(cropped_imgs), stream=True + ) + if results: + # for r in results: # Consume generator [0.1.38, Line 2431] + # total_objs += len(r.boxes) + metadata, frame_bytes = self.extract_metadata_from_results( + results, + frame, + cropped_coords, + frameNum, + self.frame_height, + self.frame_width, + ) + else: + frame_bytes = get_display_frame_in_bytes( + frame, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + + self.latest_processed_frame = frame_bytes + self.last_heartbeat = time.time() + self.last_frame_id += 1 + + def async_yolo_task(self, data): + """Heavy lifting moved to ThreadPoolExecutor""" + try: + if self.device_input == "cuda": + frameNum = data["frameNum"] + gpu_morphed_frame = data["mask"] + frame = data["full_frame"] + # self.pinned_downloaded_frame_np = data["mask"].download(self.stream) + # frame_bytes = self.contour2predictions( + # data["frameNum"], + # self.pinned_downloaded_frame_np, + # data["full_frame"], + # device_input=self.device_input, + # repeat_count=data["repeat_count"], + # ) + cropped_imgs, cropped_coords = [], [] + H, W = frame.shape[:2] # Unpack once + bbs_full_res = self.get_sorted_contours_gpu(gpu_morphed_frame, H, W) + # if not bbs_full_res: + # return num_objs + dist_thresh = min(0.05 * W, 0.05 * H) + merged = merge_boxes_limit( + bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + ) + + merged = filter_contained_boxes(merged, containment_thresh=0.9) + for x1, y1, x2, y2 in merged: + if ( + x2 > x1 + and y2 > y1 + and (x2 - x1) < self.frame_width + and (y2 - y1) < self.frame_height + ): + crop = frame[y1:y2, x1:x2] + if crop.size > 0: + cropped_imgs.append(crop) + cropped_coords.append((x1, y1)) + + if cropped_imgs: + with torch.cuda.stream(self.torch_stream): + results = self.run_model( + cropped_imgs, batch=len(cropped_imgs), stream=True + ) + if results: + # for r in results: # Consume generator [0.1.38, Line 2431] + # total_objs += len(r.boxes) + metadata, frame_bytes = self.extract_metadata_from_results( + results, + frame, + cropped_coords, + frameNum, + self.frame_height, + self.frame_width, + ) + else: + frame_bytes = get_display_frame_in_bytes( + frame, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + else: + frame_bytes = self.contour2predictions( + data["frameNum"], + data["mask"], + data["full_frame"], + device_input=self.device_input, + repeat_count=data["repeat_count"], + ) + self.latest_processed_frame = frame_bytes + self.last_heartbeat = time.time() + self.last_frame_id += 1 + except Exception: + e = traceback.format_exc() + print(f"Async YOLO Error: {e}") + + def extract_metadata_from_results( + self, results, foi, cropped_coords, frameNum, H, W, thickness=2 + ): + num_objs = 0 + # predictions = [] + metadata = dict() + label_source = ( + self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES + ) + + for ridx, r in enumerate(results): + if r.boxes is None or len(r.boxes) == 0: + continue + + # Move to CPU in one bulk operation per crop + boxes = r.boxes.xyxy.cpu().numpy().astype(int) + clss = r.boxes.cls.cpu().numpy().astype(int) + confs = r.boxes.conf.cpu().numpy() + off_x, off_y = cropped_coords[ridx][:2] + + for j in range(len(boxes)): + num_objs += 1 + bx1, by1, bx2, by2 = boxes[j] + abs_x1, abs_y1 = off_x + bx1, off_y + by1 + abs_x2, abs_y2 = off_x + bx2, off_y + by2 + class_id = clss[j] + class_name = label_source[class_id] + confidence = confs[j] + if confidence > DETECTION_THRESHOLD: + bb_color = get_detection_color(class_id, is_bgr=True) + + foi = cv2.rectangle( + foi, + (abs_x1, abs_y1), + (abs_x2, abs_y2), + bb_color, + thickness, + ) + label = f"{class_name} {confidence:.2f}" + draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) + + height = min(abs_y2, H) - max(0, abs_y1) + width = min(abs_x2, W) - max(0, abs_x1) + # object_res = [ + # abs_x1, + # abs_y1, + # height, + # width, + # class_name, + # confidence, + # H, + # W, + # ] + + # Resized + scale_x = self.resize_w / W + scale_y = self.resize_h / H + object_res = [ + int(abs_x1 * scale_x), + int(abs_y1 * scale_y), + int(height * scale_y), + int(width * scale_x), + class_name, + confidence, + int(self.resize_h), + int(self.resize_w), + ] + + framenum_str = f"{frameNum:04d}_{j:04d}" + + # Full Res + metadata[framenum_str] = { + "frameId": frameNum, + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(object_res[6]), + "frameW": int(object_res[7]), + }, + }, + } + + # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) + frame_bytes = get_display_frame_in_bytes( + foi, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + + return metadata, frame_bytes + + def get_sorted_contours(self, morphed_frame, H, W): + contours, _ = cv2.findContours( + morphed_frame, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + raw_bbs = [] + padding = 64 + for c in contours: + area = cv2.contourArea(c) + x1, y1, w, h = cv2.boundingRect(c) + if ( + area > self.min_contour_area + ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect + xx1 = max(0, int((x1 * self.scale_x)) - padding) + yy1 = max(0, int((y1 * self.scale_y)) - padding) + xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) + yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) + raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) + bbs_full_res = sorted( + [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], + key=lambda x: x[0], + reverse=True, + )[:MAX_DETECTIONS] + return bbs_full_res + + def get_sorted_contours_gpu(self, morphed_frame, H, W): + # 1. Zero-Copy Bridge + gpu_frame_cp = gpumat2cupy(morphed_frame) + + # 2. Fast Labeling (Still required for connectivity) + label_image, num_labels = cucim_label(gpu_frame_cp, return_num=True) + if num_labels == 0: + return [] + + # cupy.bincount counts occurrences of each label (index 0 is background) + areas = cupy.bincount(label_image.ravel()) + mask = areas > self.min_contour_area + mask[0] = False + if not cupy.any(mask): + return [] + + # 3. Pre-allocate BBox buffer on GPU + # Format: [num_labels, 4] -> [min_y, min_x, max_y, max_x] + # Initialize with extreme values for min/max logic + bboxes_gpu = cupy.full((num_labels + 1, 4), -1, dtype=cupy.int32) + bboxes_gpu[:, :2] = 99999 # Initial min values + + # Pass the width (morphed_frame.size()[0]) to the kernel + mask_w = morphed_frame.size()[0] + + # 4. Run the Fast BBox Kernel + bbox_kernel(label_image, mask_w, bboxes_gpu) + + # 5. Filter by Area & Constraints on GPU + # (Optional: Use cupy.bincount to get areas if needed for area filtering) + # Move ONLY valid bboxes to CPU in one bulk operation + valid_bboxes = bboxes_gpu[mask].get() + + # 6. Final Coordinate Scaling + padding = 64 + bbs_full_res = [] + for y1, x1, y2, x2 in valid_bboxes[:MAX_DETECTIONS]: + xx1 = max(0, int(x1 * self.scale_x) - padding) + yy1 = max(0, int(y1 * self.scale_y) - padding) + xx2 = min(W, int(x2 * self.scale_x) + padding) + yy2 = min(H, int(y2 * self.scale_y) + padding) + bbs_full_res.append([xx1, yy1, xx2, yy2]) + + return bbs_full_res + + +class VideoStreamHandler3(BaseHandler): + def setup_threads(self): + self.torch_stream = torch.cuda.ExternalStream(self.stream.cudaPtr()) + self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) + self.process_thread = threading.Thread( + target=self.run_realtime_inference, daemon=True + ) + + def async_yolo_task(self, data): + """Heavy lifting moved to ThreadPoolExecutor""" + try: + if self.device_input == "cuda": + self.pinned_downloaded_frame_np = data["mask"].download(self.stream) + frame_bytes = self.contour2predictions( + data["frameNum"], + self.pinned_downloaded_frame_np, + data["full_frame"], + device_input=self.device_input, + repeat_count=data["repeat_count"], + ) + else: + frame_bytes = self.contour2predictions( + data["frameNum"], + data["mask"], + data["full_frame"], + device_input=self.device_input, + repeat_count=data["repeat_count"], + ) + self.latest_processed_frame = frame_bytes + self.last_heartbeat = time.time() + self.last_frame_id += 1 + except Exception: + e = traceback.format_exc() + print(f"Async YOLO Error: {e}") + + def process_frame_async(self, frame, frame_num, repeat_count=1): + """ + Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) + in the background without blocking the video reader. + """ + try: + # Calls your existing Page 22 logic (run_pipeline) + # inf_data = self.run_pipeline(frame, frame_num + 1) + if self.device_input == "cpu": + inf_data = self.test_full_cpu_detection_gpu( + frame, frame_num + 1, repeat_count=repeat_count + ) + else: + inf_data = self.test_rbtd_detection_gpu( + frame, frame_num + 1, repeat_count=repeat_count + ) + + # if inf_data: + # # Calls your Page 20 async_yolo_task to handle mask download/inference + # self.async_yolo_task(inf_data) + if inf_data and "mask" in inf_data: + # Calls your Page 20 async_yolo_task to handle mask download/inference + self.async_yolo_task(inf_data) + else: + frame_bytes = get_display_frame_in_bytes( + frame, + self.frame_width, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + ) + self.latest_processed_frame = frame_bytes + self.last_heartbeat = time.time() + self.last_frame_id += 1 + + except Exception: + e = traceback.format_exc() + print(f"ERROR: process_frame_async failed for {self.name}: {e}") + + def run_realtime_inference(self): + """ + Main loop: Initializes the model in this thread to fix CUDA context issues. + """ + print(f"Inference thread started for {self.name}...") + + # --- CRITICAL: Initialize model INSIDE the thread --- + # This binds the GPU context to this thread specifically. + # import torch + # self.model = YOLO(model_path, verbose=False, task="detect") + # self.model.to('cuda') # Explicitly move to GPU in this thread + + target_interval = 1.0 / self.target_fps + last_process_time = time.time() + + while self.active: + # 1. REAL-TIME SYNC: Clear stale frames from buffer + # while True: + grabbed = self.cap.grab() + if not grabbed: + self.active = False + break + + now = time.time() + if now - last_process_time < target_interval: + continue + + success, frame = self.cap.retrieve() + if not success or frame is None: + continue + + last_process_time = now + + # 3. DECOUPLED AI: Only submit to AI if the worker queue is not backed up + # This prevents 'lag' if the AI is slower than the video feed + if self.get_executor_backlog() < MAX_WORKERS: + # Move the heavy 'run_pipeline' call into a background worker + self.executor.submit( + self.process_frame_async, frame.copy(), self.stat_frame_count + ) + else: + # If AI is busy, still update the display with the raw frame + # so the dashboard video stays smooth and fluid + _, buffer = cv2.imencode( + ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] + ) + self.latest_processed_frame = buffer.tobytes() + self.last_frame_id += 1 # Ensure the generator sees this 'clean' frame + + self.update_frame() + self.last_heartbeat = time.time() + + self.stop() + # Add this line to remove it from the dashboard immediately: + if self.name in self.active_streams: # noqa: F821 + del self.active_streams[self.name] # noqa: F821 + + def test_rbtd_detection_gpu(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + H, W = self.resize_h, self.resize_w + # self.cpu_resized_frame = cv2.resize(frame, (W, H)) + # self.video_writer.write(self.cpu_resized_frame) + self.gpu_fullres_frame.upload(frame, self.stream) + cv2.cuda.resize( + self.gpu_fullres_frame, + (W, H), + stream=self.stream, + dst=self.resized_frame, + interpolation=cv2.INTER_NEAREST, + ) + if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): + self.pinned_downloaded_resizedframe_np = self.resized_frame.download( + self.stream + ) + # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) + for _ in range(repeat_count): + # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) + self.video_writer.write(self.pinned_downloaded_resizedframe_np) + + # Background Subtraction on GPU + self.fgMask = self.backSub.apply( + self.resized_frame, float(self.lr), stream=self.stream + ) + + for m in list(self.mask_history): + # Dilate the historical mask on GPU + dilated = self.dilate_filter_for_enhanced_mask.apply(m) + # Bitwise AND on GPU + cv2.cuda.bitwise_and(self.prev_bkgd, dilated, self.prev_bkgd) + # dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) + # cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) + self.mask_history.append(self.fgMask.clone()) + min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) + + if max_val != min_val: + self.fgMask = cv2.cuda.bitwise_or(self.fgMask, self.prev_bkgd) + + # Thresholding + cv2.cuda.threshold( + self.fgMask, + MASK_THRESHOLD_VALUE, + MASK_MAX_VALUE, + cv2.THRESH_BINARY, + self.gpu_threshold_dst_frame, + self.stream, + ) + + # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + self.dilate_filter.apply( + self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream + ) + + return { + "frameNum": frameNum, + "mask": self.gpu_morphed_frame, + "full_frame": frame, # Original for cropping + "repeat_count": repeat_count, + } + + def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + # H, W = self.resize_h, self.resize_w + # self.cpu_resized_frame = cv2.resize(frame, (W, H)) + # self.video_writer.write(self.cpu_resized_frame) + self.gpu_fullres_frame.upload(frame, self.stream) + + cv2.cuda.resize( + self.gpu_fullres_frame, + (self.resize_w, self.resize_h), + stream=self.stream, + dst=self.resized_frame, + interpolation=cv2.INTER_NEAREST, + ) + if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): + self.pinned_downloaded_resizedframe_np = self.resized_frame.download( + self.stream + ) + # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) + for _ in range(repeat_count): + # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) + self.video_writer.write(self.pinned_downloaded_resizedframe_np) + + # Background Subtraction on GPU + self.apply_background_subtraction_gpu(include_history=True, method="and") + + if frameNum - 1 % self.frame_skip: + return { + "frameNum": frameNum, + # "mask": self.gpu_morphed_frame, + "full_frame": frame, # Original for cropping + # "repeat_count": repeat_count, + } + + # Thresholding + cv2.cuda.threshold( + self.fgMask, + MASK_THRESHOLD_VALUE, + MASK_MAX_VALUE, + cv2.THRESH_BINARY, + self.gpu_threshold_dst_frame, + self.stream, + ) + + # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + self.dilate_filter.apply( + self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream + ) + + return { + "frameNum": frameNum, + "mask": self.gpu_morphed_frame, + "full_frame": frame, # Original for cropping + "repeat_count": repeat_count, + } + + def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): + # Resize directly into the pre-allocated Pinned Memory + # This avoids a temporary CPU allocation + H, W = self.resize_h, self.resize_w + self.cpu_resized_frame = cv2.resize( + frame, (W, H), interpolation=cv2.INTER_NEAREST + ) + if ENABLE_QUERYING: + for _ in range(repeat_count): + self.video_writer.write(self.cpu_resized_frame) + + # Background Subtraction on CPU + fgMask = self.backSub.apply(self.cpu_resized_frame, learningRate=self.lr) + + prev_bkgd = np.ones_like(fgMask) # AND + for m in self.mask_history: + # Dilate the historical mask + dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) + cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) + self.mask_history.append(fgMask) + + if prev_bkgd.max() != prev_bkgd.min(): + combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) + + # Convert the boolean array back to uint8 with 0 and 255 values + fgMask = combined_mask_bool.astype(np.uint8) * 255 + + # Thresholding + _, mask = cv2.threshold( + fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY + ) + + mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + + return { + "frameNum": frameNum, + "mask": mask, + "full_frame": frame, # Original for cropping + "repeat_count": repeat_count, + } def get_detections_for_contours_bbs( self, frameNum, foi, contours, thickness=2, device_input="cuda" diff --git a/fastapi/include/utils.py b/fastapi/include/utils.py index 1be9ca2..e424471 100644 --- a/fastapi/include/utils.py +++ b/fastapi/include/utils.py @@ -11,6 +11,7 @@ from pathlib import Path from random import randint +import cupy import cv2 import numpy as np from pydantic import BaseModel @@ -146,6 +147,75 @@ def return_connection(self, conn): THICKNESS_SCALE_FACTOR = 1e-3 FONT_SCALE_FACTOR = 1e-3 +bbox_kernel = cupy.ElementwiseKernel( + "S label_image, int32 width", + "raw T bboxes", + """ + if (label_image > 0) { + int label = (int)label_image; + + int y = i / width; + int x = i % width; + // Atomic operations to find min/max coordinates + atomicMin(&bboxes[label * 4 + 0], y); // min_y + atomicMin(&bboxes[label * 4 + 1], x); // min_x + atomicMax(&bboxes[label * 4 + 2], y); // max_y + atomicMax(&bboxes[label * 4 + 3], x); // max_x + } + """, + "bbox_kernel", +) + +bbox_area_kernel = cupy.ElementwiseKernel( + "S label_image, int32 width", + "raw T bboxes, raw T areas", + """ + if (label_image > 0) { + int label = (int)label_image; + int y = i / width; + int x = i % width; + + // 1. Update Bounding Box + atomicMin(&bboxes[label * 4 + 0], y); // min_y + atomicMin(&bboxes[label * 4 + 1], x); // min_x + atomicMax(&bboxes[label * 4 + 2], y); // max_y + atomicMax(&bboxes[label * 4 + 3], x); // max_x + + // 2. Increment Area (Count pixels) + atomicAdd(&areas[label], 1); + } + """, + "bbox_area_kernel", +) + +threshold_dilate_fused_kernel = cupy.ElementwiseKernel( + "T mask, int32 threshold, int32 width, int32 height", + "raw T morphed", + """ + // 1. Threshold + bool is_active = mask > threshold; + + // 2. Simple 3x3 Dilation (Fuse directly into output) + if (is_active) { + int y = i / width; + int x = i % width; + + // 2. 3x3 Dilation Expansion + // Writes 255 to the neighbors of any pixel above threshold + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int ny = y + dy; + int nx = x + dx; + if (ny >= 0 && ny < height && nx >= 0 && nx < width) { + morphed[ny * width + nx] = 255; + } + } + } + } + """, + "threshold_dilate_fused", +) + YOLO_CLASS_NAMES = [ "person", @@ -489,6 +559,45 @@ def get_display_frame_in_bytes(foi, frame_width, display_size=(1280, 720), quali return frame_bytes +def gpumat2cupy(gpu_mat): + """Bridge OpenCV GpuMat to CuPy without copying data.""" + # 1. Get properties from GpuMat + w, h = gpu_mat.size() + # Check if it's 3-channel (CV_8UC3) or 1-channel (CV_8UC1) + channels = 3 if gpu_mat.type() == cv2.CV_8UC3 else 1 + + if channels == 3: + shape = (h, w, 3) + # strides = (bytes_per_row, bytes_per_pixel, bytes_per_channel) + strides = (gpu_mat.step, 3, 1) + else: + shape = (h, w) + strides = (gpu_mat.step, 1) + + # 2. Map OpenCV types to CuPy typestrs + # CV_8UC1 is 'u1' (unsigned 1-byte), etc. + # type_map = {cv2.CV_8U: "|u1", cv2.CV_32F: "Live Detection Dashboard

Camera: {{ id }}

-
FPS: 0.0
+
DISPLAY FPS: 0.0
+ +
TARGET FPS: 0.0
@@ -205,7 +207,9 @@

Camera: {{ id }}

card.innerHTML = '

Camera: ' + id + '

' + '
' + - '
FPS: 0.0
' + + '
DISPLAY FPS: 0.0
' + + // '
INPUT FPS: 0.0
' + + '
TARGET FPS: 0.0
' + // '
Frames: 0
' + // '
Backlog: 0
' + // '
Status: Pending
' + @@ -227,11 +231,15 @@

Camera: {{ id }}

// console.log(`Updating stats for: ${id}`); const fpsEl = document.getElementById(`fps-${id}`); + const inputfpsEl = document.getElementById(`inputfps-${id}`); + const targetfpsEl = document.getElementById(`targetfps-${id}`); // const frameEl = document.getElementById(`frames-${id}`); // const backlogEl = document.getElementById(`backlog-${id}`); // const statusEl = document.getElementById(`status-${id}`); if (fpsEl) fpsEl.textContent = data.fps; + if (inputfpsEl) inputfpsEl.textContent = data.inputfps; + if (targetfpsEl) targetfpsEl.textContent = data.targetfps; // if (frameEl) frameEl.textContent = data.frames; // if (backlogEl) { // backlogEl.textContent = data.reencode_backlog; diff --git a/finetune/Dockerfile b/finetune/Dockerfile index 9862abf..d311ff8 100644 --- a/finetune/Dockerfile +++ b/finetune/Dockerfile @@ -36,8 +36,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN python3 -m venv ${VIRTUAL_ENV} ENV PATH="$VIRTUAL_ENV/bin:$PATH" +COPY requirements.* /home/ RUN pip3 install pip --no-cache-dir --upgrade && \ - pip3 install --no-cache-dir torch torchvision "numpy<2.0" + pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt # Define environment variables for OpenCV version and installation path ENV OPENCV_VERSION="4.10.0" @@ -89,14 +90,5 @@ RUN rm -rf ${DEPENDENCY_DIR} ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${VIRTUAL_ENV}/lib WORKDIR /home -# COPY .vscode /home/.vscode -# COPY configs /app/configs -# COPY include /app/include -# COPY inputs /app/inputs -# COPY models /app/models -# COPY *.py /app/ -COPY requirements.txt /home/ - -RUN pip3 install --no-cache-dir -r /home/requirements.txt ENTRYPOINT python3 /home/finetune_test.py 2>&1 | tee /home/finetune.log \ No newline at end of file diff --git a/finetune/app/requirements.txt b/finetune/app/requirements.txt deleted file mode 100644 index d1993c8..0000000 --- a/finetune/app/requirements.txt +++ /dev/null @@ -1,25 +0,0 @@ -# fastapi -# uvicorn -# opencv-python-headless==4.11.0.86 #==4.12.0.88 # Newer versions (4.13.x) have packaging dependency on XCB libraries -# openvino-dev==2024.6.0 -pip==25.3 -numpy<2.0 -ultralytics==8.4.7 -# vdms==0.0.22 -wheel==0.46.3 -# nncf==2.19.0 -torch -torchvision - -# cuda-python==12.2.0 -# onnx>=1.12.0,<=1.19.1 -# onnxruntime-gpu -# onnxslim>=0.1.71 -# tensorrt==10.9.0.34 #>=10.12.0.36 - -nvidia-cuda-runtime-cu12==12.8.* -onnx==1.20.1 -onnxruntime-gpu==1.23.2 -onnxslim==0.1.82 -tensorrt_cu12==10.14.1.48.post1 -nvidia-cudnn-cu12 \ No newline at end of file diff --git a/finetune/requirements.txt b/finetune/requirements.txt new file mode 100644 index 0000000..1808cfa --- /dev/null +++ b/finetune/requirements.txt @@ -0,0 +1,1213 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --generate-hashes --output-file=finetune/requirements.txt finetune/requirements.in +# +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 + # via requests +charset-normalizer==3.4.6 \ + --hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \ + --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ + --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ + --hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \ + --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ + --hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \ + --hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \ + --hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \ + --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ + --hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \ + --hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \ + --hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \ + --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ + --hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \ + --hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \ + --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ + --hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \ + --hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \ + --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ + --hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \ + --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ + --hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \ + --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ + --hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \ + --hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \ + --hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \ + --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ + --hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \ + --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ + --hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \ + --hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \ + --hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \ + --hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \ + --hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \ + --hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \ + --hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \ + --hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \ + --hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \ + --hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \ + --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ + --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ + --hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \ + --hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \ + --hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \ + --hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \ + --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ + --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ + --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ + --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ + --hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \ + --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ + --hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \ + --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ + --hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \ + --hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \ + --hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \ + --hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \ + --hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \ + --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ + --hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \ + --hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \ + --hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \ + --hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \ + --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ + --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ + --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ + --hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \ + --hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \ + --hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \ + --hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \ + --hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \ + --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ + --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ + --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ + --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ + --hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \ + --hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \ + --hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \ + --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ + --hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \ + --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ + --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ + --hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \ + --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ + --hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \ + --hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \ + --hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \ + --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ + --hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \ + --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ + --hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \ + --hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \ + --hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \ + --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ + --hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \ + --hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \ + --hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \ + --hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \ + --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ + --hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \ + --hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \ + --hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \ + --hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \ + --hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \ + --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ + --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ + --hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \ + --hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \ + --hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \ + --hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \ + --hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \ + --hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \ + --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ + --hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \ + --hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \ + --hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \ + --hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \ + --hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \ + --hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \ + --hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \ + --hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \ + --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ + --hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \ + --hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \ + --hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \ + --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ + --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \ + --hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \ + --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 + # via requests +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via onnxslim +coloredlogs==15.0.1 \ + --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ + --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 + # via onnxruntime-gpu +contourpy==1.3.2 \ + --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ + --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ + --hash=sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16 \ + --hash=sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f \ + --hash=sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f \ + --hash=sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7 \ + --hash=sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e \ + --hash=sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08 \ + --hash=sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841 \ + --hash=sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5 \ + --hash=sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2 \ + --hash=sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415 \ + --hash=sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878 \ + --hash=sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0 \ + --hash=sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab \ + --hash=sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445 \ + --hash=sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43 \ + --hash=sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c \ + --hash=sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823 \ + --hash=sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69 \ + --hash=sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15 \ + --hash=sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef \ + --hash=sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5 \ + --hash=sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73 \ + --hash=sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9 \ + --hash=sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912 \ + --hash=sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5 \ + --hash=sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85 \ + --hash=sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d \ + --hash=sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 \ + --hash=sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2 \ + --hash=sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54 \ + --hash=sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773 \ + --hash=sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934 \ + --hash=sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a \ + --hash=sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441 \ + --hash=sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422 \ + --hash=sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532 \ + --hash=sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739 \ + --hash=sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b \ + --hash=sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f \ + --hash=sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1 \ + --hash=sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87 \ + --hash=sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52 \ + --hash=sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1 \ + --hash=sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd \ + --hash=sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989 \ + --hash=sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb \ + --hash=sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f \ + --hash=sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad \ + --hash=sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9 \ + --hash=sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512 \ + --hash=sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd \ + --hash=sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83 \ + --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ + --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ + --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c + # via matplotlib +cuda-toolkit[cudart]==12.8.1 \ + --hash=sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba + # via tensorrt-cu12-libs +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c + # via matplotlib +filelock==3.25.2 \ + --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ + --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 + # via torch +flatbuffers==25.12.19 \ + --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 + # via onnxruntime-gpu +fonttools==4.62.1 \ + --hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \ + --hash=sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a \ + --hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \ + --hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \ + --hash=sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82 \ + --hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \ + --hash=sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b \ + --hash=sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e \ + --hash=sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416 \ + --hash=sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae \ + --hash=sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069 \ + --hash=sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9 \ + --hash=sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 \ + --hash=sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed \ + --hash=sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800 \ + --hash=sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e \ + --hash=sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9 \ + --hash=sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b \ + --hash=sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 \ + --hash=sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe \ + --hash=sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7 \ + --hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \ + --hash=sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 \ + --hash=sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23 \ + --hash=sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae \ + --hash=sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260 \ + --hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \ + --hash=sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87 \ + --hash=sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24 \ + --hash=sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53 \ + --hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \ + --hash=sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14 \ + --hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \ + --hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \ + --hash=sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1 \ + --hash=sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca \ + --hash=sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c \ + --hash=sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d \ + --hash=sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a \ + --hash=sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782 \ + --hash=sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c \ + --hash=sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a \ + --hash=sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 \ + --hash=sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3 \ + --hash=sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7 \ + --hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d \ + --hash=sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 \ + --hash=sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4 \ + --hash=sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68 \ + --hash=sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca + # via matplotlib +fsspec==2026.2.0 \ + --hash=sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff \ + --hash=sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 + # via torch +humanfriendly==10.0 \ + --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ + --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc + # via coloredlogs +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 + # via requests +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via torch +kiwisolver==1.5.0 \ + --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \ + --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \ + --hash=sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0 \ + --hash=sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8 \ + --hash=sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 \ + --hash=sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96 \ + --hash=sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e \ + --hash=sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac \ + --hash=sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f \ + --hash=sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a \ + --hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \ + --hash=sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7 \ + --hash=sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368 \ + --hash=sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02 \ + --hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \ + --hash=sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681 \ + --hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \ + --hash=sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 \ + --hash=sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4 \ + --hash=sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920 \ + --hash=sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374 \ + --hash=sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 \ + --hash=sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa \ + --hash=sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23 \ + --hash=sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859 \ + --hash=sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb \ + --hash=sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d \ + --hash=sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc \ + --hash=sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581 \ + --hash=sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c \ + --hash=sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099 \ + --hash=sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05 \ + --hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \ + --hash=sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd \ + --hash=sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc \ + --hash=sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796 \ + --hash=sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303 \ + --hash=sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca \ + --hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \ + --hash=sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489 \ + --hash=sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57 \ + --hash=sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1 \ + --hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \ + --hash=sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021 \ + --hash=sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db \ + --hash=sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22 \ + --hash=sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028 \ + --hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \ + --hash=sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65 \ + --hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \ + --hash=sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0 \ + --hash=sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a \ + --hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \ + --hash=sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c \ + --hash=sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac \ + --hash=sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476 \ + --hash=sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53 \ + --hash=sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3 \ + --hash=sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4 \ + --hash=sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615 \ + --hash=sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb \ + --hash=sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18 \ + --hash=sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b \ + --hash=sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1 \ + --hash=sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2 \ + --hash=sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c \ + --hash=sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac \ + --hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \ + --hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \ + --hash=sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2 \ + --hash=sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f \ + --hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \ + --hash=sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4 \ + --hash=sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9 \ + --hash=sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e \ + --hash=sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737 \ + --hash=sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b \ + --hash=sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed \ + --hash=sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 \ + --hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \ + --hash=sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08 \ + --hash=sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e \ + --hash=sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902 \ + --hash=sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd \ + --hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \ + --hash=sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310 \ + --hash=sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537 \ + --hash=sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554 \ + --hash=sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e \ + --hash=sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87 \ + --hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \ + --hash=sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c \ + --hash=sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79 \ + --hash=sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e \ + --hash=sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16 \ + --hash=sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1 \ + --hash=sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875 \ + --hash=sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd \ + --hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \ + --hash=sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9 \ + --hash=sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646 \ + --hash=sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657 \ + --hash=sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4 \ + --hash=sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232 \ + --hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \ + --hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \ + --hash=sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309 \ + --hash=sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede \ + --hash=sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2 \ + --hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \ + --hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7 \ + --hash=sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df \ + --hash=sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c \ + --hash=sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167 \ + --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \ + --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \ + --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398 + # via matplotlib +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +matplotlib==3.10.8 \ + --hash=sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7 \ + --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \ + --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \ + --hash=sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3 \ + --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \ + --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \ + --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \ + --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \ + --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \ + --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \ + --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \ + --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \ + --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \ + --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \ + --hash=sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656 \ + --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \ + --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \ + --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \ + --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \ + --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \ + --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \ + --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \ + --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \ + --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \ + --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \ + --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \ + --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \ + --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \ + --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \ + --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \ + --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \ + --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \ + --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \ + --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \ + --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \ + --hash=sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17 \ + --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \ + --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \ + --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \ + --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \ + --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \ + --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \ + --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \ + --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \ + --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \ + --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \ + --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \ + --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \ + --hash=sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df \ + --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \ + --hash=sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933 \ + --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 \ + --hash=sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8 \ + --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \ + --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7 + # via ultralytics +ml-dtypes==0.5.4 \ + --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ + --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ + --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ + --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ + --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ + --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ + --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ + --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ + --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ + --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ + --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ + --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ + --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ + --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ + --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ + --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ + --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ + --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ + --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ + --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ + --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ + --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ + --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ + --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ + --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ + --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ + --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ + --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ + --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ + --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ + --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ + --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ + --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ + --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ + --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ + --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ + --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ + --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ + --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 + # via + # onnx + # onnxslim +mpmath==1.3.0 \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + # via sympy +networkx==3.4.2 \ + --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ + --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f + # via torch +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f + # via + # -r finetune/requirements.in + # contourpy + # matplotlib + # ml-dtypes + # onnx + # onnxruntime-gpu + # opencv-python + # scipy + # torchvision + # ultralytics + # ultralytics-thop +nvidia-cublas-cu12==12.8.4.1 \ + --hash=sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af \ + --hash=sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142 \ + --hash=sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0 + # via + # nvidia-cudnn-cu12 + # nvidia-cusolver-cu12 + # torch +nvidia-cuda-cupti-cu12==12.8.90 \ + --hash=sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed \ + --hash=sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e \ + --hash=sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182 + # via torch +nvidia-cuda-nvrtc-cu12==12.8.93 \ + --hash=sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909 \ + --hash=sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994 \ + --hash=sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8 + # via torch +nvidia-cuda-runtime-cu12==12.8.90 \ + --hash=sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d \ + --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90 \ + --hash=sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8 + # via + # -r finetune/requirements.in + # cuda-toolkit + # torch +nvidia-cudnn-cu12==9.10.2.21 \ + --hash=sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8 \ + --hash=sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e \ + --hash=sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8 + # via + # -r finetune/requirements.in + # torch +nvidia-cufft-cu12==11.3.3.83 \ + --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74 \ + --hash=sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7 \ + --hash=sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a + # via torch +nvidia-cufile-cu12==1.13.1.3 \ + --hash=sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc \ + --hash=sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a + # via torch +nvidia-curand-cu12==10.3.9.90 \ + --hash=sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9 \ + --hash=sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd \ + --hash=sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec + # via torch +nvidia-cusolver-cu12==11.7.3.90 \ + --hash=sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450 \ + --hash=sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34 \ + --hash=sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0 + # via torch +nvidia-cusparse-cu12==12.5.8.93 \ + --hash=sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b \ + --hash=sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd \ + --hash=sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc + # via + # nvidia-cusolver-cu12 + # torch +nvidia-cusparselt-cu12==0.7.1 \ + --hash=sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5 \ + --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623 \ + --hash=sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075 + # via torch +nvidia-nccl-cu12==2.27.5 \ + --hash=sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a \ + --hash=sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457 + # via torch +nvidia-nvjitlink-cu12==12.8.93 \ + --hash=sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88 \ + --hash=sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7 \ + --hash=sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f + # via + # nvidia-cufft-cu12 + # nvidia-cusolver-cu12 + # nvidia-cusparse-cu12 + # torch +nvidia-nvshmem-cu12==3.3.20 \ + --hash=sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0 \ + --hash=sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5 + # via torch +nvidia-nvtx-cu12==12.8.90 \ + --hash=sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f \ + --hash=sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e \ + --hash=sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615 + # via torch +onnx==1.20.1 \ + --hash=sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2 \ + --hash=sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031 \ + --hash=sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a \ + --hash=sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826 \ + --hash=sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2 \ + --hash=sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c \ + --hash=sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00 \ + --hash=sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0 \ + --hash=sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3 \ + --hash=sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be \ + --hash=sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095 \ + --hash=sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431 \ + --hash=sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e \ + --hash=sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080 \ + --hash=sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5 \ + --hash=sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489 \ + --hash=sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945 \ + --hash=sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf \ + --hash=sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281 \ + --hash=sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67 \ + --hash=sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38 \ + --hash=sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a \ + --hash=sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def + # via + # -r finetune/requirements.in + # onnxslim +onnxruntime-gpu==1.23.2 \ + --hash=sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4 \ + --hash=sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d \ + --hash=sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26 \ + --hash=sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f \ + --hash=sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2 \ + --hash=sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767 \ + --hash=sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59 \ + --hash=sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321 \ + --hash=sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95 + # via -r finetune/requirements.in +onnxslim==0.1.82 \ + --hash=sha256:3190340f53c93620779f2159b41d114e571b7c1a0cfa8630cba3f7be92d3399e \ + --hash=sha256:4f48decf32863e583976fff6e9cfd9d6fe6a4a9814e7577c2cf8ce082973c6eb + # via -r finetune/requirements.in +opencv-python==4.11.0.86 \ + --hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \ + --hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \ + --hash=sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202 \ + --hash=sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a \ + --hash=sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d \ + --hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \ + --hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66 + # via ultralytics +packaging==26.0 \ + --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ + --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 + # via + # matplotlib + # onnxruntime-gpu + # onnxslim +pillow==12.1.1 \ + --hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \ + --hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \ + --hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \ + --hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \ + --hash=sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713 \ + --hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \ + --hash=sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9 \ + --hash=sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0 \ + --hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \ + --hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \ + --hash=sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 \ + --hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \ + --hash=sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5 \ + --hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \ + --hash=sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35 \ + --hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \ + --hash=sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff \ + --hash=sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 \ + --hash=sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4 \ + --hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \ + --hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \ + --hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \ + --hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \ + --hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \ + --hash=sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e \ + --hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \ + --hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \ + --hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \ + --hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \ + --hash=sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d \ + --hash=sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b \ + --hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \ + --hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \ + --hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \ + --hash=sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a \ + --hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \ + --hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \ + --hash=sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f \ + --hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \ + --hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \ + --hash=sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9 \ + --hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \ + --hash=sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40 \ + --hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \ + --hash=sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c \ + --hash=sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 \ + --hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \ + --hash=sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af \ + --hash=sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735 \ + --hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \ + --hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \ + --hash=sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b \ + --hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \ + --hash=sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9 \ + --hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \ + --hash=sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e \ + --hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \ + --hash=sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4 \ + --hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \ + --hash=sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 \ + --hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \ + --hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \ + --hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \ + --hash=sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3 \ + --hash=sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052 \ + --hash=sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 \ + --hash=sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293 \ + --hash=sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523 \ + --hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \ + --hash=sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b \ + --hash=sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80 \ + --hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \ + --hash=sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79 \ + --hash=sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23 \ + --hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \ + --hash=sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e \ + --hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \ + --hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \ + --hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \ + --hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \ + --hash=sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5 \ + --hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \ + --hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \ + --hash=sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 \ + --hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \ + --hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \ + --hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \ + --hash=sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3 \ + --hash=sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 \ + --hash=sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090 \ + --hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289 + # via + # matplotlib + # torchvision + # ultralytics +polars==1.39.3 \ + --hash=sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c \ + --hash=sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56 + # via ultralytics +polars-runtime-32==1.39.3 \ + --hash=sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14 \ + --hash=sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2 \ + --hash=sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9 \ + --hash=sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4 \ + --hash=sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed \ + --hash=sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d \ + --hash=sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339 \ + --hash=sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562 \ + --hash=sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860 + # via polars +protobuf==7.34.1 \ + --hash=sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a \ + --hash=sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a \ + --hash=sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b \ + --hash=sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4 \ + --hash=sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280 \ + --hash=sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11 \ + --hash=sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7 \ + --hash=sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c + # via + # onnx + # onnxruntime-gpu +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via ultralytics +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via matplotlib +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via matplotlib +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via ultralytics +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via ultralytics +scipy==1.15.3 \ + --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ + --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ + --hash=sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723 \ + --hash=sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730 \ + --hash=sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539 \ + --hash=sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb \ + --hash=sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6 \ + --hash=sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594 \ + --hash=sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92 \ + --hash=sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82 \ + --hash=sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49 \ + --hash=sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759 \ + --hash=sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba \ + --hash=sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982 \ + --hash=sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8 \ + --hash=sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65 \ + --hash=sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4 \ + --hash=sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e \ + --hash=sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed \ + --hash=sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c \ + --hash=sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5 \ + --hash=sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5 \ + --hash=sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019 \ + --hash=sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e \ + --hash=sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1 \ + --hash=sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889 \ + --hash=sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca \ + --hash=sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825 \ + --hash=sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9 \ + --hash=sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62 \ + --hash=sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb \ + --hash=sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b \ + --hash=sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13 \ + --hash=sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb \ + --hash=sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40 \ + --hash=sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c \ + --hash=sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253 \ + --hash=sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb \ + --hash=sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f \ + --hash=sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163 \ + --hash=sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45 \ + --hash=sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7 \ + --hash=sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11 \ + --hash=sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf \ + --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ + --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 + # via ultralytics +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil +sympy==1.14.0 \ + --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ + --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + # via + # onnxruntime-gpu + # onnxslim + # torch +tensorrt-cu12==10.14.1.48.post1 \ + --hash=sha256:5a6d4d78560be7c8fff877711fa8334e8e2b441b702f047ea3107311b9897341 + # via -r finetune/requirements.in +tensorrt-cu12-bindings==10.14.1.48.post1 \ + --hash=sha256:03bd44f645a30e04f38d4a866bdfb0e9e16a34601384ce29ef8d008950175828 \ + --hash=sha256:1cc29c5bb32a0719d5fadbbd2e69971837b2b0d2f1575d9a2ddc1cf3f6f5d8f0 \ + --hash=sha256:2a0b5a301c84d1c95e67cede52bb49f44fda02a45c6a4b409fa16f0632394046 \ + --hash=sha256:2b83b1608e21c25da72776501533b17fe1d000595b9a191613665f67e0598868 \ + --hash=sha256:40100265c49dc91e0a3f0a030e7de0077c034e6e30c11828001b036052de1d77 \ + --hash=sha256:68ae05d23f4918fdd36a505daf8b93b64444ef9328516284363480ab776a2595 \ + --hash=sha256:78da2abb803370147e75045eaeaa2a3f134f5aa7537405f86b22eaa36c0a11ed \ + --hash=sha256:9b2c8f41d847b202e35054fbaccd55f4efbb673da11f562befc273c0b2d65f48 \ + --hash=sha256:aad15bf393acc85b2e5015f0ca2082b0d162d5966ac1f58de48d1205446e237f \ + --hash=sha256:b2bf5597c7790c36fa858b8dfd6a867a482ee41d7c35e8732b3fd671b013869c \ + --hash=sha256:b90ce26abe1d49da527211411d023f95a235806fab2d00277585558e265f9b93 \ + --hash=sha256:d9cb40e646e11225b295eaeaf74aeb7e422c425271d51ca8c416776449fec617 + # via tensorrt-cu12 +tensorrt-cu12-libs==10.14.1.48.post1 \ + --hash=sha256:46e9e84e16ca7d89ca572e0900d9480945bb6faaa0c385e6f63e1ae46a834b25 + # via tensorrt-cu12 +torch==2.9.1 \ + --hash=sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73 \ + --hash=sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7 \ + --hash=sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb \ + --hash=sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6 \ + --hash=sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e \ + --hash=sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a \ + --hash=sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4 \ + --hash=sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65 \ + --hash=sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587 \ + --hash=sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db \ + --hash=sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a \ + --hash=sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475 \ + --hash=sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9 \ + --hash=sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2 \ + --hash=sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e \ + --hash=sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d \ + --hash=sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083 \ + --hash=sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2 \ + --hash=sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c \ + --hash=sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951 \ + --hash=sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e \ + --hash=sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb \ + --hash=sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9 \ + --hash=sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e \ + --hash=sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c \ + --hash=sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b \ + --hash=sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d \ + --hash=sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6 + # via + # -r finetune/requirements.in + # torchvision + # ultralytics + # ultralytics-thop +torchvision==0.24.1 \ + --hash=sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600 \ + --hash=sha256:1540a9e7f8cf55fe17554482f5a125a7e426347b71de07327d5de6bfd8d17caa \ + --hash=sha256:16274823b93048e0a29d83415166a2e9e0bf4e1b432668357b657612a4802864 \ + --hash=sha256:18f9cb60e64b37b551cd605a3d62c15730c086362b40682d23e24b616a697d41 \ + --hash=sha256:1b495edd3a8f9911292424117544f0b4ab780452e998649425d1f4b2bed6695f \ + --hash=sha256:1e39619de698e2821d71976c92c8a9e50cdfd1e993507dfb340f2688bfdd8283 \ + --hash=sha256:480b271d6edff83ac2e8d69bbb4cf2073f93366516a50d48f140ccfceedb002e \ + --hash=sha256:4aa6cb806eb8541e92c9b313e96192c6b826e9eb0042720e2fa250d021079952 \ + --hash=sha256:54ed17c3d30e718e08d8da3fd5b30ea44b0311317e55647cb97077a29ecbc25b \ + --hash=sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7 \ + --hash=sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050 \ + --hash=sha256:8a6696db7fb71eadb2c6a48602106e136c785642e598eb1533e0b27744f2cce6 \ + --hash=sha256:9ef95d819fd6df81bc7cc97b8f21a15d2c0d3ac5dbfaab5cbc2d2ce57114b19e \ + --hash=sha256:a0f106663e60332aa4fcb1ca2159ef8c3f2ed266b0e6df88de261048a840e0df \ + --hash=sha256:a9308cdd37d8a42e14a3e7fd9d271830c7fecb150dd929b642f3c1460514599a \ + --hash=sha256:ab211e1807dc3e53acf8f6638df9a7444c80c0ad050466e8d652b3e83776987b \ + --hash=sha256:af9201184c2712d808bd4eb656899011afdfce1e83721c7cb08000034df353fe \ + --hash=sha256:cccf4b4fec7fdfcd3431b9ea75d1588c0a8596d0333245dafebee0462abe3388 \ + --hash=sha256:d83e16d70ea85d2f196d678bfb702c36be7a655b003abed84e465988b6128938 \ + --hash=sha256:db2125c46f9cb25dc740be831ce3ce99303cfe60439249a41b04fd9f373be671 \ + --hash=sha256:ded5e625788572e4e1c4d155d1bbc48805c113794100d70e19c76e39e4d53465 \ + --hash=sha256:e3f96208b4bef54cd60e415545f5200346a65024e04f29a26cd0006dbf9e8e66 \ + --hash=sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193 \ + --hash=sha256:ec9d7379c519428395e4ffda4dbb99ec56be64b0a75b95989e00f9ec7ae0b2d7 \ + --hash=sha256:f035f0cacd1f44a8ff6cb7ca3627d84c54d685055961d73a1a9fb9827a5414c8 \ + --hash=sha256:f231f6a4f2aa6522713326d0d2563538fa72d613741ae364f9913027fa52ea35 \ + --hash=sha256:f476da4e085b7307aaab6f540219617d46d5926aeda24be33e1359771c83778f \ + --hash=sha256:fbdbdae5e540b868a681240b7dbd6473986c862445ee8a138680a6a97d6c34ff + # via + # -r finetune/requirements.in + # ultralytics +triton==3.5.1 \ + --hash=sha256:02c770856f5e407d24d28ddc66e33cf026e6f4d360dcb8b2fabe6ea1fc758621 \ + --hash=sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8 \ + --hash=sha256:275a045b6ed670dd1bd005c3e6c2d61846c74c66f4512d6f33cc027b11de8fd4 \ + --hash=sha256:56765ffe12c554cd560698398b8a268db1f616c120007bfd8829d27139abd24a \ + --hash=sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94 \ + --hash=sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579 \ + --hash=sha256:8932391d7f93698dfe5bc9bead77c47a24f97329e9f20c10786bb230a9083f56 \ + --hash=sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478 \ + --hash=sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60 \ + --hash=sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232 \ + --hash=sha256:da47169e30a779bade679ce78df4810fca6d78a955843d2ddb11f226adc517dc \ + --hash=sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba \ + --hash=sha256:f617aa7925f9ea9968ec2e1adaf93e87864ff51549c8f04ce658f29bbdb71e2d \ + --hash=sha256:f63e34dcb32d7bd3a1d0195f60f30d2aee8b08a69a0424189b71017e23dfc3d2 + # via torch +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # onnx + # torch +ultralytics==8.4.7 \ + --hash=sha256:37464fd86080a4cac278575a3e6a1a52ad311d45c8d317b863a437c82315cbb7 \ + --hash=sha256:7438696c981cf58d17250b0e4c7e29886bda9cc3e91ffbbf9544c622f65da91c + # via -r finetune/requirements.in +ultralytics-thop==2.0.18 \ + --hash=sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf \ + --hash=sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec + # via ultralytics +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via requests diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 27b11ec..9c3cffc 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -15,10 +15,9 @@ RUN apt-get install --only-upgrade libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-pip && \ rm -rf /var/lib/apt/lists/* -# COPY requirements.txt /home/requirements.txt -# RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt -COPY requirements.in /home/requirements.in -RUN pip3 install --no-cache-dir -r /home/requirements.in +COPY requirements.* /home/ +RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt +# RUN pip3 install --no-cache-dir -r /home/requirements.in COPY *.py /home/ COPY *.conf /etc/nginx/ diff --git a/frontend/requirements.txt b/frontend/requirements.txt index c3cbdb3..56cd654 100644 --- a/frontend/requirements.txt +++ b/frontend/requirements.txt @@ -143,7 +143,9 @@ protobuf==5.29.5 \ --hash=sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671 \ --hash=sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238 \ --hash=sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015 - # via vdms + # via + # -r frontend/requirements.in + # vdms psutil==5.9.0 \ --hash=sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5 \ --hash=sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a \ @@ -178,10 +180,14 @@ psutil==5.9.0 \ --hash=sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c \ --hash=sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3 # via -r frontend/requirements.in +# requests==2.32.5 \ +# --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ +# --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e +# # via -r frontend/requirements.in requests==2.32.5 \ - --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ - --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e - # via -r frontend/requirements.in + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via -r fastapi/requirements.txt tornado==6.5 \ --hash=sha256:007f036f7b661e899bd9ef3fa5f87eb2cb4d1b2e7d67368e778e140a2f101a7a \ --hash=sha256:03576ab51e9b1677e4cdaae620d6700d9823568b7939277e4690fe4085886c55 \ @@ -204,8 +210,3 @@ vdms==0.0.22 \ --hash=sha256:4d59fedd914a645fb8a42c504c9535131f9de9a435b5add00900a2abade50036 \ --hash=sha256:a1ca7fb79f81526ccf5cc9b5066bbbaa513a9b258002e40b4b93765b4739818a # via -r frontend/requirements.in - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. -# pip diff --git a/udf/requirements.txt b/udf/requirements.txt index 0926f8a..25dab0c 100644 --- a/udf/requirements.txt +++ b/udf/requirements.txt @@ -234,8 +234,3 @@ wheel==0.46.3 \ --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ --hash=sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803 # via -r udf/requirements.in - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. -# pip diff --git a/video/Dockerfile b/video/Dockerfile index bfc4310..ef81446 100644 --- a/video/Dockerfile +++ b/video/Dockerfile @@ -33,16 +33,8 @@ COPY requirements.* /home/ ARG DEVICE="CPU" ENV DEVICE="${DEVICE}" -# RUN if [ "${DEVICE}" = "CPU" ]; then \ -# pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ -# else \ -# pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ -# fi; -# RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ -# pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt -RUN pip3 install --no-cache-dir -r /home/requirements.in -# && \ -# pip3 install --no-cache-dir -r /home/requirements.${DEVICE}.txt +# RUN pip3 install --no-cache-dir -r /home/requirements.in +RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt COPY *.py /home/ COPY *.yaml /home/ diff --git a/video/Dockerfile.base b/video/Dockerfile.base deleted file mode 100644 index 1536390..0000000 --- a/video/Dockerfile.base +++ /dev/null @@ -1,34 +0,0 @@ - -FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a as build - -ENV VIRTUAL_ENV=/opt/venv -ARG DEBIAN_FRONTEND=noninteractive - -# fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 -RUN apt-get update -RUN apt-get install --only-upgrade libc-bin libc6 && \ - apt-get install -y -q --no-install-recommends python3-setuptools \ - python3-tornado python3-ply python3-pip python3-venv \ - && rm -rf /var/lib/apt/lists/* - -RUN python3 -m venv ${VIRTUAL_ENV} -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -COPY resources /home/resources -COPY requirements.* /home/ - -ARG DEVICE="CPU" -ENV DEVICE="${DEVICE}" - -RUN if [ "${DEVICE}" = "CPU" ]; then \ - pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" --index-url https://download.pytorch.org/whl/cpu; \ - else \ - pip3 install --no-cache-dir "torch==2.9.1" "torchvision==0.24.1" ; \ - fi; -# RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ -# pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt -RUN pip3 install --no-cache-dir -r /home/requirements.in && \ - pip3 install --no-cache-dir -r /home/requirements.${DEVICE}.txt - -# RUN pip3 install --no-cache-dir openvino-dev>=2024.6.0 -RUN omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 diff --git a/video/requirements.GPU.txt b/video/requirements.GPU.txt deleted file mode 100644 index c5dd166..0000000 --- a/video/requirements.GPU.txt +++ /dev/null @@ -1,228 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --generate-hashes --output-file=video/requirements.GPU.txt video/requirements.GPU.in -# -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via onnxslim -coloredlogs==15.0.1 \ - --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ - --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 - # via onnxruntime-gpu -cuda-toolkit[cudart]==12.9.1 \ - --hash=sha256:0c8636dfacbecfe9867a949a211864f080a805bc54023ce4a361aa4e1fd8738b - # via tensorrt-cu12-libs -flatbuffers==25.12.19 \ - --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 - # via onnxruntime-gpu -humanfriendly==10.0 \ - --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ - --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc - # via coloredlogs -ml-dtypes==0.5.4 \ - --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ - --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ - --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ - --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ - --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ - --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ - --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ - --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ - --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ - --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ - --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ - --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ - --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ - --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ - --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ - --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ - --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ - --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ - --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ - --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ - --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ - --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ - --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ - --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ - --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ - --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ - --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ - --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ - --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ - --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ - --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ - --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ - --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ - --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ - --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ - --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ - --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ - --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ - --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 - # via - # onnx - # onnxslim -mpmath==1.3.0 \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - # via sympy -numpy==2.2.6 \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 - # via - # ml-dtypes - # onnx - # onnxruntime-gpu -nvidia-cuda-runtime-cu12==12.9.79 \ - --hash=sha256:25bba2dfb01d48a9b59ca474a1ac43c6ebf7011f1b0b8cc44f54eb6ac48a96c3 \ - --hash=sha256:83469a846206f2a733db0c42e223589ab62fd2fabac4432d2f8802de4bded0a4 \ - --hash=sha256:8e018af8fa02363876860388bd10ccb89eb9ab8fb0aa749aaf58430a9f7c4891 - # via cuda-toolkit -onnx==1.20.1 \ - --hash=sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2 \ - --hash=sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031 \ - --hash=sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a \ - --hash=sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826 \ - --hash=sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2 \ - --hash=sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c \ - --hash=sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00 \ - --hash=sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0 \ - --hash=sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3 \ - --hash=sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be \ - --hash=sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095 \ - --hash=sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431 \ - --hash=sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e \ - --hash=sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080 \ - --hash=sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5 \ - --hash=sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489 \ - --hash=sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945 \ - --hash=sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf \ - --hash=sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281 \ - --hash=sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67 \ - --hash=sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38 \ - --hash=sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a \ - --hash=sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def - # via - # -r video/requirements.GPU.in - # onnxslim -onnxruntime-gpu==1.23.2 \ - --hash=sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4 \ - --hash=sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d \ - --hash=sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26 \ - --hash=sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f \ - --hash=sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2 \ - --hash=sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767 \ - --hash=sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59 \ - --hash=sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321 \ - --hash=sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95 - # via -r video/requirements.GPU.in -onnxslim==0.1.82 \ - --hash=sha256:3190340f53c93620779f2159b41d114e571b7c1a0cfa8630cba3f7be92d3399e \ - --hash=sha256:4f48decf32863e583976fff6e9cfd9d6fe6a4a9814e7577c2cf8ce082973c6eb - # via -r video/requirements.GPU.in -packaging==26.0 \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 - # via - # onnxruntime-gpu - # onnxslim -protobuf==6.33.4 \ - --hash=sha256:0f12ddbf96912690c3582f9dffb55530ef32015ad8e678cd494312bd78314c4f \ - --hash=sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc \ - --hash=sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0 \ - --hash=sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9 \ - --hash=sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e \ - --hash=sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc \ - --hash=sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d \ - --hash=sha256:955478a89559fa4568f5a81dce77260eabc5c686f9e8366219ebd30debf06aa6 \ - --hash=sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6 \ - --hash=sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91 - # via - # onnx - # onnxruntime-gpu -sympy==1.14.0 \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 - # via - # onnxruntime-gpu - # onnxslim -tensorrt-cu12==10.14.1.48.post1 \ - --hash=sha256:5a6d4d78560be7c8fff877711fa8334e8e2b441b702f047ea3107311b9897341 - # via -r video/requirements.GPU.in -tensorrt-cu12-bindings==10.14.1.48.post1 \ - --hash=sha256:03bd44f645a30e04f38d4a866bdfb0e9e16a34601384ce29ef8d008950175828 \ - --hash=sha256:1cc29c5bb32a0719d5fadbbd2e69971837b2b0d2f1575d9a2ddc1cf3f6f5d8f0 \ - --hash=sha256:2a0b5a301c84d1c95e67cede52bb49f44fda02a45c6a4b409fa16f0632394046 \ - --hash=sha256:2b83b1608e21c25da72776501533b17fe1d000595b9a191613665f67e0598868 \ - --hash=sha256:40100265c49dc91e0a3f0a030e7de0077c034e6e30c11828001b036052de1d77 \ - --hash=sha256:68ae05d23f4918fdd36a505daf8b93b64444ef9328516284363480ab776a2595 \ - --hash=sha256:78da2abb803370147e75045eaeaa2a3f134f5aa7537405f86b22eaa36c0a11ed \ - --hash=sha256:9b2c8f41d847b202e35054fbaccd55f4efbb673da11f562befc273c0b2d65f48 \ - --hash=sha256:aad15bf393acc85b2e5015f0ca2082b0d162d5966ac1f58de48d1205446e237f \ - --hash=sha256:b2bf5597c7790c36fa858b8dfd6a867a482ee41d7c35e8732b3fd671b013869c \ - --hash=sha256:b90ce26abe1d49da527211411d023f95a235806fab2d00277585558e265f9b93 \ - --hash=sha256:d9cb40e646e11225b295eaeaf74aeb7e422c425271d51ca8c416776449fec617 - # via tensorrt-cu12 -tensorrt-cu12-libs==10.14.1.48.post1 \ - --hash=sha256:46e9e84e16ca7d89ca572e0900d9480945bb6faaa0c385e6f63e1ae46a834b25 - # via tensorrt-cu12 -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - # via onnx diff --git a/video/requirements.txt b/video/requirements.txt index a5a2fdd..9ceaec4 100644 --- a/video/requirements.txt +++ b/video/requirements.txt @@ -8,873 +8,157 @@ build==1.4.0 \ --hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \ --hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936 # via inotify -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.6 \ + --hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \ + --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ + --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ + --hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \ + --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ + --hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \ + --hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \ + --hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \ + --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ + --hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \ + --hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \ + --hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \ + --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ + --hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \ + --hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \ + --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ + --hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \ + --hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \ + --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ + --hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \ + --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ + --hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \ + --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ + --hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \ + --hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \ + --hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \ + --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ + --hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \ + --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ + --hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \ + --hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \ + --hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \ + --hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \ + --hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \ + --hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \ + --hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \ + --hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \ + --hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \ + --hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \ + --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ + --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ + --hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \ + --hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \ + --hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \ + --hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \ + --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ + --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ + --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ + --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ + --hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \ + --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ + --hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \ + --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ + --hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \ + --hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \ + --hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \ + --hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \ + --hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \ + --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ + --hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \ + --hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \ + --hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \ + --hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \ + --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ + --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ + --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ + --hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \ + --hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \ + --hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \ + --hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \ + --hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \ + --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ + --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ + --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ + --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ + --hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \ + --hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \ + --hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \ + --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ + --hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \ + --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ + --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ + --hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \ + --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ + --hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \ + --hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \ + --hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \ + --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ + --hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \ + --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ + --hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \ + --hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \ + --hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \ + --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ + --hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \ + --hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \ + --hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \ + --hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \ + --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ + --hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \ + --hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \ + --hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \ + --hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \ + --hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \ + --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ + --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ + --hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \ + --hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \ + --hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \ + --hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \ + --hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \ + --hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \ + --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ + --hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \ + --hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \ + --hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \ + --hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \ + --hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \ + --hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \ + --hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \ + --hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \ + --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ + --hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \ + --hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \ + --hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \ + --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ + --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \ + --hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \ + --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 # via requests -contourpy==1.3.2 \ - --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ - --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ - --hash=sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16 \ - --hash=sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f \ - --hash=sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f \ - --hash=sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7 \ - --hash=sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e \ - --hash=sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08 \ - --hash=sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841 \ - --hash=sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5 \ - --hash=sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2 \ - --hash=sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415 \ - --hash=sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878 \ - --hash=sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0 \ - --hash=sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab \ - --hash=sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445 \ - --hash=sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43 \ - --hash=sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c \ - --hash=sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823 \ - --hash=sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69 \ - --hash=sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15 \ - --hash=sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef \ - --hash=sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5 \ - --hash=sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73 \ - --hash=sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9 \ - --hash=sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912 \ - --hash=sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5 \ - --hash=sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85 \ - --hash=sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d \ - --hash=sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 \ - --hash=sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2 \ - --hash=sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54 \ - --hash=sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773 \ - --hash=sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934 \ - --hash=sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a \ - --hash=sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441 \ - --hash=sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422 \ - --hash=sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532 \ - --hash=sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739 \ - --hash=sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b \ - --hash=sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f \ - --hash=sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1 \ - --hash=sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87 \ - --hash=sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52 \ - --hash=sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1 \ - --hash=sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd \ - --hash=sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989 \ - --hash=sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb \ - --hash=sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f \ - --hash=sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad \ - --hash=sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9 \ - --hash=sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512 \ - --hash=sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd \ - --hash=sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83 \ - --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ - --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ - --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c - # via matplotlib -cycler==0.12.1 \ - --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ - --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c - # via matplotlib -defusedxml==0.7.1 \ - --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ - --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - # via openvino-dev -filelock==3.20.3 \ - --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ - --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 - # via torch -fonttools==4.61.1 \ - --hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \ - --hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \ - --hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \ - --hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \ - --hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \ - --hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \ - --hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \ - --hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \ - --hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \ - --hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \ - --hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \ - --hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \ - --hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \ - --hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \ - --hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \ - --hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \ - --hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \ - --hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \ - --hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \ - --hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \ - --hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \ - --hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \ - --hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \ - --hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \ - --hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \ - --hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \ - --hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \ - --hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \ - --hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \ - --hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \ - --hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \ - --hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \ - --hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \ - --hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \ - --hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \ - --hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \ - --hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \ - --hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \ - --hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \ - --hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \ - --hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \ - --hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \ - --hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \ - --hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \ - --hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \ - --hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \ - --hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \ - --hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \ - --hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \ - --hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd - # via matplotlib -fsspec==2026.1.0 \ - --hash=sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc \ - --hash=sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b - # via torch -idna==2.10 \ - --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ - --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 # via requests inotify==0.2.12 \ --hash=sha256:9aee407f92c7d51a2ce50f3b78291a9094e334e34bd68e82bf60020795fa2c94 \ --hash=sha256:e4f1c8ec7ba5ec2a1a7fce48c0c917234af9d756495ebae7ffa00e41a305ab90 # via -r video/requirements.in -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via torch -kiwisolver==1.4.9 \ - --hash=sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c \ - --hash=sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7 \ - --hash=sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21 \ - --hash=sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e \ - --hash=sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff \ - --hash=sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7 \ - --hash=sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c \ - --hash=sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26 \ - --hash=sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa \ - --hash=sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f \ - --hash=sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1 \ - --hash=sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891 \ - --hash=sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77 \ - --hash=sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543 \ - --hash=sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d \ - --hash=sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce \ - --hash=sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3 \ - --hash=sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60 \ - --hash=sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a \ - --hash=sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089 \ - --hash=sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab \ - --hash=sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78 \ - --hash=sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771 \ - --hash=sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f \ - --hash=sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b \ - --hash=sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14 \ - --hash=sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32 \ - --hash=sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527 \ - --hash=sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185 \ - --hash=sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634 \ - --hash=sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed \ - --hash=sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1 \ - --hash=sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c \ - --hash=sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11 \ - --hash=sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752 \ - --hash=sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5 \ - --hash=sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4 \ - --hash=sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58 \ - --hash=sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5 \ - --hash=sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198 \ - --hash=sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536 \ - --hash=sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134 \ - --hash=sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf \ - --hash=sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2 \ - --hash=sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2 \ - --hash=sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370 \ - --hash=sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1 \ - --hash=sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154 \ - --hash=sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b \ - --hash=sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197 \ - --hash=sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386 \ - --hash=sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a \ - --hash=sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48 \ - --hash=sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748 \ - --hash=sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c \ - --hash=sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8 \ - --hash=sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5 \ - --hash=sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999 \ - --hash=sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369 \ - --hash=sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122 \ - --hash=sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b \ - --hash=sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 \ - --hash=sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9 \ - --hash=sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f \ - --hash=sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799 \ - --hash=sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028 \ - --hash=sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2 \ - --hash=sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525 \ - --hash=sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d \ - --hash=sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb \ - --hash=sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872 \ - --hash=sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64 \ - --hash=sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586 \ - --hash=sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf \ - --hash=sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552 \ - --hash=sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2 \ - --hash=sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415 \ - --hash=sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c \ - --hash=sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6 \ - --hash=sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64 \ - --hash=sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d \ - --hash=sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548 \ - --hash=sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07 \ - --hash=sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61 \ - --hash=sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d \ - --hash=sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771 \ - --hash=sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9 \ - --hash=sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c \ - --hash=sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3 \ - --hash=sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16 \ - --hash=sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145 \ - --hash=sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611 \ - --hash=sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2 \ - --hash=sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464 \ - --hash=sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2 \ - --hash=sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04 \ - --hash=sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54 \ - --hash=sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df \ - --hash=sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f \ - --hash=sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1 \ - --hash=sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220 - # via matplotlib -markupsafe==3.0.3 \ - --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ - --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ - --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ - --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ - --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 - # via jinja2 -matplotlib==3.10.8 \ - --hash=sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7 \ - --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \ - --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \ - --hash=sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3 \ - --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \ - --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \ - --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \ - --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \ - --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \ - --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \ - --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \ - --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \ - --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \ - --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \ - --hash=sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656 \ - --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \ - --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \ - --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \ - --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \ - --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \ - --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \ - --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \ - --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \ - --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \ - --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \ - --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \ - --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \ - --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \ - --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \ - --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \ - --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \ - --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \ - --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \ - --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \ - --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \ - --hash=sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17 \ - --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \ - --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \ - --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \ - --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \ - --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \ - --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \ - --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \ - --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \ - --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \ - --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \ - --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \ - --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \ - --hash=sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df \ - --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \ - --hash=sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933 \ - --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 \ - --hash=sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8 \ - --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \ - --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7 - # via ultralytics -mpmath==1.3.0 \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - # via sympy -networkx==3.1 \ - --hash=sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36 \ - --hash=sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61 - # via - # openvino-dev - # torch -numpy==1.26.4 \ - --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ - --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ - --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ - --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ - --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ - --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ - --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ - --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ - --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ - --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ - --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ - --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ - --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ - --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ - --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ - --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ - --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ - --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ - --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ - --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ - --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ - --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ - --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ - --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ - --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ - --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ - --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ - --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ - --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ - --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ - --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ - --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ - --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ - --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ - --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ - --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f - # via - # contourpy - # matplotlib - # opencv-python - # opencv-python-headless - # openvino - # openvino-dev - # scipy - # torchvision - # ultralytics - # ultralytics-thop -nvidia-cublas-cu12==12.8.4.1 \ - --hash=sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af \ - --hash=sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142 \ - --hash=sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0 - # via - # nvidia-cudnn-cu12 - # nvidia-cusolver-cu12 - # torch -nvidia-cuda-cupti-cu12==12.8.90 \ - --hash=sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed \ - --hash=sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e \ - --hash=sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182 - # via torch -nvidia-cuda-nvrtc-cu12==12.8.93 \ - --hash=sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909 \ - --hash=sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994 \ - --hash=sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8 - # via torch -nvidia-cuda-runtime-cu12==12.8.90 \ - --hash=sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d \ - --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90 \ - --hash=sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8 - # via torch -nvidia-cudnn-cu12==9.10.2.21 \ - --hash=sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8 \ - --hash=sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e \ - --hash=sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8 - # via torch -nvidia-cufft-cu12==11.3.3.83 \ - --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74 \ - --hash=sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7 \ - --hash=sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a - # via torch -nvidia-cufile-cu12==1.13.1.3 \ - --hash=sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc \ - --hash=sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a - # via torch -nvidia-curand-cu12==10.3.9.90 \ - --hash=sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9 \ - --hash=sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd \ - --hash=sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec - # via torch -nvidia-cusolver-cu12==11.7.3.90 \ - --hash=sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450 \ - --hash=sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34 \ - --hash=sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0 - # via torch -nvidia-cusparse-cu12==12.5.8.93 \ - --hash=sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b \ - --hash=sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd \ - --hash=sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc - # via - # nvidia-cusolver-cu12 - # torch -nvidia-cusparselt-cu12==0.7.1 \ - --hash=sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5 \ - --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623 \ - --hash=sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075 - # via torch -nvidia-nccl-cu12==2.27.5 \ - --hash=sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a \ - --hash=sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457 - # via torch -nvidia-nvjitlink-cu12==12.8.93 \ - --hash=sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88 \ - --hash=sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7 \ - --hash=sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f - # via - # nvidia-cufft-cu12 - # nvidia-cusolver-cu12 - # nvidia-cusparse-cu12 - # torch -nvidia-nvshmem-cu12==3.3.20 \ - --hash=sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0 \ - --hash=sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5 - # via torch -nvidia-nvtx-cu12==12.8.90 \ - --hash=sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f \ - --hash=sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e \ - --hash=sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615 - # via torch -opencv-python==4.11.0.86 \ - --hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \ - --hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \ - --hash=sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202 \ - --hash=sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a \ - --hash=sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d \ - --hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \ - --hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66 - # via ultralytics -opencv-python-headless==4.11.0.86 \ - --hash=sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b \ - --hash=sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca \ - --hash=sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca \ - --hash=sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb \ - --hash=sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798 \ - --hash=sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81 \ - --hash=sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b - # via -r video/requirements.in -openvino==2024.6.0 \ - --hash=sha256:05a436e2526bf6775b487712ab4a6decf7afe04183109bc7b129b95205ec4247 \ - --hash=sha256:151c6010f0613ace29087bf864dc5dd1db0afa25ab87379c735f85500545b167 \ - --hash=sha256:17cbdae3b069d9a13389b43a15b521a86fef75f350cfd0d992cc1780a6123788 \ - --hash=sha256:1d59ccc2df4a8c8515f30683f6cc0091c5b4e6a01a71ebfb1dc8f2ebdb5c83f4 \ - --hash=sha256:263496653200270b8a17456dc7bc67a198ed29b601b0ad7280d90783ba918d66 \ - --hash=sha256:2acfe89f61ec18d608c79147cedea6ba85d80783723ab9a9416891b74b15785e \ - --hash=sha256:444a6d3a746dd728654877993c30582faefcad2868c80ec261839a144b8925bc \ - --hash=sha256:46d3e20838c918668ec10ae124e92d56ff47a7c14da0d9d19d01d91fc9dece94 \ - --hash=sha256:4a94ef27022d56a159ea8677e5561bdd6f0d6c6237736e13c0f7cde71224ce7f \ - --hash=sha256:89c866fa87ddeccb982252e7d1c8516101a4acb95aaebac7e43f450112cb6942 \ - --hash=sha256:8d0390f4ab9b8f27814cf0feeddb0bba449ae8cd99933c2c2fae3011d33973b3 \ - --hash=sha256:9150c75e19606a2f2ab411271c79031726980fb6870b96445db1faca5ab64f5f \ - --hash=sha256:989c803d1f8e6d12ee8d2e3e8c7d56189d96688433b2238270dbfbf64f163f18 \ - --hash=sha256:ac812efa9c139387db534498a1e980103a35b9b2b5f3b2d1ae6ed43db51b4bba \ - --hash=sha256:c25d6820e960c08c0cc8d7823e9e6e395d762bc603772af14f0a576fab1628eb \ - --hash=sha256:c3477e833541df4f316240491d9df157c20a81487991748eb33e02e534009999 \ - --hash=sha256:c6c6a40a890b51007b20cf97d81783c4b43edfbcd4aabb06b6b04b7f2fb3137f \ - --hash=sha256:cb94f4ca9c1857bda2c5b510e519db20ea246958dbd76ddc38c133e2216231a5 \ - --hash=sha256:f7177d9a734a61a0708bcd706592f59fc2377d8efbb0bf9f8a33edfd2998a45d \ - --hash=sha256:ff5f871162299906fb517d5c9ded79f464a0341d8e2d55a037d4f9e38eed8652 - # via openvino-dev -openvino-dev==2024.6.0 \ - --hash=sha256:b8f4a1baeea1b138bc9b75b53dc3bdad3307c2cd4d58526ad977df81635870c5 - # via -r video/requirements.in -openvino-telemetry==2025.2.0 \ - --hash=sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c \ - --hash=sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599 - # via - # openvino - # openvino-dev packaging==26.0 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 - # via - # build - # matplotlib - # openvino - # openvino-dev - # wheel -pillow==12.1.0 \ - --hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \ - --hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \ - --hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \ - --hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \ - --hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \ - --hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \ - --hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \ - --hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \ - --hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \ - --hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \ - --hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \ - --hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \ - --hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \ - --hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \ - --hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \ - --hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \ - --hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \ - --hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \ - --hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \ - --hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \ - --hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \ - --hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \ - --hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \ - --hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \ - --hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \ - --hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \ - --hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \ - --hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \ - --hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \ - --hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \ - --hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \ - --hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \ - --hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \ - --hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \ - --hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \ - --hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \ - --hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \ - --hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \ - --hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \ - --hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \ - --hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \ - --hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \ - --hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \ - --hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \ - --hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \ - --hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \ - --hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \ - --hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \ - --hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \ - --hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \ - --hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \ - --hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \ - --hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \ - --hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \ - --hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \ - --hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \ - --hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \ - --hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \ - --hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \ - --hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \ - --hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \ - --hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \ - --hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \ - --hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \ - --hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \ - --hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \ - --hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \ - --hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \ - --hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \ - --hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \ - --hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \ - --hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \ - --hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \ - --hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \ - --hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \ - --hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \ - --hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \ - --hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \ - --hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \ - --hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \ - --hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \ - --hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \ - --hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \ - --hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \ - --hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \ - --hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \ - --hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \ - --hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \ - --hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \ - --hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \ - --hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd - # via - # matplotlib - # torchvision - # ultralytics -polars==1.37.1 \ - --hash=sha256:0309e2a4633e712513401964b4d95452f124ceabf7aec6db50affb9ced4a274e \ - --hash=sha256:377fed8939a2f1223c1563cfabdc7b4a3d6ff846efa1f2ddeb8644fafd9b1aff - # via ultralytics -polars-runtime-32==1.37.1 \ - --hash=sha256:04f5d5a2f013dca7391b7d8e7672fa6d37573a87f1d45d3dd5f0d9b5565a4b0f \ - --hash=sha256:0b8d4d73ea9977d3731927740e59d814647c5198bdbe359bcf6a8bfce2e79771 \ - --hash=sha256:55f2c4847a8d2e267612f564de7b753a4bde3902eaabe7b436a0a4abf75949a0 \ - --hash=sha256:68779d4a691da20a5eb767d74165a8f80a2bdfbde4b54acf59af43f7fa028d8f \ - --hash=sha256:a8362d11ac5193b994c7e9048ffe22ccfb976699cfbf6e128ce0302e06728894 \ - --hash=sha256:c682bf83f5f352e5e02f5c16c652c48ca40442f07b236f30662b22217320ce76 \ - --hash=sha256:da3d3642ae944e18dd17109d2a3036cb94ce50e5495c5023c77b1599d4c861bc \ - --hash=sha256:fbfde7c0ca8209eeaed546e4a32cca1319189aa61c5f0f9a2b4494262bd0c689 \ - --hash=sha256:fc82b5bbe70ca1a4b764eed1419f6336752d6ba9fc1245388d7f8b12438afa2c - # via polars -protobuf==5.29.5 \ - --hash=sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079 \ - --hash=sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc \ - --hash=sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353 \ - --hash=sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61 \ - --hash=sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5 \ - --hash=sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736 \ - --hash=sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e \ - --hash=sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84 \ - --hash=sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671 \ - --hash=sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238 \ - --hash=sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015 - # via vdms -psutil==7.2.1 \ - --hash=sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1 \ - --hash=sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266 \ - --hash=sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442 \ - --hash=sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79 \ - --hash=sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf \ - --hash=sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f \ - --hash=sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679 \ - --hash=sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8 \ - --hash=sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49 \ - --hash=sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f \ - --hash=sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129 \ - --hash=sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67 \ - --hash=sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6 \ - --hash=sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17 \ - --hash=sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42 \ - --hash=sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d \ - --hash=sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672 \ - --hash=sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a \ - --hash=sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc \ - --hash=sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3 \ - --hash=sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8 - # via - # -r video/requirements.in - # ultralytics -pyparsing==3.3.2 \ - --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ - --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc - # via matplotlib + # via build pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 # via build -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via matplotlib pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -949,72 +233,11 @@ pyyaml==6.0.3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 - # via - # openvino-dev - # ultralytics + # via -r video/requirements.in requests==2.32.5 \ - --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ - --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e - # via - # -r video/requirements.in - # openvino-dev - # ultralytics -scipy==1.15.3 \ - --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ - --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ - --hash=sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723 \ - --hash=sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730 \ - --hash=sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539 \ - --hash=sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb \ - --hash=sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6 \ - --hash=sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594 \ - --hash=sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92 \ - --hash=sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82 \ - --hash=sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49 \ - --hash=sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759 \ - --hash=sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba \ - --hash=sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982 \ - --hash=sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8 \ - --hash=sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65 \ - --hash=sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4 \ - --hash=sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e \ - --hash=sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed \ - --hash=sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c \ - --hash=sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5 \ - --hash=sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5 \ - --hash=sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019 \ - --hash=sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e \ - --hash=sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1 \ - --hash=sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889 \ - --hash=sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca \ - --hash=sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825 \ - --hash=sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9 \ - --hash=sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62 \ - --hash=sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb \ - --hash=sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b \ - --hash=sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13 \ - --hash=sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb \ - --hash=sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40 \ - --hash=sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c \ - --hash=sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253 \ - --hash=sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb \ - --hash=sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f \ - --hash=sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163 \ - --hash=sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45 \ - --hash=sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7 \ - --hash=sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11 \ - --hash=sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf \ - --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ - --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 - # via ultralytics -six==1.17.0 \ - --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ - --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via python-dateutil -sympy==1.14.0 \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 - # via torch + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via -r video/requirements.in tomli==2.4.0 \ --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ @@ -1064,69 +287,6 @@ tomli==2.4.0 \ --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 # via build -torch==2.9.1 \ - --hash=sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73 \ - --hash=sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7 \ - --hash=sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb \ - --hash=sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6 \ - --hash=sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e \ - --hash=sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a \ - --hash=sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4 \ - --hash=sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65 \ - --hash=sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587 \ - --hash=sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db \ - --hash=sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a \ - --hash=sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475 \ - --hash=sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9 \ - --hash=sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2 \ - --hash=sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e \ - --hash=sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d \ - --hash=sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083 \ - --hash=sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2 \ - --hash=sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c \ - --hash=sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951 \ - --hash=sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e \ - --hash=sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb \ - --hash=sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9 \ - --hash=sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e \ - --hash=sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c \ - --hash=sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b \ - --hash=sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d \ - --hash=sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6 - # via - # torchvision - # ultralytics - # ultralytics-thop -torchvision==0.24.1 \ - --hash=sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600 \ - --hash=sha256:1540a9e7f8cf55fe17554482f5a125a7e426347b71de07327d5de6bfd8d17caa \ - --hash=sha256:16274823b93048e0a29d83415166a2e9e0bf4e1b432668357b657612a4802864 \ - --hash=sha256:18f9cb60e64b37b551cd605a3d62c15730c086362b40682d23e24b616a697d41 \ - --hash=sha256:1b495edd3a8f9911292424117544f0b4ab780452e998649425d1f4b2bed6695f \ - --hash=sha256:1e39619de698e2821d71976c92c8a9e50cdfd1e993507dfb340f2688bfdd8283 \ - --hash=sha256:480b271d6edff83ac2e8d69bbb4cf2073f93366516a50d48f140ccfceedb002e \ - --hash=sha256:4aa6cb806eb8541e92c9b313e96192c6b826e9eb0042720e2fa250d021079952 \ - --hash=sha256:54ed17c3d30e718e08d8da3fd5b30ea44b0311317e55647cb97077a29ecbc25b \ - --hash=sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7 \ - --hash=sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050 \ - --hash=sha256:8a6696db7fb71eadb2c6a48602106e136c785642e598eb1533e0b27744f2cce6 \ - --hash=sha256:9ef95d819fd6df81bc7cc97b8f21a15d2c0d3ac5dbfaab5cbc2d2ce57114b19e \ - --hash=sha256:a0f106663e60332aa4fcb1ca2159ef8c3f2ed266b0e6df88de261048a840e0df \ - --hash=sha256:a9308cdd37d8a42e14a3e7fd9d271830c7fecb150dd929b642f3c1460514599a \ - --hash=sha256:ab211e1807dc3e53acf8f6638df9a7444c80c0ad050466e8d652b3e83776987b \ - --hash=sha256:af9201184c2712d808bd4eb656899011afdfce1e83721c7cb08000034df353fe \ - --hash=sha256:cccf4b4fec7fdfcd3431b9ea75d1588c0a8596d0333245dafebee0462abe3388 \ - --hash=sha256:d83e16d70ea85d2f196d678bfb702c36be7a655b003abed84e465988b6128938 \ - --hash=sha256:db2125c46f9cb25dc740be831ce3ce99303cfe60439249a41b04fd9f373be671 \ - --hash=sha256:ded5e625788572e4e1c4d155d1bbc48805c113794100d70e19c76e39e4d53465 \ - --hash=sha256:e3f96208b4bef54cd60e415545f5200346a65024e04f29a26cd0006dbf9e8e66 \ - --hash=sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193 \ - --hash=sha256:ec9d7379c519428395e4ffda4dbb99ec56be64b0a75b95989e00f9ec7ae0b2d7 \ - --hash=sha256:f035f0cacd1f44a8ff6cb7ca3627d84c54d685055961d73a1a9fb9827a5414c8 \ - --hash=sha256:f231f6a4f2aa6522713326d0d2563538fa72d613741ae364f9913027fa52ea35 \ - --hash=sha256:f476da4e085b7307aaab6f540219617d46d5926aeda24be33e1359771c83778f \ - --hash=sha256:fbdbdae5e540b868a681240b7dbd6473986c862445ee8a138680a6a97d6c34ff - # via ultralytics tornado==6.5 \ --hash=sha256:007f036f7b661e899bd9ef3fa5f87eb2cb4d1b2e7d67368e778e140a2f101a7a \ --hash=sha256:03576ab51e9b1677e4cdaae620d6700d9823568b7939277e4690fe4085886c55 \ @@ -1141,48 +301,7 @@ tornado==6.5 \ --hash=sha256:f81067dad2e4443b015368b24e802d0083fecada4f0a4572fdb72fc06e54a9a6 \ --hash=sha256:fd20c816e31be1bbff1f7681f970bbbd0bb241c364220140228ba24242bcdc59 # via -r video/requirements.in -triton==3.5.1 \ - --hash=sha256:02c770856f5e407d24d28ddc66e33cf026e6f4d360dcb8b2fabe6ea1fc758621 \ - --hash=sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8 \ - --hash=sha256:275a045b6ed670dd1bd005c3e6c2d61846c74c66f4512d6f33cc027b11de8fd4 \ - --hash=sha256:56765ffe12c554cd560698398b8a268db1f616c120007bfd8829d27139abd24a \ - --hash=sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94 \ - --hash=sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579 \ - --hash=sha256:8932391d7f93698dfe5bc9bead77c47a24f97329e9f20c10786bb230a9083f56 \ - --hash=sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478 \ - --hash=sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60 \ - --hash=sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232 \ - --hash=sha256:da47169e30a779bade679ce78df4810fca6d78a955843d2ddb11f226adc517dc \ - --hash=sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba \ - --hash=sha256:f617aa7925f9ea9968ec2e1adaf93e87864ff51549c8f04ce658f29bbdb71e2d \ - --hash=sha256:f63e34dcb32d7bd3a1d0195f60f30d2aee8b08a69a0424189b71017e23dfc3d2 - # via torch -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - # via torch -ultralytics==8.4.7 \ - --hash=sha256:37464fd86080a4cac278575a3e6a1a52ad311d45c8d317b863a437c82315cbb7 \ - --hash=sha256:7438696c981cf58d17250b0e4c7e29886bda9cc3e91ffbbf9544c622f65da91c - # via -r video/requirements.in -ultralytics-thop==2.0.18 \ - --hash=sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf \ - --hash=sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec - # via ultralytics -urllib3==1.26.20 \ - --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ - --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via requests -vdms==0.0.22 \ - --hash=sha256:4d59fedd914a645fb8a42c504c9535131f9de9a435b5add00900a2abade50036 \ - --hash=sha256:a1ca7fb79f81526ccf5cc9b5066bbbaa513a9b258002e40b4b93765b4739818a - # via -r video/requirements.in -wheel==0.46.3 \ - --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ - --hash=sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803 - # via -r video/requirements.in - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. -# pip From 250b82325dd2d6ad6a4d27e294ba272f0731b690 Mon Sep 17 00:00:00 2001 From: cwlacewe Date: Tue, 31 Mar 2026 11:06:46 -0700 Subject: [PATCH 05/20] Harden finetune code and add documentation Signed-off-by: cwlacewe --- .gitignore | 3 +- finetune/Dockerfile | 114 ++++- finetune/app/README.md | 85 ++++ finetune/app/finetune.py | 410 +++++++++++------- finetune/app/include/train_args.py | 15 + finetune/app/include/utils.py | 46 +- finetune/docker-compose.yml | 21 +- finetune/requirements.GPU.txt | 673 +++++++++++++++++++++++++++++ finetune/requirements.txt | 426 +++++++++++------- 9 files changed, 1418 insertions(+), 375 deletions(-) create mode 100644 finetune/app/README.md create mode 100644 finetune/app/include/train_args.py create mode 100644 finetune/requirements.GPU.txt diff --git a/.gitignore b/.gitignore index 42c342d..4c1c7a7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ build/* fastapi/resources/models/intel fastapi/resources/models/ultralytics -finetune/.env \ No newline at end of file +finetune/.env +finetune/app/*-Results diff --git a/finetune/Dockerfile b/finetune/Dockerfile index d311ff8..3468aab 100644 --- a/finetune/Dockerfile +++ b/finetune/Dockerfile @@ -1,10 +1,19 @@ -FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04 -ENV VIRTUAL_ENV=/opt/venv +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a AS build + ARG DEBIAN_FRONTEND=noninteractive +ENV VIRTUAL_ENV=/opt/venv + +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 -# Update and install necessary build tools and dependencies for OpenCV -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt-get update +RUN apt-get install --only-upgrade libc-bin libc6 && \ + apt-get install -y -q --no-install-recommends python3-setuptools \ + python3-dev python3-pip python3-venv \ + curl libgl1-mesa-glx \ + # Update and install necessary build tools and dependencies for OpenCV build-essential \ cmake \ git \ @@ -27,35 +36,47 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libv4l-dev \ libdc1394-dev \ libatlas-base-dev \ - gfortran \ - python3-dev \ - python3-numpy \ - python3-pip \ - python3-venv && \ + gfortran && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get clean + + +RUN curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb -o /tmp/cuda-keyring.deb && \ + dpkg -i /tmp/cuda-keyring.deb && \ + rm /tmp/cuda-keyring.deb +RUN apt-get update && apt-get install -y -q --no-install-recommends \ + cuda-toolkit-12-4 \ + libcudnn9-cuda-12 \ + libnvinfer10 \ + libnvonnxparsers10 \ + libnvinfer-plugin10 && \ rm -rf /var/lib/apt/lists/* + + RUN python3 -m venv ${VIRTUAL_ENV} -ENV PATH="$VIRTUAL_ENV/bin:$PATH" +ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" +ENV LD_LIBRARY_PATH="$VIRTUAL_ENV/lib:/usr/local/cuda/lib64:${LD_LIBRARY_PATH}" -COPY requirements.* /home/ RUN pip3 install pip --no-cache-dir --upgrade && \ - pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt + pip3 install --no-cache-dir "torch>=2.9.1" "torchvision>=0.24.1" "numpy<2.0" -# Define environment variables for OpenCV version and installation path -ENV OPENCV_VERSION="4.10.0" -ENV PYTHON_VERSION="3.$(${VIRTUAL_ENV}/bin/python -V | cut -f 1 | cut -d '.' -f 2)" -ENV DEPENDENCY_DIR=/tmp/build_opencv -ENV PYTHONPATH=${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages:${DEPENDENCY_DIR}/opencv/modules -# Working directory for source code +ARG PYTHON_VERSION=3.10 +ENV PYTHON_VERSION=${PYTHON_VERSION} +# OPENCV W/ CUDA SUPPORT +ENV OPENCV_VERSION="4.11.0" +# ENV PYTHON_VERSION="3.$(${VIRTUAL_ENV}/bin/python -V | cut -f 1 | cut -d '.' -f 2)" +ENV DEPENDENCY_DIR=/tmp/build_opencv +# ENV PYTHONPATH=${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages:${DEPENDENCY_DIR}/opencv/modules WORKDIR ${DEPENDENCY_DIR} # Clone OpenCV and OpenCV Contrib repositories RUN git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv.git ${DEPENDENCY_DIR}/opencv&& \ git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv_contrib.git ${DEPENDENCY_DIR}/opencv_contrib - # Create build directory and run CMake WORKDIR ${DEPENDENCY_DIR}/opencv/build -RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ +RUN NUMPY_PATH=$(${VIRTUAL_ENV}/bin/python -c "import numpy; print(numpy.get_include())") && \ + cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D BUILD_EXAMPLES=OFF \ -D BUILD_JAVA=OFF \ -D BUILD_opencv_python2=OFF \ @@ -71,7 +92,10 @@ RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D OPENCV_EXTRA_MODULES_PATH=${DEPENDENCY_DIR}/opencv_contrib/modules \ -D PYTHON_DEFAULT_EXECUTABLE=${VIRTUAL_ENV}/bin/python \ -D PYTHON3_EXECUTABLE=${VIRTUAL_ENV}/bin/python \ - -D PYTHON3_NUMPY_INCLUDE_DIRS=$(${VIRTUAL_ENV}/bin/python -c "import numpy; print(numpy.get_include())") \ + -D PYTHON3_NUMPY_INCLUDE_DIRS=${NUMPY_PATH} \ + -D PYTHON3_PACKAGES_PATH=${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages \ + -D PYTHON3_LIBRARY=${VIRTUAL_ENV}/lib/libpython${PYTHON_VERSION}.so \ + -D PYTHON3_INCLUDE_DIR=$(python3 -c "import sysconfig; print(sysconfig.get_path('include'))") \ -D WITH_CUBLAS=ON \ -D WITH_CUDA=ON \ -D WITH_CUDNN=ON \ @@ -83,12 +107,54 @@ RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ make install && \ ldconfig +# Verify where it actually installed +RUN find ${VIRTUAL_ENV} -name "cv2*.so" +# Test the import immediately +RUN ${VIRTUAL_ENV}/bin/python -c "import cv2; print(cv2.__version__)" # Clean up RUN rm -rf ${DEPENDENCY_DIR} -# Set environment variables for installed OpenCV -ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${VIRTUAL_ENV}/lib +ARG DEVICE="GPU" +ENV DEVICE="${DEVICE}" +# Set the working directory in the container WORKDIR /home -ENTRYPOINT python3 /home/finetune_test.py 2>&1 | tee /home/finetune.log \ No newline at end of file +RUN if [ "${DEVICE}" != "CPU" ]; then \ + # Dynamically find the site-packages folder without hardcoding '3.10' + PY_SITEPACKAGES=$(find ${VIRTUAL_ENV}/lib/ -maxdepth 2 -name "site-packages") && \ + echo "Found site-packages at: ${PY_SITEPACKAGES}" && \ + \ + # Link TensorRT libs + ln -sf ${PY_SITEPACKAGES}/tensorrt_libs/lib* /usr/lib/ && \ + \ + # Link NVIDIA libs + find ${PY_SITEPACKAGES}/nvidia/ -name "lib*.so*" -exec ln -sf {} /usr/lib/ \; && \ + \ + ldconfig; \ + fi + +COPY requirements.* /home/ +# RUN python -m pip install pip --upgrade --no-cache-dir && \ +# pip3 install --no-cache-dir -r requirements.in && \ +# if [ "${DEVICE}" = "CPU" ]; then \ +# pip3 install --no-cache-dir -r requirements.CPU.in; \ +# else \ +# apt-get update; \ +# apt-get install -y -q --no-install-recommends cuda-toolkit-12-4 libcudnn9-cuda-12 libnvinfer10 libnvonnxparsers10 ibnvinfer-plugin10; \ +# rm -rf /var/lib/apt/lists/*; \ +# pip3 install --no-cache-dir -r requirements.GPU.in; \ +# fi; +RUN python -m pip install pip --upgrade --no-cache-dir && \ + pip3 install --no-cache-dir --require-hashes -r requirements.txt && \ + if [ "${DEVICE}" = "CPU" ]; then \ + pip3 install --no-cache-dir --require-hashes -r requirements.CPU.txt; \ + else \ + apt-get update; \ + apt-get install -y -q --no-install-recommends cuda-toolkit-12-4 libcudnn9-cuda-12 libnvinfer10 libnvonnxparsers10 ibnvinfer-plugin10; \ + rm -rf /var/lib/apt/lists/*; \ + pip3 install --no-cache-dir --require-hashes -r requirements.GPU.txt; \ + fi; + +ARG DEBUG="0" +ENV DEBUG="${DEBUG}" diff --git a/finetune/app/README.md b/finetune/app/README.md new file mode 100644 index 0000000..951d24f --- /dev/null +++ b/finetune/app/README.md @@ -0,0 +1,85 @@ +# Fine-Tune YOLO Model + +This quide provides details on how to fine-tune a YOLO model using Ultralytics. +For simplicity, the use-case for this guide is Drone Detection. +Therefore, the goal is to finetune the YOLO11n model to detect only one class (`drone`). + + +## Drone Detection Dataset +To prepare for fine-tuning, first identify a large dataset for the use-case. +If detecting only one class, be sure the dataset contains negative images (without object of interest). +For a well rounded dataset, be sure it contains train, validation, AND test sets to follow the expected [Ultralytics YOLO Format](https://docs.ultralytics.com/datasets/detect/). +The provided script checks if the original dataset contains `train`, `validation`, AND `test` directories. +If it does not, it proceeds with converting the dataset into this format assuming the original dataset has `images` and `labels` directory with sub-directories `train`, `validation`, AND `test`. +If your dataset does not follow this format, please modify `prepare_dataset` in `finetune.py`. + +In this guide, the [SynDroneVision dataset](https://zenodo.org/records/13360116) is used. +Please see their [paper](https://ieeexplore.ieee.org/document/10943801) for more details. +The original dataset is saved in `SynDroneVision` directory and since it is not in the proper YOLO format, the script converts the data structure and saves new structure in directory `SynDroneVision_yolo`. ***NOTE:*** In other cases, if data structure is modified, the new directory prefixes the original directory name with `_yolo`. + + +## Training Configurations +The configurations used for training on 2x NVIDIA A100 80GB PCIe are specified in `include/train_args.py`. +Feel free to modify these parameters based on your hardware limitations such as VRAM of GPU. + + +## Finetune Script +THe finetune script is used to run training, validation, and also test on a provided video (optional). +To make deployment easy, we provide a Dockerfile which has the ideal environment and allow the script to run with deployment. +The script has a few adjustible arguments, so feel free to modify the call in next section as needed. +```bash +# Runs default arguments +python finetune.py +``` + +THe following arguments are available: + +| Argument | Default | Description | +| ----------------------------------------------- | ------------------------ | ----------- | +| -r RESULT_DIR,
--result-dir RESULT_DIR | `SynDroneVision-Results` | Directory to store any results. | +| --devices DEVICES_STR | `0,1` | A comma-separated list of GPU devices to use. This value will be CUDA_VISIBLE_DEVICES. | +| -d LOCAL_DATA_DIR,
--data-dir LOCAL_DATA_DIR | `/datasets` | Parent directory of dataset directories. | +| --dataset-name DATASET_NAME | `SynDroneVision` | Name of dataset; A directory with this name should be in --data-dir (-d). | +| -l LABELS_STR,
--labels LABELS_STR | `drone` | A comma-separated list of labels (classes) for model. | +| --yaml-name YAML_NAME | `drones` | Name of file (YAML_NAME.yaml) with data specifications. Be sure the `path` in this file correlates with `LOCAL_DATA_DIR` value. | +| --no-train | | Skip finetune stage | +| --test-video TEST_VIDEO | | Test video path for prediction. If not provided, inference is disabled | +
+ + +## Deployment +Python 3.10.12 on Ubuntu 22 was used for testing. +To manually setup your environment, use the provided `requirements.txt` and `requirements.GPU.txt` files. +Be sure your system contains the required NVIDIA packages to use GPUs for training. + +To avoid modifying your system for training, you can use the provided Dockerfile to deploy a container. +For easy access of host data, the `inputs` directory containing any input videos, the `finetune/app` directory containing this code, and the parent directory where datasets are stored (i.e. `/data1/datasets`) are mounted to the container. +Please see below for instructions for deploying container via `docker` and `docker compose`. +***NOTE:*** `REPO_DIR` is the path of this repo's main directory. Also be sure to update values in `.env` if using docker compose. +- **Docker:** For this option, be sure to build container first. You can start the container and finetune script via run command. + ```bash + REPO_DIR=`pwd` + + # Build container + cd finetune + docker build -f Dockerfile -t lcc_finetune:latest . + + # Run finetune script default values + docker run -it --ipc=host --gpus all \ + --name finetune_container \ + -v ${REPO_DIR}/inputs:/watch_dir \ + -v ${REPO_DIR}/finetune/app:/home \ + -v /data1/dataset:/datasets \ + lcc_finetune:latest /bin/bash -c "python finetune.py" + ``` +- **Docker Compose:** If different arguments are needed, please update `command:` in `finetune/docker-compose.yml` prior to deployment. + ```bash + REPO_DIR=`pwd` + + cd finetune + docker compose up + ``` + +Once all stages are completed, stop and/or remove running container. +Keep note of the latest model, as this model will be copied to different location for inclusion in full application. + diff --git a/finetune/app/finetune.py b/finetune/app/finetune.py index 0879004..72aacfe 100644 --- a/finetune/app/finetune.py +++ b/finetune/app/finetune.py @@ -7,199 +7,303 @@ # Otherwise only new classes will be detected (1st attempt) ################################################################################# +import argparse import gc import os import time +from datetime import datetime from pathlib import Path +from include.train_args import ( + BATCH_SIZE, + CLOSE_MOSAIC, + IMGZ_SHAPE, + LEARNING_RATE, + MULTI_SCALE, + NUM_EPOCHS, + NUM_WORKER_THREADS, + OPTIMIZER_NAME, + PATIENCE, + RECT_FLAG, + SCALE, + WARMUP_EPOCHS, +) from include.utils import ( DETECTION_THRESHOLD, IOU_THRESHOLD, - convert_SynDroneVision_2_Train_Structure, + convert_Dataset_2_Train_Structure, get_logger, ) from torch.cuda import empty_cache from ultralytics import YOLO -# WORKSPACE = Path(__file__).parent -TRAIN_MODEL = True -WORKSPACE = Path("/workspace/app") +WORKSPACE = Path(__file__).parent +RESULT_DIR = ( + WORKSPACE / "SynDroneVision-Results" +) # Default location where to store results SYSTEM_DATA_DIR = Path( - # "/data1/dataset" - "/workspace/dataset" -) # Path(__file__).parent # Where to store data -LOCAL_DATA_DIR = Path(__file__).parent / "data" # Where data info yamls are stored -ORIGINAL_DATA_DIR = SYSTEM_DATA_DIR / "SynDroneVision" -DATA_DIR = SYSTEM_DATA_DIR / "SynDroneVision_yolo" -YAML_PATH = DATA_DIR / "drones.yaml" -RESULT_DIR = WORKSPACE / "SynDroneVision-Results" -PROJECT_NAME = str(RESULT_DIR / "finetune_revised_3.9") - -TRAIN_RUN_NAME = "train_output" -DEVICES = [4, 5] # [0, 1] -os.environ["CUDA_VISIBLE_DEVICES"] = ",".join([str(d) for d in DEVICES]) -BATCH_SIZE = 16 # 8 # 16 -NUM_EPOCHS = 100 # 60 -IMGZ_SHAPE = 1280 # 1024 # 640 #Image shape: 2560x1489 too large, using 1280 -LEARNING_RATE = 0.001 # 0.001 -OPTIMIZER_NAME = "AdamW" -RECT_FLAG = False # True # Enables minimum padding strategy; cannot use with multi-gpu training -WARMUP_EPOCHS = ( - 3 # Set to 0 to prevent the learning rate from starting too low [Default: 3] -) -PATIENCE = 20 # 5 # Automatically stops training if no improvement after P epochs [Default: 100] -MULTI_SCALE = 0 # .75 #True # Change imgsz by up to a factor of 0.5 during training to be more accurate with multiple imgsz during inference -SCALE = 0.8 # Default:0.5 This tells YOLO to zoom in significantly on your 2560px images during training, effectively creating "crops" on the fly that keep the drone closer to its original size + "/datasets" +) # Default location where dataset directories are stored -VAL_RUN_NAME = "val_output" -NUM_WORKER_THREADS = 2 # 4 -DETECT_RUN_NAME = "predict_output" -TEST_VIDEO = "./inputs/anduril_swarm.mp4" -# TEST_VIDEO = "inputs/pexels-joseph-redfield-8459631 (1080p).mp4" +def csv_to_int_list(value_string): + """ + Converts a comma-separated string to a list of integers. + Raises argparse.ArgumentError if any value is not a valid integer. + """ + try: + # Split the string by commas and convert each part to an integer + return [int(item) for item in value_string.split(",")] + except ValueError as err: + # Raise an ArgumentTypeError to provide a clear error message to the user + raise argparse.ArgumentTypeError( + f"Invalid value: '{value_string}'. Values must be comma-separated integers." + ) from err -Path(PROJECT_NAME).mkdir(parents=True, exist_ok=True) -from datetime import datetime +def get_input_args(): + parser = argparse.ArgumentParser() -timestamp = datetime.now().strftime("%Y%m%d") -# log_filename = LOGS_DIR / f"{operation}_{timestamp}.log" -log_filename = Path(PROJECT_NAME) / f"finetune_test_{timestamp}.log" -logger = get_logger(log_filename) -logger.info( - f"🚀 Logging initialized. Writing to screen and {log_filename.relative_to(WORKSPACE)}" -) + """ GENERAL """ + parser.add_argument( + "-r", + "--result-dir", + type=Path, + default=RESULT_DIR, + help=f"Directory to store any results. [Default: {RESULT_DIR.relative_to(WORKSPACE)}]", + ) + parser.add_argument( + "--devices", + type=str, + dest="devices_str", + default="0,1", + help="A comma-separated list of GPU devices to use. This value will be CUDA_VISIBLE_DEVICES. [Default: 0,1]", + ) -# Convert Data Structure to Structure of Training -if not (DATA_DIR / "train/images").exists(): - convert_SynDroneVision_2_Train_Structure(ORIGINAL_DATA_DIR, DATA_DIR) + """ TRAINING """ + parser.add_argument( + "-d", + "--data-dir", + type=Path, + dest="local_data_dir", + default=SYSTEM_DATA_DIR, + help=f"Parent directory of dataset directories. [Default: {SYSTEM_DATA_DIR}]", + ) + parser.add_argument( + "--dataset-name", + type=str, + default="SynDroneVision", + help="Name of dataset; A directory with this name should be in --data-dir (-d). [Default: SynDroneVision]", + ) + parser.add_argument( + "-l", + "--labels", + type=str, + dest="labels_str", + default="drone", + help="A comma-separated list of labels (classes) for model. [Default: drone]", + ) + parser.add_argument( + "--yaml-name", + type=str, + default="drones", + help="Name of file (.yaml) with data specifications [Default: drones]", + ) + parser.add_argument( + "--no-train", + action="store_false", + dest="train_model", + help="Skip finetune stage", + ) + + """ INFERENCE """ + parser.add_argument( + "--test-video", + type=str, + help="Test video path for prediction. If not provided, inference is disabled", + ) + args = parser.parse_args() -# Generate dataset configuration for dataset -if not YAML_PATH.exists(): - data_info_content = f""" -# Dataset root directory -path: {DATA_DIR} # dataset root dir -train: train # train images relative path -val: val # validation images relative path -test: test # test images relative path (optional) + # Define additional variables + os.environ["CUDA_VISIBLE_DEVICES"] = args.devices_str + args.devices = csv_to_int_list(args.devices_str) + args.labels = args.labels_str.split(",") + # File/Directory definition + timestamp = datetime.now().strftime("%Y%m%d") + args.result_dir.mkdir(parents=True, exist_ok=True) + args.project_name = args.result_dir / f"finetune_{timestamp}" + args.log_filename = args.project_name / f"finetune_{timestamp}.log" + args.info_file = args.project_name / f"Summary_{timestamp}.txt" + args.project_name.mkdir(parents=True, exist_ok=True) + args.project_name = str(args.project_name) -# Num. of classes -nc: 1 + # Prepare dataset for finetuning/validation/inference + def prepare_dataset(): + args.original_data_dir = args.local_data_dir / args.dataset_name + if all( + not (args.original_data_dir / f"{stage}/images").exists() + for stage in ["train", "val", "test"] + ): + args.data_dir = args.local_data_dir / f"{args.dataset_name}_yolo" + if all( + not (args.data_dir / f"{stage}/images").exists() + for stage in ["train", "val", "test"] + ): + convert_Dataset_2_Train_Structure(args.original_data_dir, args.data_dir) + else: + args.data_dir = args.original_data_dir -# Classes -names: ["drone"] - """ + args.yaml_path = args.data_dir / f"{args.yaml_name}.yaml" - with open(YAML_PATH, "w") as f: - f.write(data_info_content) + # Generate dataset configuration for dataset + if not args.yaml_path.exists(): + num_labels = len(args.labels) + data_info_content = f""" + # Dataset root directory + path: {args.data_dir} # dataset root dir + train: train # train images relative path + val: val # validation images relative path + test: test # test images relative path (optional) -# Create Results directory -if not RESULT_DIR.exists(): - RESULT_DIR.mkdir(parents=True, exist_ok=True) + # Num. of classes + nc: {num_labels} + # Classes + names: {args.labels} + """ -""" TRAIN """ -if TRAIN_MODEL: - model = YOLO(RESULT_DIR / "yolo11n.pt") - start_train = time.time() - results = model.train( + with open(args.yaml_path, "w") as f: + f.write(data_info_content) + + prepare_dataset() + + return args + + +def main(args): + TRAIN_RUN_NAME = "train_output" + VAL_RUN_NAME = "val_output" + DETECT_RUN_NAME = "predict_output" + + logger = get_logger(args.log_filename) + logger.info( + f"🚀 Logging initialized. Writing to screen and {args.log_filename.relative_to(WORKSPACE)}" + ) + + """ TRAIN """ + if args.train_model: + logger.info("Running training ...") + model = YOLO(str(args.result_dir / "yolo11n.pt")) + start_train = time.time() + _ = model.train( + batch=BATCH_SIZE, + data=args.yaml_path, + epochs=NUM_EPOCHS, + imgsz=IMGZ_SHAPE, + lr0=LEARNING_RATE, + optimizer=OPTIMIZER_NAME, + project=args.project_name, + name=TRAIN_RUN_NAME, + device=args.devices, + patience=PATIENCE, + multi_scale=MULTI_SCALE, + workers=NUM_WORKER_THREADS, + rect=RECT_FLAG, + warmup_epochs=WARMUP_EPOCHS, + close_mosaic=CLOSE_MOSAIC, # Turn off mosaic earlier to stabilize + scale=SCALE, # set scale=0.8 or higher (the default is usually 0.5) + ) + train_time = time.time() - start_train + logger.info(f"Training took {train_time:0.3f} secs\n") + empty_cache() # Frees memory no longer used + gc.collect() # Forces garbage collector + else: + if ( + Path(f"{args.project_name}/{TRAIN_RUN_NAME}/results.csv").exists() + and not Path(f"{args.project_name}/{TRAIN_RUN_NAME}/results.png").exists() + ): + from ultralytics.utils.plotting import plot_results + + plot_results(file=f"{args.project_name}/{TRAIN_RUN_NAME}/results.csv") + + """ VALIDATION """ + # Check latest directory in case of multiple training runs + idx_run = 0 + original_TRAIN_RUN_NAME = TRAIN_RUN_NAME + for train_runs in Path(args.project_name).glob(f"{original_TRAIN_RUN_NAME}*"): + name_idx_str = train_runs.name.replace(original_TRAIN_RUN_NAME, "") + if name_idx_str != "" and int(name_idx_str) > idx_run: + TRAIN_RUN_NAME = train_runs.name + idx_run = int(name_idx_str) + + logger.info("Running validation ...") + model = YOLO(f"{args.project_name}/{TRAIN_RUN_NAME}/weights/best.pt") + start_val = time.time() + val_result = model.val( batch=BATCH_SIZE, - data=YAML_PATH, - epochs=NUM_EPOCHS, + data=args.yaml_path, imgsz=IMGZ_SHAPE, - lr0=LEARNING_RATE, - optimizer=OPTIMIZER_NAME, - project=PROJECT_NAME, - name=TRAIN_RUN_NAME, - device=DEVICES, - patience=PATIENCE, - multi_scale=MULTI_SCALE, + conf=DETECTION_THRESHOLD, + iou=IOU_THRESHOLD, + split="test", + project=args.project_name, + name=VAL_RUN_NAME, workers=NUM_WORKER_THREADS, - rect=RECT_FLAG, - warmup_epochs=WARMUP_EPOCHS, - close_mosaic=10, # Turn off mosaic earlier to stabilize - scale=SCALE, # set scale=0.8 or higher (the default is usually 0.5) + device=",".join([f"cuda:{d}" for d in args.devices]), ) - train_time = time.time() - start_train + val_time = time.time() - start_val + logger.info(f"Validation took {val_time:0.3f} secs\n") empty_cache() # Frees memory no longer used gc.collect() # Forces garbage collector -else: - if ( - Path(f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/results.csv").exists() - and not Path(f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/results.png").exists() - ): - from ultralytics.utils.plotting import plot_results - - plot_results(file=f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/results.csv") - - -""" VALIDATION """ -# Check latest directory in case of multiple training runs -idx_run = 0 -original_TRAIN_RUN_NAME = TRAIN_RUN_NAME -for train_runs in Path(PROJECT_NAME).glob(f"{original_TRAIN_RUN_NAME}*"): - name_idx_str = train_runs.name.replace(original_TRAIN_RUN_NAME, "") - if name_idx_str != "" and int(name_idx_str) > idx_run: - TRAIN_RUN_NAME = train_runs.name - idx_run = int(name_idx_str) - -model = YOLO(f"{PROJECT_NAME}/{TRAIN_RUN_NAME}/weights/best.pt") -start_val = time.time() -val_result = model.val( - batch=BATCH_SIZE, - data=YAML_PATH, - imgsz=IMGZ_SHAPE, - conf=DETECTION_THRESHOLD, - iou=IOU_THRESHOLD, - split="test", - project=PROJECT_NAME, - name=VAL_RUN_NAME, - workers=NUM_WORKER_THREADS, - device=",".join([f"cuda:{d}" for d in DEVICES]), -) -val_time = time.time() - start_val -empty_cache() # Frees memory no longer used -gc.collect() # Forces garbage collector - - -""" DETECT """ -start_detect = time.time() -result = model.predict( - source=TEST_VIDEO, - conf=DETECTION_THRESHOLD, - iou=IOU_THRESHOLD, - show=False, - imgsz=IMGZ_SHAPE, - save=True, - project=PROJECT_NAME, - name=DETECT_RUN_NAME, - exist_ok=False, # overwrite if folder exists - device=DEVICES[0], -) -detect_time = time.time() - start_detect -empty_cache() # Frees memory no longer used -gc.collect() # Forces garbage collector + """ INFERENCE """ + if args.test_video: + logger.info("Running inference ...") + start_detect = time.time() + _ = model.predict( + source=args.test_video, + conf=DETECTION_THRESHOLD, + iou=IOU_THRESHOLD, + show=False, + imgsz=IMGZ_SHAPE, + save=True, + project=args.project_name, + name=DETECT_RUN_NAME, + exist_ok=False, # overwrite if folder exists + device=args.devices[0], + ) + detect_time = time.time() - start_detect + logger.info(f"Inference took {detect_time:0.3f} secs\n") + empty_cache() # Frees memory no longer used + gc.collect() # Forces garbage collector + + """ SUMMARY """ + with open(args.info_file, "w") as f: + """ TRAINING SUMMARY """ + if args.train_model: + print( + f"Training took {train_time:0.3f} secs for bs {BATCH_SIZE} and {NUM_EPOCHS} epochs", + file=f, + ) -""" SUMMARY """ -info_file = Path(PROJECT_NAME) / "Summary.txt" -with open(info_file, "w") as f: - if TRAIN_MODEL: + """ VALIDATION SUMMARY """ print( - f"Training took {train_time:0.3f} secs for bs {BATCH_SIZE} and {NUM_EPOCHS} epochs", + f"\n\nValidation took {val_time:0.3f} secs", file=f, ) + print("mAP50-95:", val_result.box.map, file=f) + print("mAP50:", val_result.box.map50, file=f) + print("mAP75:", val_result.box.map75, file=f) + print("mAP:", val_result.box.maps, file=f) + + """ INFERENCE SUMMARY """ + if args.test_video: + print(f"\n\nDetection took {detect_time:0.3f} secs", file=f) - print( - f"\n\nValidation took {val_time:0.3f} secs", - file=f, - ) - print("mAP50-95:", val_result.box.map, file=f) - print("mAP50:", val_result.box.map50, file=f) - print("mAP75:", val_result.box.map75, file=f) - print("mAP:", val_result.box.maps, file=f) - print(f"\n\nDetection took {detect_time:0.3f} secs", file=f) +if __name__ == "__main__": + in_params = get_input_args() + main(in_params) diff --git a/finetune/app/include/train_args.py b/finetune/app/include/train_args.py new file mode 100644 index 0000000..1e58fbd --- /dev/null +++ b/finetune/app/include/train_args.py @@ -0,0 +1,15 @@ +NUM_WORKER_THREADS = 2 # 4 + +BATCH_SIZE = 16 # 8 # 16 +CLOSE_MOSAIC = 10 +IMGZ_SHAPE = 1280 # 1024 # 640 #Image shape: 2560x1489 too large, using 1280 +LEARNING_RATE = 0.001 # 0.001 +MULTI_SCALE = 0 # .75 #True # Change imgsz by up to a factor of 0.5 during training to be more accurate with multiple imgsz during inference +NUM_EPOCHS = 100 # 60 +OPTIMIZER_NAME = "AdamW" +PATIENCE = 20 # 5 # Automatically stops training if no improvement after P epochs [Default: 100] +RECT_FLAG = False # True # Enables minimum padding strategy; cannot use with multi-gpu training +SCALE = 0.8 # Default:0.5 This tells YOLO to zoom in significantly on your 2560px images during training, effectively creating "crops" on the fly that keep the drone closer to its original size +WARMUP_EPOCHS = ( + 3 # Set to 0 to prevent the learning rate from starting too low [Default: 3] +) diff --git a/finetune/app/include/utils.py b/finetune/app/include/utils.py index 1e93cb3..fac3e59 100644 --- a/finetune/app/include/utils.py +++ b/finetune/app/include/utils.py @@ -3,7 +3,7 @@ import sys from pathlib import Path -from colorlog import ColoredFormatter +# from colorlog import ColoredFormatter # Model Variables DETECTION_THRESHOLD = 0.25 @@ -46,30 +46,30 @@ def get_logger(log_filename): # Define the format (added date to file, kept succinct for screen) # We color the name of the logger (e.g., kiss, stdout, main) differently - console_formatter = ColoredFormatter( - "%(log_color)s%(levelname)-8s%(reset)s | %(name_log_color)s%(name)-12s%(reset)s | %(message)s", - log_colors={ - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "bold_red", - }, - secondary_log_colors={ - "name": { - "stdout": "purple", # Prints will be purple - "stderr": "red", # Stderr will be red - "ultralytics": "blue", # ultralytics logs will be blue - "openvino": "cyan", # GEPA logs will be cyan - "main": "white", # Main program is white - } - }, - style="%", - ) + # console_formatter = ColoredFormatter( + # "%(log_color)s%(levelname)-8s%(reset)s | %(name_log_color)s%(name)-12s%(reset)s | %(message)s", + # log_colors={ + # "DEBUG": "cyan", + # "INFO": "green", + # "WARNING": "yellow", + # "ERROR": "red", + # "CRITICAL": "bold_red", + # }, + # secondary_log_colors={ + # "name": { + # "stdout": "purple", # Prints will be purple + # "stderr": "red", # Stderr will be red + # "ultralytics": "blue", # ultralytics logs will be blue + # "openvino": "cyan", # GEPA logs will be cyan + # "main": "white", # Main program is white + # } + # }, + # style="%", + # ) file_formatter = logging.Formatter( "%(asctime)s | %(name)s | %(levelname)s | %(message)s" ) - # console_formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") + console_formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") # Create StreamHandler (Screen) console_handler = logging.StreamHandler(sys.__stdout__) @@ -103,7 +103,7 @@ def copy_file(src: Path, dst: Path): raise FileExistsError(f"File exists: {dst}") -def convert_SynDroneVision_2_Train_Structure(ORIGINAL_DATA_DIR, DATA_DIR): +def convert_Dataset_2_Train_Structure(ORIGINAL_DATA_DIR, DATA_DIR): for stage in ["train", "val", "test"]: (DATA_DIR / f"{stage}/images").mkdir(parents=True, exist_ok=True) (DATA_DIR / f"{stage}/labels").mkdir(parents=True, exist_ok=True) diff --git a/finetune/docker-compose.yml b/finetune/docker-compose.yml index 9e29108..d4e4971 100644 --- a/finetune/docker-compose.yml +++ b/finetune/docker-compose.yml @@ -1,13 +1,20 @@ services: finetune-service: - # image: lcc_finetune:latest - build: ./ - container_name: finetune_model + image: lcc_finetune:latest + build: + context: ./ + args: + http_proxy: "${http_proxy}" + HTTP_PROXY: "${HTTP_PROXY}" + https_proxy: "${https_proxy}" + HTTPS_PROXY: "${HTTPS_PROXY}" + container_name: finetune_container privileged: true network_mode: "host" + # shm_size: '2gb' # Give it plenty of space for video frames ipc: "host" - shm_size: '2gb' # Give it plenty of space for video frames + command: ["/bin/bash", "-c", "python /home/finetune.py --no-train"] environment: YOLO_CONFIG_DIR: "/tmp" DBHOST: "vdms-service" @@ -20,7 +27,7 @@ services: GPU_BATCH_SIZE: 1 DEBUG: "1" DEVICE: "GPU" - INGESTION: "object,face" + INGESTION: "object" WATCH_DIR: "/watch_dir" http_proxy: "${http_proxy}" HTTP_PROXY: "${HTTP_PROXY}" @@ -32,9 +39,7 @@ services: - /etc/localtime:/etc/localtime:ro - ../../inputs:/watch_dir:ro - ./app:/home - - ${LOCAL_DATA_DIR}:/dataset - networks: - - appnet + - ${LOCAL_DATA_DIR}:/datasets restart: always runtime: nvidia deploy: diff --git a/finetune/requirements.GPU.txt b/finetune/requirements.GPU.txt new file mode 100644 index 0000000..5a6fb34 --- /dev/null +++ b/finetune/requirements.GPU.txt @@ -0,0 +1,673 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --generate-hashes --output-file=fastapi/requirements.GPU.txt fastapi/requirements.GPU.in +# +click==8.3.1 \ + --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ + --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 + # via cucim +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via onnxslim +coloredlogs==15.0.1 \ + --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ + --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 + # via onnxruntime-gpu +cucim==23.10.0 \ + --hash=sha256:2c061ad28c3c1fa67bb62260f0a556f354c8ec2d6e3811eece375ed896d62945 + # via -r fastapi/requirements.GPU.in +cuda-toolkit[cudart]==12.8.1 \ + --hash=sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba + # via tensorrt-cu12-libs +cupy-cuda12x==13.6.0 \ + --hash=sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447 \ + --hash=sha256:4d2dfd9bb4705d446f542739a3616b4c9eea98d674fce247402cc9bcec89a1e4 \ + --hash=sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a \ + --hash=sha256:6ccd2fc75b0e0e24493531b8f8d8f978efecddb45f8479a48890c40d3805eb87 \ + --hash=sha256:771f3135861b68199c18b49345210180d4fcdce4681b51c28224db389c4aac5d \ + --hash=sha256:77ba6745a130d880c962e687e4e146ebbb9014f290b0a80dbc4e4634eb5c3b48 \ + --hash=sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1 \ + --hash=sha256:9e37f60f27ff9625dfdccc4688a09852707ec613e32ea9404f425dd22a386d14 \ + --hash=sha256:a20b7acdc583643a623c8d8e3efbe0db616fbcf5916e9c99eedf73859b6133af \ + --hash=sha256:a6970ceefe40f9acbede41d7fe17416bd277b1bd2093adcde457b23b578c5a59 \ + --hash=sha256:c790d012fd4d86872b9c89af9f5f15d91c30b8e3a4aa4dd04c2610f45f06ac44 \ + --hash=sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e \ + --hash=sha256:e5426ae3b1b9cf59927481e457a89e3f0b50a35b114a8034ec9110e7a833434c \ + --hash=sha256:e78409ea72f5ac7d6b6f3d33d99426a94005254fa57e10617f430f9fd7c3a0a1 \ + --hash=sha256:f33c9c975782ef7a42c79b6b4fb3d5b043498f9b947126d792592372b432d393 + # via -r fastapi/requirements.GPU.in +fastrlock==0.8.3 \ + --hash=sha256:001fd86bcac78c79658bac496e8a17472d64d558cd2227fdc768aa77f877fe40 \ + --hash=sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30 \ + --hash=sha256:05029d7080c0c61a81d5fee78e842c9a1bf22552cd56129451a252655290dcef \ + --hash=sha256:0a9dc6fa73174f974dfb22778d05a44445b611a41d5d3776b0d5daa9e50225c6 \ + --hash=sha256:0d6a77b3f396f7d41094ef09606f65ae57feeb713f4285e8e417f4021617ca62 \ + --hash=sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe \ + --hash=sha256:15e13a8b01a3bbf25f1615a6ac1d6ed40ad3bcb8db134ee5ffa7360214a8bc5c \ + --hash=sha256:1dd7f1520f7424793c812e1a4090570f8ff312725dbaf10a925b688aef7425f1 \ + --hash=sha256:1fced4cb0b3f1616be68092b70a56e9173713a4a943d02e90eb9c7897a7b5e07 \ + --hash=sha256:239e85cbebda16f14be92468ce648d0bc25e2442a3d11818deca59a7c43a4416 \ + --hash=sha256:24522689f4b5311afad0c8f998daec84a3dbe3a70cf821a615a763f843903030 \ + --hash=sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a \ + --hash=sha256:314e787532ce555a7362d3c438f0a680cd88a82c69b655e7181a4dd5e67712f5 \ + --hash=sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd \ + --hash=sha256:350f517a7d22d383f8ef76652b0609dc79de6693880a99bafc8a05c100e8c5e7 \ + --hash=sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4 \ + --hash=sha256:387b2ac642938a20170a50f528817026c561882ea33306c5cbe750ae10d0a7c2 \ + --hash=sha256:3df8514086e16bb7c66169156a8066dc152f3be892c7817e85bf09a27fa2ada2 \ + --hash=sha256:3e77a3d0ca5b29695d86b7d03ea88029c0ed8905cfee658eb36052df3861855a \ + --hash=sha256:40b328369005a0b32de14b699192aed32f549c2d2b27a5e1f614fb7ac4cec4e9 \ + --hash=sha256:45055702fe9bff719cdc62caa849aa7dbe9e3968306025f639ec62ef03c65e88 \ + --hash=sha256:494fc374afd0b6c7281c87f2ded9607c2731fc0057ec63bd3ba4451e7b7cb642 \ + --hash=sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160 \ + --hash=sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d \ + --hash=sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45 \ + --hash=sha256:558b538221e9c5502bb8725a1f51157ec38467a20498212838e385807e4d1b89 \ + --hash=sha256:55d42f6286b9d867370af4c27bc70d04ce2d342fe450c4a4fcce14440514e695 \ + --hash=sha256:5a0d31840a28d66573047d2df410eb971135a2461fb952894bf51c9533cbfea5 \ + --hash=sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c \ + --hash=sha256:5eef1d32d7614e0ceb6db198cf53df2a5830685cccbcf141a3e116faca967384 \ + --hash=sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670 \ + --hash=sha256:668fad1c8322badbc8543673892f80ee563f3da9113e60e256ae9ddd5b23daa4 \ + --hash=sha256:6cbfb6f7731b5a280851c93883624424068fa5b22c2f546d8ae6f1fd9311e36d \ + --hash=sha256:767ec79b7f6ed9b9a00eb9ff62f2a51f56fdb221c5092ab2dadec34a9ccbfc6e \ + --hash=sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5 \ + --hash=sha256:7a77ebb0a24535ef4f167da2c5ee35d9be1e96ae192137e9dc3ff75b8dfc08a5 \ + --hash=sha256:80876d9e04e8e35abbdb3e1a81a56558f4d5cf90c8592e428d4d12efce048347 \ + --hash=sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed \ + --hash=sha256:8c9d459ce344c21ff03268212a1845aa37feab634d242131bc16c2a2355d5f65 \ + --hash=sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da \ + --hash=sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8 \ + --hash=sha256:924abbf21eba69c1b35c04278f3ca081e8de1ef5933355756e86e05499123238 \ + --hash=sha256:92577ff82ef4a94c5667d6d2841f017820932bc59f31ffd83e4a2c56c1738f90 \ + --hash=sha256:963123bafc41c9fba72e57145917a3f23086b5d631b6cda9cf858c428a606ff9 \ + --hash=sha256:9842b7722e4923fe76b08d8c58a9415a9a50d4c29b80673cffeae4874ea6626a \ + --hash=sha256:9c2c24856d2adc60ab398780f7b7cd8a091e4bd0c0e3bb3e67f12bef2800f377 \ + --hash=sha256:9c4068f21fddc47393a3526ce95b180a2f4e1ac286db8d9e59e56771da50c815 \ + --hash=sha256:a0eadc772353cfa464b34c814b2a97c4f3c0ba0ed7b8e1c2e0ad3ebba84bf8e0 \ + --hash=sha256:a8fd6727c1e0952ba93fdc5975753781039772be6c1a3911a3afc87b53460dc0 \ + --hash=sha256:ac4fcc9b43160f7f64b49bd7ecfd129faf0793c1c8c6f0f56788c3bacae7f54a \ + --hash=sha256:accd897ab2799024bb87b489c0f087d6000b89af1f184a66e996d3d96a025a3b \ + --hash=sha256:b6ac082d670e195ad53ec8d0c5d2e87648f8838b0d48f7d44a6e696b8a9528e2 \ + --hash=sha256:bbbe31cb60ec32672969651bf68333680dacaebe1a1ec7952b8f5e6e23a70aa5 \ + --hash=sha256:bbc3bf96dcbd68392366c477f78c9d5c47e5d9290cb115feea19f20a43ef6d05 \ + --hash=sha256:c6e5bfecbc0d72ff07e43fed81671747914d6794e0926700677ed26d894d4f4f \ + --hash=sha256:cc5fa9166e05409f64a804d5b6d01af670979cdb12cd2594f555cb33cdc155bd \ + --hash=sha256:cdee8c02c20a0b17dbc52f54c48ede3bd421985e5d9cef5cd2136b14da967996 \ + --hash=sha256:d3ebb29de71bf9e330c2769c34a6b5e69d560126f02994e6c09635a2784f6de3 \ + --hash=sha256:d51f7fb0db8dab341b7f03a39a3031678cf4a98b18533b176c533c122bfce47d \ + --hash=sha256:d7edaf0071a6a98340fc2ec45b0ba37b7a16ed7761479aab577e41e09b3565e1 \ + --hash=sha256:d7f359bb989c01a5875e8dbde9acab37b9da0943b60ef97ba9887c4598eb3009 \ + --hash=sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c \ + --hash=sha256:da53350b90a67d5431df726816b041f1f96fd558ad6e2fc64948e13be3c7c29a \ + --hash=sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259 \ + --hash=sha256:de8c90c1a23fbe929d8a9628a6c1f0f1d8af6019e786354a682a26fa22ea21be \ + --hash=sha256:e0ceefadde046a5f6a261bfeaf25de9e0eba3ee790a9795b1fa9634111d3220e \ + --hash=sha256:f2b84b2fe858e64946e54e0e918b8a0e77fc7b09ca960ae1e50a130e8fbc9af8 \ + --hash=sha256:f68c551cf8a34b6460a3a0eba44bd7897ebfc820854e19970c52a76bf064a59f \ + --hash=sha256:fcb50e195ec981c92d0211a201704aecbd9e4f9451aea3a6f71ac5b1ec2c98cf + # via cupy-cuda12x +filelock==3.25.2 \ + --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ + --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 + # via torch +flatbuffers==25.12.19 \ + --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 + # via onnxruntime-gpu +fsspec==2026.2.0 \ + --hash=sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff \ + --hash=sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 + # via torch +humanfriendly==10.0 \ + --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ + --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc + # via coloredlogs +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via torch +lazy-loader==0.5 \ + --hash=sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3 \ + --hash=sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005 + # via cucim +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +ml-dtypes==0.5.4 \ + --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ + --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ + --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ + --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ + --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ + --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ + --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ + --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ + --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ + --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ + --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ + --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ + --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ + --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ + --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ + --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ + --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ + --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ + --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ + --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ + --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ + --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ + --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ + --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ + --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ + --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ + --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ + --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ + --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ + --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ + --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ + --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ + --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ + --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ + --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ + --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ + --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ + --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ + --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 + # via + # onnx + # onnxslim +mpmath==1.3.0 \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + # via sympy +networkx==3.4.2 \ + --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ + --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f + # via torch +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f + # via + # -r fastapi/requirements.GPU.in + # cucim + # cupy-cuda12x + # ml-dtypes + # onnx + # onnxruntime-gpu + # torchvision +nvidia-cublas-cu12==12.8.4.1 \ + --hash=sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af \ + --hash=sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142 \ + --hash=sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0 + # via + # nvidia-cudnn-cu12 + # nvidia-cusolver-cu12 + # torch +nvidia-cuda-cupti-cu12==12.8.90 \ + --hash=sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed \ + --hash=sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e \ + --hash=sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182 + # via torch +nvidia-cuda-nvrtc-cu12==12.8.93 \ + --hash=sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909 \ + --hash=sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994 \ + --hash=sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8 + # via torch +nvidia-cuda-runtime-cu12==12.8.90 \ + --hash=sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d \ + --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90 \ + --hash=sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8 + # via + # -r fastapi/requirements.GPU.in + # cuda-toolkit + # torch +nvidia-cudnn-cu12==9.10.2.21 \ + --hash=sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8 \ + --hash=sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e \ + --hash=sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8 + # via + # -r fastapi/requirements.GPU.in + # torch +nvidia-cufft-cu12==11.3.3.83 \ + --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74 \ + --hash=sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7 \ + --hash=sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a + # via torch +nvidia-cufile-cu12==1.13.1.3 \ + --hash=sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc \ + --hash=sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a + # via torch +nvidia-curand-cu12==10.3.9.90 \ + --hash=sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9 \ + --hash=sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd \ + --hash=sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec + # via torch +nvidia-cusolver-cu12==11.7.3.90 \ + --hash=sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450 \ + --hash=sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34 \ + --hash=sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0 + # via torch +nvidia-cusparse-cu12==12.5.8.93 \ + --hash=sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b \ + --hash=sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd \ + --hash=sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc + # via + # nvidia-cusolver-cu12 + # torch +nvidia-cusparselt-cu12==0.7.1 \ + --hash=sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5 \ + --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623 \ + --hash=sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075 + # via torch +nvidia-nccl-cu12==2.27.5 \ + --hash=sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a \ + --hash=sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457 + # via torch +nvidia-nvjitlink-cu12==12.8.93 \ + --hash=sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88 \ + --hash=sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7 \ + --hash=sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f + # via + # nvidia-cufft-cu12 + # nvidia-cusolver-cu12 + # nvidia-cusparse-cu12 + # torch +nvidia-nvshmem-cu12==3.3.20 \ + --hash=sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0 \ + --hash=sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5 + # via torch +nvidia-nvtx-cu12==12.8.90 \ + --hash=sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f \ + --hash=sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e \ + --hash=sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615 + # via torch +onnx==1.20.1 \ + --hash=sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2 \ + --hash=sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031 \ + --hash=sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a \ + --hash=sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826 \ + --hash=sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2 \ + --hash=sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c \ + --hash=sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00 \ + --hash=sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0 \ + --hash=sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3 \ + --hash=sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be \ + --hash=sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095 \ + --hash=sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431 \ + --hash=sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e \ + --hash=sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080 \ + --hash=sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5 \ + --hash=sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489 \ + --hash=sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945 \ + --hash=sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf \ + --hash=sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281 \ + --hash=sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67 \ + --hash=sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38 \ + --hash=sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a \ + --hash=sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def + # via + # -r fastapi/requirements.GPU.in + # onnxslim +onnxruntime-gpu==1.23.2 \ + --hash=sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4 \ + --hash=sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d \ + --hash=sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26 \ + --hash=sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f \ + --hash=sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2 \ + --hash=sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767 \ + --hash=sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59 \ + --hash=sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321 \ + --hash=sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95 + # via -r fastapi/requirements.GPU.in +onnxslim==0.1.82 \ + --hash=sha256:3190340f53c93620779f2159b41d114e571b7c1a0cfa8630cba3f7be92d3399e \ + --hash=sha256:4f48decf32863e583976fff6e9cfd9d6fe6a4a9814e7577c2cf8ce082973c6eb + # via -r fastapi/requirements.GPU.in +packaging==26.0 \ + --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ + --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 + # via + # lazy-loader + # onnxruntime-gpu + # onnxslim +pillow==12.1.1 \ + --hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \ + --hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \ + --hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \ + --hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \ + --hash=sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713 \ + --hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \ + --hash=sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9 \ + --hash=sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0 \ + --hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \ + --hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \ + --hash=sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 \ + --hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \ + --hash=sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5 \ + --hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \ + --hash=sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35 \ + --hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \ + --hash=sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff \ + --hash=sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 \ + --hash=sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4 \ + --hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \ + --hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \ + --hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \ + --hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \ + --hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \ + --hash=sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e \ + --hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \ + --hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \ + --hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \ + --hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \ + --hash=sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d \ + --hash=sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b \ + --hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \ + --hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \ + --hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \ + --hash=sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a \ + --hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \ + --hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \ + --hash=sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f \ + --hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \ + --hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \ + --hash=sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9 \ + --hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \ + --hash=sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40 \ + --hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \ + --hash=sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c \ + --hash=sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 \ + --hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \ + --hash=sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af \ + --hash=sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735 \ + --hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \ + --hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \ + --hash=sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b \ + --hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \ + --hash=sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9 \ + --hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \ + --hash=sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e \ + --hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \ + --hash=sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4 \ + --hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \ + --hash=sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 \ + --hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \ + --hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \ + --hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \ + --hash=sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3 \ + --hash=sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052 \ + --hash=sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 \ + --hash=sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293 \ + --hash=sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523 \ + --hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \ + --hash=sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b \ + --hash=sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80 \ + --hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \ + --hash=sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79 \ + --hash=sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23 \ + --hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \ + --hash=sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e \ + --hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \ + --hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \ + --hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \ + --hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \ + --hash=sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5 \ + --hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \ + --hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \ + --hash=sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 \ + --hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \ + --hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \ + --hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \ + --hash=sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3 \ + --hash=sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 \ + --hash=sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090 \ + --hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289 + # via torchvision +protobuf==7.34.1 \ + --hash=sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a \ + --hash=sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a \ + --hash=sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b \ + --hash=sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4 \ + --hash=sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280 \ + --hash=sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11 \ + --hash=sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7 \ + --hash=sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c + # via + # onnx + # onnxruntime-gpu +sympy==1.14.0 \ + --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ + --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + # via + # onnxruntime-gpu + # onnxslim + # torch +tensorrt-cu12==10.14.1.48.post1 \ + --hash=sha256:5a6d4d78560be7c8fff877711fa8334e8e2b441b702f047ea3107311b9897341 + # via -r fastapi/requirements.GPU.in +tensorrt-cu12-bindings==10.14.1.48.post1 \ + --hash=sha256:03bd44f645a30e04f38d4a866bdfb0e9e16a34601384ce29ef8d008950175828 \ + --hash=sha256:1cc29c5bb32a0719d5fadbbd2e69971837b2b0d2f1575d9a2ddc1cf3f6f5d8f0 \ + --hash=sha256:2a0b5a301c84d1c95e67cede52bb49f44fda02a45c6a4b409fa16f0632394046 \ + --hash=sha256:2b83b1608e21c25da72776501533b17fe1d000595b9a191613665f67e0598868 \ + --hash=sha256:40100265c49dc91e0a3f0a030e7de0077c034e6e30c11828001b036052de1d77 \ + --hash=sha256:68ae05d23f4918fdd36a505daf8b93b64444ef9328516284363480ab776a2595 \ + --hash=sha256:78da2abb803370147e75045eaeaa2a3f134f5aa7537405f86b22eaa36c0a11ed \ + --hash=sha256:9b2c8f41d847b202e35054fbaccd55f4efbb673da11f562befc273c0b2d65f48 \ + --hash=sha256:aad15bf393acc85b2e5015f0ca2082b0d162d5966ac1f58de48d1205446e237f \ + --hash=sha256:b2bf5597c7790c36fa858b8dfd6a867a482ee41d7c35e8732b3fd671b013869c \ + --hash=sha256:b90ce26abe1d49da527211411d023f95a235806fab2d00277585558e265f9b93 \ + --hash=sha256:d9cb40e646e11225b295eaeaf74aeb7e422c425271d51ca8c416776449fec617 + # via tensorrt-cu12 +tensorrt-cu12-libs==10.14.1.48.post1 \ + --hash=sha256:46e9e84e16ca7d89ca572e0900d9480945bb6faaa0c385e6f63e1ae46a834b25 + # via tensorrt-cu12 +torch==2.9.1 \ + --hash=sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73 \ + --hash=sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7 \ + --hash=sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb \ + --hash=sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6 \ + --hash=sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e \ + --hash=sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a \ + --hash=sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4 \ + --hash=sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65 \ + --hash=sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587 \ + --hash=sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db \ + --hash=sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a \ + --hash=sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475 \ + --hash=sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9 \ + --hash=sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2 \ + --hash=sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e \ + --hash=sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d \ + --hash=sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083 \ + --hash=sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2 \ + --hash=sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c \ + --hash=sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951 \ + --hash=sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e \ + --hash=sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb \ + --hash=sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9 \ + --hash=sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e \ + --hash=sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c \ + --hash=sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b \ + --hash=sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d \ + --hash=sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6 + # via + # -r fastapi/requirements.GPU.in + # torchvision +torchvision==0.24.1 \ + --hash=sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600 \ + --hash=sha256:1540a9e7f8cf55fe17554482f5a125a7e426347b71de07327d5de6bfd8d17caa \ + --hash=sha256:16274823b93048e0a29d83415166a2e9e0bf4e1b432668357b657612a4802864 \ + --hash=sha256:18f9cb60e64b37b551cd605a3d62c15730c086362b40682d23e24b616a697d41 \ + --hash=sha256:1b495edd3a8f9911292424117544f0b4ab780452e998649425d1f4b2bed6695f \ + --hash=sha256:1e39619de698e2821d71976c92c8a9e50cdfd1e993507dfb340f2688bfdd8283 \ + --hash=sha256:480b271d6edff83ac2e8d69bbb4cf2073f93366516a50d48f140ccfceedb002e \ + --hash=sha256:4aa6cb806eb8541e92c9b313e96192c6b826e9eb0042720e2fa250d021079952 \ + --hash=sha256:54ed17c3d30e718e08d8da3fd5b30ea44b0311317e55647cb97077a29ecbc25b \ + --hash=sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7 \ + --hash=sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050 \ + --hash=sha256:8a6696db7fb71eadb2c6a48602106e136c785642e598eb1533e0b27744f2cce6 \ + --hash=sha256:9ef95d819fd6df81bc7cc97b8f21a15d2c0d3ac5dbfaab5cbc2d2ce57114b19e \ + --hash=sha256:a0f106663e60332aa4fcb1ca2159ef8c3f2ed266b0e6df88de261048a840e0df \ + --hash=sha256:a9308cdd37d8a42e14a3e7fd9d271830c7fecb150dd929b642f3c1460514599a \ + --hash=sha256:ab211e1807dc3e53acf8f6638df9a7444c80c0ad050466e8d652b3e83776987b \ + --hash=sha256:af9201184c2712d808bd4eb656899011afdfce1e83721c7cb08000034df353fe \ + --hash=sha256:cccf4b4fec7fdfcd3431b9ea75d1588c0a8596d0333245dafebee0462abe3388 \ + --hash=sha256:d83e16d70ea85d2f196d678bfb702c36be7a655b003abed84e465988b6128938 \ + --hash=sha256:db2125c46f9cb25dc740be831ce3ce99303cfe60439249a41b04fd9f373be671 \ + --hash=sha256:ded5e625788572e4e1c4d155d1bbc48805c113794100d70e19c76e39e4d53465 \ + --hash=sha256:e3f96208b4bef54cd60e415545f5200346a65024e04f29a26cd0006dbf9e8e66 \ + --hash=sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193 \ + --hash=sha256:ec9d7379c519428395e4ffda4dbb99ec56be64b0a75b95989e00f9ec7ae0b2d7 \ + --hash=sha256:f035f0cacd1f44a8ff6cb7ca3627d84c54d685055961d73a1a9fb9827a5414c8 \ + --hash=sha256:f231f6a4f2aa6522713326d0d2563538fa72d613741ae364f9913027fa52ea35 \ + --hash=sha256:f476da4e085b7307aaab6f540219617d46d5926aeda24be33e1359771c83778f \ + --hash=sha256:fbdbdae5e540b868a681240b7dbd6473986c862445ee8a138680a6a97d6c34ff + # via -r fastapi/requirements.GPU.in +triton==3.5.1 \ + --hash=sha256:02c770856f5e407d24d28ddc66e33cf026e6f4d360dcb8b2fabe6ea1fc758621 \ + --hash=sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8 \ + --hash=sha256:275a045b6ed670dd1bd005c3e6c2d61846c74c66f4512d6f33cc027b11de8fd4 \ + --hash=sha256:56765ffe12c554cd560698398b8a268db1f616c120007bfd8829d27139abd24a \ + --hash=sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94 \ + --hash=sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579 \ + --hash=sha256:8932391d7f93698dfe5bc9bead77c47a24f97329e9f20c10786bb230a9083f56 \ + --hash=sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478 \ + --hash=sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60 \ + --hash=sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232 \ + --hash=sha256:da47169e30a779bade679ce78df4810fca6d78a955843d2ddb11f226adc517dc \ + --hash=sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba \ + --hash=sha256:f617aa7925f9ea9968ec2e1adaf93e87864ff51549c8f04ce658f29bbdb71e2d \ + --hash=sha256:f63e34dcb32d7bd3a1d0195f60f30d2aee8b08a69a0424189b71017e23dfc3d2 + # via torch +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # onnx + # torch diff --git a/finetune/requirements.txt b/finetune/requirements.txt index 1808cfa..3914f57 100644 --- a/finetune/requirements.txt +++ b/finetune/requirements.txt @@ -2,8 +2,20 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --generate-hashes --output-file=finetune/requirements.txt finetune/requirements.in +# pip-compile --generate-hashes --output-file=fastapi/requirements.txt fastapi/requirements.in # +annotated-doc==0.0.4 \ + --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ + --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 + # via fastapi +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.13.0 \ + --hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \ + --hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc + # via starlette certifi==2026.2.25 \ --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 @@ -139,14 +151,10 @@ charset-normalizer==3.4.6 \ --hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \ --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 # via requests -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via onnxslim -coloredlogs==15.0.1 \ - --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ - --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 - # via onnxruntime-gpu +click==8.3.1 \ + --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ + --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 + # via uvicorn contourpy==1.3.2 \ --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ @@ -206,20 +214,26 @@ contourpy==1.3.2 \ --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c # via matplotlib -cuda-toolkit[cudart]==12.8.1 \ - --hash=sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba - # via tensorrt-cu12-libs cycler==0.12.1 \ --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c # via matplotlib +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via openvino-dev +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 + # via anyio +fastapi==0.135.1 \ + --hash=sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e \ + --hash=sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd + # via -r fastapi/requirements.in filelock==3.25.2 \ --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 # via torch -flatbuffers==25.12.19 \ - --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 - # via onnxruntime-gpu fonttools==4.62.1 \ --hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \ --hash=sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a \ @@ -276,14 +290,16 @@ fsspec==2026.2.0 \ --hash=sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff \ --hash=sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 # via torch -humanfriendly==10.0 \ - --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ - --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc - # via coloredlogs +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via uvicorn idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via requests + # via + # anyio + # requests jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 @@ -555,57 +571,16 @@ matplotlib==3.10.8 \ --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \ --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7 # via ultralytics -ml-dtypes==0.5.4 \ - --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ - --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ - --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ - --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ - --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ - --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ - --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ - --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ - --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ - --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ - --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ - --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ - --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ - --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ - --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ - --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ - --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ - --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ - --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ - --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ - --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ - --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ - --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ - --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ - --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ - --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ - --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ - --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ - --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ - --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ - --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ - --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ - --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ - --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ - --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ - --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ - --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ - --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ - --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 - # via - # onnx - # onnxslim mpmath==1.3.0 \ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c # via sympy -networkx==3.4.2 \ - --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ - --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f - # via torch +networkx==3.1 \ + --hash=sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36 \ + --hash=sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61 + # via + # openvino-dev + # torch numpy==1.26.4 \ --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ @@ -644,13 +619,12 @@ numpy==1.26.4 \ --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f # via - # -r finetune/requirements.in + # -r fastapi/requirements.in # contourpy # matplotlib - # ml-dtypes - # onnx - # onnxruntime-gpu # opencv-python + # openvino + # openvino-dev # scipy # torchvision # ultralytics @@ -677,17 +651,12 @@ nvidia-cuda-runtime-cu12==12.8.90 \ --hash=sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d \ --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90 \ --hash=sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8 - # via - # -r finetune/requirements.in - # cuda-toolkit - # torch + # via torch nvidia-cudnn-cu12==9.10.2.21 \ --hash=sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8 \ --hash=sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e \ --hash=sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8 - # via - # -r finetune/requirements.in - # torch + # via torch nvidia-cufft-cu12==11.3.3.83 \ --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74 \ --hash=sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7 \ @@ -741,48 +710,6 @@ nvidia-nvtx-cu12==12.8.90 \ --hash=sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e \ --hash=sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615 # via torch -onnx==1.20.1 \ - --hash=sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2 \ - --hash=sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031 \ - --hash=sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a \ - --hash=sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826 \ - --hash=sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2 \ - --hash=sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c \ - --hash=sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00 \ - --hash=sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0 \ - --hash=sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3 \ - --hash=sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be \ - --hash=sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095 \ - --hash=sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431 \ - --hash=sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e \ - --hash=sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080 \ - --hash=sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5 \ - --hash=sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489 \ - --hash=sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945 \ - --hash=sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf \ - --hash=sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281 \ - --hash=sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67 \ - --hash=sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38 \ - --hash=sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a \ - --hash=sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def - # via - # -r finetune/requirements.in - # onnxslim -onnxruntime-gpu==1.23.2 \ - --hash=sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4 \ - --hash=sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d \ - --hash=sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26 \ - --hash=sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f \ - --hash=sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2 \ - --hash=sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767 \ - --hash=sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59 \ - --hash=sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321 \ - --hash=sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95 - # via -r finetune/requirements.in -onnxslim==0.1.82 \ - --hash=sha256:3190340f53c93620779f2159b41d114e571b7c1a0cfa8630cba3f7be92d3399e \ - --hash=sha256:4f48decf32863e583976fff6e9cfd9d6fe6a4a9814e7577c2cf8ce082973c6eb - # via -r finetune/requirements.in opencv-python==4.11.0.86 \ --hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \ --hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \ @@ -792,13 +719,45 @@ opencv-python==4.11.0.86 \ --hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \ --hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66 # via ultralytics +openvino==2024.6.0 \ + --hash=sha256:05a436e2526bf6775b487712ab4a6decf7afe04183109bc7b129b95205ec4247 \ + --hash=sha256:151c6010f0613ace29087bf864dc5dd1db0afa25ab87379c735f85500545b167 \ + --hash=sha256:17cbdae3b069d9a13389b43a15b521a86fef75f350cfd0d992cc1780a6123788 \ + --hash=sha256:1d59ccc2df4a8c8515f30683f6cc0091c5b4e6a01a71ebfb1dc8f2ebdb5c83f4 \ + --hash=sha256:263496653200270b8a17456dc7bc67a198ed29b601b0ad7280d90783ba918d66 \ + --hash=sha256:2acfe89f61ec18d608c79147cedea6ba85d80783723ab9a9416891b74b15785e \ + --hash=sha256:444a6d3a746dd728654877993c30582faefcad2868c80ec261839a144b8925bc \ + --hash=sha256:46d3e20838c918668ec10ae124e92d56ff47a7c14da0d9d19d01d91fc9dece94 \ + --hash=sha256:4a94ef27022d56a159ea8677e5561bdd6f0d6c6237736e13c0f7cde71224ce7f \ + --hash=sha256:89c866fa87ddeccb982252e7d1c8516101a4acb95aaebac7e43f450112cb6942 \ + --hash=sha256:8d0390f4ab9b8f27814cf0feeddb0bba449ae8cd99933c2c2fae3011d33973b3 \ + --hash=sha256:9150c75e19606a2f2ab411271c79031726980fb6870b96445db1faca5ab64f5f \ + --hash=sha256:989c803d1f8e6d12ee8d2e3e8c7d56189d96688433b2238270dbfbf64f163f18 \ + --hash=sha256:ac812efa9c139387db534498a1e980103a35b9b2b5f3b2d1ae6ed43db51b4bba \ + --hash=sha256:c25d6820e960c08c0cc8d7823e9e6e395d762bc603772af14f0a576fab1628eb \ + --hash=sha256:c3477e833541df4f316240491d9df157c20a81487991748eb33e02e534009999 \ + --hash=sha256:c6c6a40a890b51007b20cf97d81783c4b43edfbcd4aabb06b6b04b7f2fb3137f \ + --hash=sha256:cb94f4ca9c1857bda2c5b510e519db20ea246958dbd76ddc38c133e2216231a5 \ + --hash=sha256:f7177d9a734a61a0708bcd706592f59fc2377d8efbb0bf9f8a33edfd2998a45d \ + --hash=sha256:ff5f871162299906fb517d5c9ded79f464a0341d8e2d55a037d4f9e38eed8652 + # via openvino-dev +openvino-dev==2024.6.0 \ + --hash=sha256:b8f4a1baeea1b138bc9b75b53dc3bdad3307c2cd4d58526ad977df81635870c5 + # via -r fastapi/requirements.in +openvino-telemetry==2025.2.0 \ + --hash=sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c \ + --hash=sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599 + # via + # openvino + # openvino-dev packaging==26.0 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 # via # matplotlib - # onnxruntime-gpu - # onnxslim + # openvino + # openvino-dev + # wheel pillow==12.1.1 \ --hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \ --hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \ @@ -910,18 +869,19 @@ polars-runtime-32==1.39.3 \ --hash=sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562 \ --hash=sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860 # via polars -protobuf==7.34.1 \ - --hash=sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a \ - --hash=sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a \ - --hash=sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b \ - --hash=sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4 \ - --hash=sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280 \ - --hash=sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11 \ - --hash=sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7 \ - --hash=sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c - # via - # onnx - # onnxruntime-gpu +protobuf==5.29.5 \ + --hash=sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079 \ + --hash=sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc \ + --hash=sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353 \ + --hash=sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61 \ + --hash=sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5 \ + --hash=sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736 \ + --hash=sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e \ + --hash=sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84 \ + --hash=sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671 \ + --hash=sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238 \ + --hash=sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015 + # via vdms psutil==7.2.2 \ --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ @@ -945,6 +905,133 @@ psutil==7.2.2 \ --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 # via ultralytics +pydantic==2.12.5 \ + --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ + --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d + # via fastapi +pydantic-core==2.41.5 \ + --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ + --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ + --hash=sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504 \ + --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ + --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ + --hash=sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c \ + --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ + --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ + --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ + --hash=sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a \ + --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ + --hash=sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2 \ + --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ + --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ + --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ + --hash=sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba \ + --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ + --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ + --hash=sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963 \ + --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ + --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ + --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ + --hash=sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1 \ + --hash=sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2 \ + --hash=sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5 \ + --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ + --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ + --hash=sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b \ + --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ + --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ + --hash=sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093 \ + --hash=sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5 \ + --hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \ + --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ + --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ + --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ + --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ + --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ + --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ + --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ + --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ + --hash=sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f \ + --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ + --hash=sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51 \ + --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ + --hash=sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97 \ + --hash=sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a \ + --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ + --hash=sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9 \ + --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ + --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ + --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ + --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ + --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ + --hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \ + --hash=sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941 \ + --hash=sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3 \ + --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ + --hash=sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3 \ + --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ + --hash=sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe \ + --hash=sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146 \ + --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ + --hash=sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60 \ + --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ + --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ + --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ + --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ + --hash=sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460 \ + --hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \ + --hash=sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf \ + --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ + --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ + --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ + --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ + --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ + --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ + --hash=sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 \ + --hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \ + --hash=sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d \ + --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ + --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ + --hash=sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26 \ + --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ + --hash=sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8 \ + --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ + --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ + --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ + --hash=sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034 \ + --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ + --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ + --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ + --hash=sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b \ + --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ + --hash=sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a \ + --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ + --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ + --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ + --hash=sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a \ + --hash=sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556 \ + --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ + --hash=sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49 \ + --hash=sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2 \ + --hash=sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9 \ + --hash=sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b \ + --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ + --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ + --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ + --hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \ + --hash=sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82 \ + --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ + --hash=sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b \ + --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ + --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ + --hash=sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5 \ + --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ + --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ + --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b \ + --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 \ + --hash=sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425 \ + --hash=sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52 + # via pydantic pyparsing==3.3.2 \ --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc @@ -1027,11 +1114,15 @@ pyyaml==6.0.3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 - # via ultralytics + # via + # openvino-dev + # ultralytics requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via ultralytics + # via + # openvino-dev + # ultralytics scipy==1.15.3 \ --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ @@ -1084,33 +1175,14 @@ six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via python-dateutil +starlette==1.0.0 \ + --hash=sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149 \ + --hash=sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b + # via fastapi sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 - # via - # onnxruntime-gpu - # onnxslim - # torch -tensorrt-cu12==10.14.1.48.post1 \ - --hash=sha256:5a6d4d78560be7c8fff877711fa8334e8e2b441b702f047ea3107311b9897341 - # via -r finetune/requirements.in -tensorrt-cu12-bindings==10.14.1.48.post1 \ - --hash=sha256:03bd44f645a30e04f38d4a866bdfb0e9e16a34601384ce29ef8d008950175828 \ - --hash=sha256:1cc29c5bb32a0719d5fadbbd2e69971837b2b0d2f1575d9a2ddc1cf3f6f5d8f0 \ - --hash=sha256:2a0b5a301c84d1c95e67cede52bb49f44fda02a45c6a4b409fa16f0632394046 \ - --hash=sha256:2b83b1608e21c25da72776501533b17fe1d000595b9a191613665f67e0598868 \ - --hash=sha256:40100265c49dc91e0a3f0a030e7de0077c034e6e30c11828001b036052de1d77 \ - --hash=sha256:68ae05d23f4918fdd36a505daf8b93b64444ef9328516284363480ab776a2595 \ - --hash=sha256:78da2abb803370147e75045eaeaa2a3f134f5aa7537405f86b22eaa36c0a11ed \ - --hash=sha256:9b2c8f41d847b202e35054fbaccd55f4efbb673da11f562befc273c0b2d65f48 \ - --hash=sha256:aad15bf393acc85b2e5015f0ca2082b0d162d5966ac1f58de48d1205446e237f \ - --hash=sha256:b2bf5597c7790c36fa858b8dfd6a867a482ee41d7c35e8732b3fd671b013869c \ - --hash=sha256:b90ce26abe1d49da527211411d023f95a235806fab2d00277585558e265f9b93 \ - --hash=sha256:d9cb40e646e11225b295eaeaf74aeb7e422c425271d51ca8c416776449fec617 - # via tensorrt-cu12 -tensorrt-cu12-libs==10.14.1.48.post1 \ - --hash=sha256:46e9e84e16ca7d89ca572e0900d9480945bb6faaa0c385e6f63e1ae46a834b25 - # via tensorrt-cu12 + # via torch torch==2.9.1 \ --hash=sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73 \ --hash=sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7 \ @@ -1141,7 +1213,6 @@ torch==2.9.1 \ --hash=sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d \ --hash=sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6 # via - # -r finetune/requirements.in # torchvision # ultralytics # ultralytics-thop @@ -1174,9 +1245,7 @@ torchvision==0.24.1 \ --hash=sha256:f231f6a4f2aa6522713326d0d2563538fa72d613741ae364f9913027fa52ea35 \ --hash=sha256:f476da4e085b7307aaab6f540219617d46d5926aeda24be33e1359771c83778f \ --hash=sha256:fbdbdae5e540b868a681240b7dbd6473986c862445ee8a138680a6a97d6c34ff - # via - # -r finetune/requirements.in - # ultralytics + # via ultralytics triton==3.5.1 \ --hash=sha256:02c770856f5e407d24d28ddc66e33cf026e6f4d360dcb8b2fabe6ea1fc758621 \ --hash=sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8 \ @@ -1197,12 +1266,25 @@ typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via - # onnx + # anyio + # exceptiongroup + # fastapi + # pydantic + # pydantic-core + # starlette # torch + # typing-inspection + # uvicorn +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic ultralytics==8.4.7 \ --hash=sha256:37464fd86080a4cac278575a3e6a1a52ad311d45c8d317b863a437c82315cbb7 \ --hash=sha256:7438696c981cf58d17250b0e4c7e29886bda9cc3e91ffbbf9544c622f65da91c - # via -r finetune/requirements.in + # via -r fastapi/requirements.in ultralytics-thop==2.0.18 \ --hash=sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf \ --hash=sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec @@ -1211,3 +1293,15 @@ urllib3==2.6.3 \ --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via requests +uvicorn==0.42.0 \ + --hash=sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359 \ + --hash=sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775 + # via -r fastapi/requirements.in +vdms==0.0.22 \ + --hash=sha256:4d59fedd914a645fb8a42c504c9535131f9de9a435b5add00900a2abade50036 \ + --hash=sha256:a1ca7fb79f81526ccf5cc9b5066bbbaa513a9b258002e40b4b93765b4739818a + # via -r fastapi/requirements.in +wheel==0.46.3 \ + --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ + --hash=sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803 + # via -r fastapi/requirements.in From e9d6ad8a3701146cdccde2c390808a3925e2f9ba Mon Sep 17 00:00:00 2001 From: cwlacewe Date: Tue, 31 Mar 2026 11:10:35 -0700 Subject: [PATCH 06/20] Move finetune README location Signed-off-by: cwlacewe --- finetune/{app => }/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename finetune/{app => }/README.md (100%) diff --git a/finetune/app/README.md b/finetune/README.md similarity index 100% rename from finetune/app/README.md rename to finetune/README.md From 6692cc7e900e343e889cce3a6bf86838eee98100 Mon Sep 17 00:00:00 2001 From: "Lacewell, Chaunte W" Date: Tue, 31 Mar 2026 12:28:43 -0700 Subject: [PATCH 07/20] Moved finetune instructions to doc/ and added placeholder for latest pipeline Signed-off-by: Lacewell, Chaunte W --- finetune/README.md => doc/finetune.md | 1 + doc/pipeline.md | 57 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) rename finetune/README.md => doc/finetune.md (99%) create mode 100644 doc/pipeline.md diff --git a/finetune/README.md b/doc/finetune.md similarity index 99% rename from finetune/README.md rename to doc/finetune.md index 951d24f..ae6c471 100644 --- a/finetune/README.md +++ b/doc/finetune.md @@ -81,5 +81,6 @@ Please see below for instructions for deploying container via `docker` and `dock ``` Once all stages are completed, stop and/or remove running container. + Keep note of the latest model, as this model will be copied to different location for inclusion in full application. diff --git a/doc/pipeline.md b/doc/pipeline.md new file mode 100644 index 0000000..4e36216 --- /dev/null +++ b/doc/pipeline.md @@ -0,0 +1,57 @@ +# Detection using Fine-Tuned YOLO Model + +This guide assumes a YOLO model or fine-tuned YOLO model is available for detection. + + +## Prepare Model for Application + +Place your model in the appropriate directory for the application. + +| Directory | Description | +| ---------------------------------------------------- | ----------- | +| `fastapi/resources/models/ultralytics/custom_models` | Place any custom models in this directory. Each model file should have a unique name which will be used to deploy (and/or export) the model via the application start script. The PT model (`${UNIQUE_MODEL_NAME}.pt`) is exported to OpenVINO (`${UNIQUE_MODEL_NAME}_openvino_model/`) or TensorRT (`${UNIQUE_MODEL_NAME}.engine`), dependent on device used. | +| `fastapi/resources/models/ultralytics/${MODEL_NAME}/FP16` | Ultralytics YOLO models are typically placed in this directory where MODEL_NAME is the short name for the model (i.e. `yolo11n`). The PT model (`${MODEL_NAME}.pt`) is exported to OpenVINO (`${MODEL_NAME}_openvino_model/`) or TensorRT (`${MODEL_NAME}.engine`), dependent on device used. | + +Please note the model labels are retrieved from the model directly, so the model must contain these details. + + +## High Resolution Object Detection Pipeline +Resource limitation.... + +PLACE IMAGE OF PIPELINE + + +### Smart Filtering Pipeline +The Smart Filtering is a portion of the pipeline which filters high resolution videos for region of interest and the ROIs are only used in the detection phase instead of the entire frame. +This helps ...... + +The current implementation of the Smart Filtering pipeline is optimized for the test use-case, drone detection. +Drones are typically small in the video frames so if your use-case of interest has different objects, it may be beneficial to test the pipeline results on an existing test video for your use-case. + +For this case, we provide [``]() which annotates bbs of objects identified by the Smart Filtering pipeline onto each frame of the video for visual inspection. +If you are not satisfied with the results, feel free to modify/optimize the pipeline further. + +IDENTIFY OPTIMIZATION POINTS FOR EACH COMPONENT + +Once satisfied with annotated results, you can proceed with running the full pipeline. + + +## Pipeline Deployment + +DEPLLOYMENT +./stop.sh –p +./start_app.sh –e GPU –o –m + + +VISUALIZATION/QUERY +# View live detection +GOTO: http://:30077/ + +# View Page to Query +GOTO: http://:30007/ + + +STOPPING +./stop.sh –p + + From 37a50eba357f23378b0b7020a6f1b5c2bc50752a Mon Sep 17 00:00:00 2001 From: "Lacewell, Chaunte W" Date: Mon, 6 Apr 2026 10:59:40 -0700 Subject: [PATCH 08/20] Code cleanup Signed-off-by: Lacewell, Chaunte W --- .gitignore | 2 + doc/pipeline.md | 39 +- fastapi/include/handlers.py | 2362 +++++++++++++--------------------- fastapi/include/utils.py | 245 ++-- fastapi/main.py | 283 ++-- fastapi/nginx.conf | 6 +- fastapi/templates/index.html | 56 +- 7 files changed, 1336 insertions(+), 1657 deletions(-) diff --git a/.gitignore b/.gitignore index 4c1c7a7..0e4a938 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ fastapi/resources/models/intel fastapi/resources/models/ultralytics finetune/.env finetune/app/*-Results +fastapi/tests/*.mp4 +fastapi/tests/*_test_imgs diff --git a/doc/pipeline.md b/doc/pipeline.md index 4e36216..dfdc255 100644 --- a/doc/pipeline.md +++ b/doc/pipeline.md @@ -1,4 +1,4 @@ -# Detection using Fine-Tuned YOLO Model +# Detection using Fine-Tuned YOLO Model (WIP) This guide assumes a YOLO model or fine-tuned YOLO model is available for detection. @@ -29,8 +29,43 @@ The current implementation of the Smart Filtering pipeline is optimized for the Drones are typically small in the video frames so if your use-case of interest has different objects, it may be beneficial to test the pipeline results on an existing test video for your use-case. For this case, we provide [``]() which annotates bbs of objects identified by the Smart Filtering pipeline onto each frame of the video for visual inspection. + +For testing purposes, we will manually deploy the fastapi Dockerfile as it contains the same setup used in the application AND start the test script. +Here we will build the container, if not available: +```bash +REPO_DIR=`pwd` + +# Build container +cd fastapi +docker build --build-arg DEVICE=GPU -f Dockerfile -t lcc_fastapi:stream . +``` + +The script for testing the pipeline is already in `fastapi/tests/` and your test video is expected to be in the same directory (i.e. `anduril_swarm_8K.mp4`). +To deploy this test, run the following command but modify the name of the test_video. +The annotated video will be saved as `_annotated.mp4` in the same directory. +```bash +docker run --rm -it --ipc=host --gpus all \ +--name filtering_test \ +--env DEVICE=GPU \ +-v ${REPO_DIR}/inputs:/watch_dir \ +-v ${REPO_DIR}/fastapi/resources:/home/resources \ +-v ${REPO_DIR}/fastapi/tests:/home/tests \ +lcc_fastapi:stream /bin/bash -c "python /home/tests/filtering_test.py -v .mp4" +``` + + If you are not satisfied with the results, feel free to modify/optimize the pipeline further. +```bash +docker run --rm -it --ipc=host --gpus all \ +--name filtering_test \ +--env DEVICE=GPU \ +-v ${REPO_DIR}/inputs:/watch_dir \ +-v ${REPO_DIR}/fastapi/resources:/home/resources \ +-v ${REPO_DIR}/fastapi/tests:/home/tests \ +lcc_fastapi:stream bash +``` + IDENTIFY OPTIMIZATION POINTS FOR EACH COMPONENT Once satisfied with annotated results, you can proceed with running the full pipeline. @@ -38,7 +73,7 @@ Once satisfied with annotated results, you can proceed with running the full pip ## Pipeline Deployment -DEPLLOYMENT +DEPLOYMENT ./stop.sh –p ./start_app.sh –e GPU –o –m diff --git a/fastapi/include/handlers.py b/fastapi/include/handlers.py index 41b378c..ccfaab8 100644 --- a/fastapi/include/handlers.py +++ b/fastapi/include/handlers.py @@ -6,8 +6,6 @@ import shutil import subprocess import sys - -# Force FFmpeg to use more threads for decoding import threading import time import traceback @@ -16,17 +14,33 @@ from contextlib import asynccontextmanager from datetime import datetime -import cupy import cv2 import numpy as np -from cucim.skimage.measure import label as cucim_label + +# Force OpenCV to use a single thread for its operations. +# This prevents internal OpenCV threads from "racing" against your AI logic. +cv2.setNumThreads(1) from ultralytics import YOLO from ultralytics.utils.checks import check_imgsz from fastapi import FastAPI +# Global lock for thread-safe access to the shared YOLO model +model_lock = threading.Lock() + +# Create a global lock for stream management +stream_lock = asyncio.Lock() + +# os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( +# # "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" +# # "rtsp_transport;tcp|hwaccel;cuda|threads;4|probesize;5000000|analyzeduration;5000000" +# "rtsp_transport;udp|hwaccel;cuda|threads;8" +# "|stimeout;5000000|listen_timeout;5000" # Add timeouts to prevent hanging +# ) os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( - "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" + "rtsp_transport;tcp|hwaccel;cuda|threads;auto|low_delay;1|probesize;5000000" + # "rtsp_transport;tcp|hwaccel;cuda|threads;1|probesize;32|analyzeduration;0" + # "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" ) @@ -38,20 +52,18 @@ DEBUG_FLAG, DETECTION_THRESHOLD, DEVICE, - MODEL_H, + # MODEL_H, MODEL_NAME, - MODEL_PRECISION, - MODEL_W, + # MODEL_PRECISION, + # MODEL_W, OMIT_DETECTIONS_FLAG, TARGET_FPS, YOLO_CLASS_NAMES, PipelineMapping, - bbox_kernel, draw_label, filter_contained_boxes, get_detection_color, get_display_frame_in_bytes, - gpumat2cupy, manual_fps_calculation, merge_boxes_limit, metadata2vdms, @@ -67,18 +79,22 @@ # ----- SPECIAL VARIABLES ----- +MODEL_MAX_BATCH_SIZE = 64 +MODEL_PRECISION = "FP16" +MODEL_W, MODEL_H = (640, 640) +# MODEL_W, MODEL_H = (1280, 1280) CLIP_DURATION = 10 # seconds KERNEL_RATIO = 0.05 # 0.03 # .05 # .025 MASK_MAX_VALUE = 255 MASK_THRESHOLD_VALUE = 127 MAX_DETECTIONS = 100 MAX_WORKERS = 4 -# DISPLAY_FRAME_SIZE = (1280, 720) # DISPLAY_FRAME_SIZE = (640, 360) -# DISPLAY_FRAME_SIZE = (854, 480) +# DISPLAY_FRAME_QUALITY = 80 DISPLAY_FRAME_SIZE = (960, 540) DISPLAY_FRAME_QUALITY = 50 ENABLE_QUERYING = False +return_bytes = True # True, False if CUSTOM_MODEL_FLAG: model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" @@ -94,7 +110,7 @@ if torch.cuda.is_available(): torch.cuda.set_device(0) torch.cuda.empty_cache() - print(f"Using GPU: {torch.cuda.get_device_name(0)}") + print(f"Using GPU: {torch.cuda.get_device_name(0)}", flush=True) else: model_path += "_openvino_model/" @@ -197,64 +213,92 @@ async def auto_cleanup_janitor(app): while True: await asyncio.sleep(10) now = time.time() - # Iterating over a list of keys to avoid "dictionary changed size" error - for name in list(app.state.active_streams.keys()): - streamer = app.state.active_streams[name] - # Check if the stream is marked inactive OR timed out - # streamer.active should be False when the video source ends - if not streamer.active or (now - streamer.last_heartbeat > 30): - if DEBUG == "1": - print(f"CLEANUP: Removing {name} from active_streams") - streamer.stop() - del app.state.active_streams[name] + async with stream_lock: + # Iterating over a list of keys to avoid "dictionary changed size" error + for name in list(app.state.active_streams.keys()): + streamer = app.state.active_streams.get(name) + if not streamer: + continue + + backlog = streamer.get_executor_backlog() + + # Check if the stream is marked inactive OR timed out + # streamer.active should be False when the video source ends + is_stale = now - streamer.last_heartbeat > 30 + + should_remove = False + + if not streamer.active and backlog == 0: + should_remove = True # Video ended naturally + elif is_stale and backlog == 0: + should_remove = True # Browser tab closed/Network lost + elif now - streamer.last_heartbeat > 90: + should_remove = True # Hard timeout for hung processes + + if should_remove: + async with stream_lock: + if DEBUG == "1": + print(f"CLEANUP: Removing {name} from active_streams") + streamer.stop() + app.state.active_streams.pop(name, None) @asynccontextmanager async def lifespan(app: FastAPI): - # This is the ONLY place this should be initialized + # --- STARTUP --- if not hasattr(app.state, "active_streams"): app.state.active_streams = {} app.state.status = "Ready" - # app.state.model = YOLO(model_path, verbose=False, task="detect") + app.state.model = YOLO(model_path, verbose=False, task="detect") # app.state.model_lock = threading.Lock() - asyncio.create_task(auto_cleanup_janitor(app)) + device_input = "cuda" if DEVICE == "GPU" else "cpu" + print("Starting shared model warmup...") + dummy_input = torch.zeros((1, 3, MODEL_H, MODEL_W)).to(device_input) + for _ in range(20): + _ = app.state.model(dummy_input, verbose=False) + + janitor_task = asyncio.create_task(auto_cleanup_janitor(app)) + if DEBUG == "1": print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") + yield - # Cleanup logic here... - for s in app.state.active_streams.values(): - s.stop() + + # --- CLEANUP --- + janitor_task.cancel() + + # for s in app.state.active_streams.values(): + # s.stop() + + async with stream_lock: + for name, streamer in list(app.state.active_streams.items()): + print(f"Shutting down stream: {name}") + streamer.stop() # Custom stop method defined below + app.state.active_streams.pop(name, None) app.state.status = "Stopped" -class FastPinnedReader: - def __init__(self, source, h, w, maxlen=10, target_fps=TARGET_FPS): - self.cap = cv2.VideoCapture(str(source), cv2.CAP_FFMPEG) - # Enable multi-threaded CPU decoding (1 thread per 2K pixels approx) - # self.cap.set(cv2.CAP_PROP_HW_ACCEL, 0) - # os.environ["OPENCV_FFMPEG_THREADS"] = "4" # Force 16 CPU threads - self.h, self.w = h, w - self.maxlen = maxlen - self.target_fps = target_fps - self.frame_interval = 1.0 / target_fps # 0.0666s for 15 FPS - self.frame_queue = deque( - maxlen=self.maxlen - ) # Small queue to prevent VRAM bloat +class HybridReader: + """ + Decouples frame acquisition from processing. + Uses a background thread to ingest frames into a small deque, + preventing OpenCV buffer lag. + """ + + def __init__(self, source, target_fps=TARGET_FPS): + self.source = str(source) + self.cap = self._create_capture() + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Force low latency + + self.frame_queue = deque(maxlen=5) # Keep queue small to stay "real-time" self.stopped = False - # Pre-allocate Pinned (Page-Locked) Memory for the reader thread - # self.pinned_buf_1 = cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) - # self.pinned_buf_2 = cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) - # self.buffers = [self.pinned_buf_1, self.pinned_buf_2] - self.buffers = [ - cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) - for _ in range(self.maxlen) - ] - self.thread = None + self.target_fps = target_fps + self.frame_interval = 1.0 / target_fps + self.device = DEVICE # Global from include.utils def start(self): - self.thread = threading.Thread(target=self.update, daemon=True) - self.thread.start() + threading.Thread(target=self.update, daemon=True).start() return self def stop(self): @@ -263,116 +307,133 @@ def stop(self): if self.cap.isOpened(): self.cap.release() self.frame_queue.clear() - # Optionally join if you want to ensure the thread is dead + # Optionally join if want to ensure the thread is dead # self.thread.join(timeout=1.0) + def _create_capture(self): + """Creates a VideoCapture with stable RTSP options.""" + return cv2.VideoCapture(self.source, cv2.CAP_FFMPEG) + def update(self): - idx = 0 + """ + Continuously grabs frames. Throttles local files to maintain + the target FPS and manages RTSP reconnections. + """ + retry_attempt = 0 + max_retries = 10 + is_network_stream = "://" in self.source # Detect if it's RTSP + last_frame_time = time.perf_counter() + while not self.stopped: - ret, frame = self.cap.read() - if not ret: - self.stopped = True - break + # Grab frame from buffer + if not self.cap.grab(): + if not is_network_stream: + self.stopped = True + break + + # --- RECONNECTION LOGIC --- + retry_attempt += 1 + if retry_attempt > max_retries: + print(f"❌ [RTSP] Max retries reached for {self.source}. Stopping.") + self.stopped = True + break + + # Exponential Backoff: Wait 2s, 4s, 8s... up to 30s + wait_time = min(2**retry_attempt, 30) + # print(f"⚠️ [RTSP] Connection lost. Retry {retry_attempt}/{max_retries} in {wait_time}s...") + + self.cap.release() + time.sleep(wait_time) + self.cap = self._create_capture() + continue - # Rapid copy into pinned memory for DMA upload - target_buf = self.buffers[idx % self.maxlen] - target_buf[:] = frame - self.frame_queue.append((target_buf, idx)) - idx += 1 + # Throttle ingestion for local files to match real-time cadence + elapsed = time.perf_counter() - last_frame_time + if elapsed < self.frame_interval: + time.sleep(self.frame_interval - elapsed) - # # 2. SKIP frames (Light Bitstream Parsing) - # # This is the "Compute Saver" - it bypasses the decoder for N frames - # for _ in range(self.skip_count): - # if not self.cap.grab(): - # self.stopped = True - # break + last_frame_time = time.perf_counter() + success, frame = self.cap.retrieve() + + if success: + retry_attempt = 0 # Reset retries on successful frame + self.frame_queue.append(frame) + + # CPU/GPU Specific Handling + # if self.device == "GPU": + # # Keep as-is for DMA upload + # self.frame_queue.append(frame) + # else: + # # For CPU: Downscale immediately to save AI thread work + # # This is the BIGGEST FPS gain for CPU mode + # # small_frame = cv2.resize(frame, (MODEL_W, MODEL_H), interpolation=cv2.INTER_NEAREST) + # # self.frame_queue.append(small_frame) + # self.frame_queue.append(frame) + + # last_frame_time = time.time() def read(self): - return self.frame_queue.popleft() if self.frame_queue else (None, None) - - -# class FastPinnedReader: -# def __init__(self, source, h, w, maxlen=10, target_fps=15): -# self.cap = cv2.VideoCapture(str(source), cv2.CAP_FFMPEG) -# # Optimized for RTSP latency -# self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) -# self.maxlen = maxlen -# self.buffers = [cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) for _ in range(self.maxlen)] -# self.h, self.w = h, w -# self.stopped = False - -# # Pre-allocate two Pinned Buffers to prevent write-during-read -# self.buffers = [ -# cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3), -# cv2.cuda.createContinuous(h, w, cv2.CV_8UC3).reshape(h, w, 3) -# ] - -# self.latest_idx = 0 -# self.frame_ready = threading.Event() -# self.lock = threading.Lock() -# self.f_cnt = 0 - -# def start(self): -# self.thread = threading.Thread(target=self.update, daemon=True) -# self.thread.start() -# return self - -# def update(self): -# write_idx = 0 -# while not self.stopped: -# ret, frame = self.cap.read() -# if not ret: -# self.stopped = True -# break - -# # Switch between the two pinned buffers (Double Buffering) -# target_buf = self.buffers[write_idx % self.maxlen] -# target_buf[:] = frame - -# with self.lock: -# self.latest_idx = write_idx % self.maxlen -# self.f_cnt += 1 - -# self.frame_ready.set() # Signal inference thread that new data is here -# write_idx += 1 - -# def read(self): -# """Returns the absolute newest frame available.""" -# if not self.frame_ready.is_set(): -# return None, None - -# with self.lock: -# idx = self.latest_idx -# count = self.f_cnt -# self.frame_ready.clear() -# return self.buffers[idx], count - -# def stop(self): -# self.stopped = True -# if self.cap.isOpened(): -# self.cap.release() + return self.frame_queue.popleft() if self.frame_queue else None class BaseHandler: - def __init__(self, source, name, active_streams): - self.model = YOLO(model_path, verbose=False, task="detect") + """ + Core handler for camera metadata, hardware resource allocation, + and the common AI processing pipeline (BGS and YOLO). + """ + + def __init__(self, source, name, active_streams, **kwargs): + target_fps = kwargs.get("target_fps", TARGET_FPS) + self.model = kwargs.get("model") + + if not self.model: + self.model = YOLO(model_path, verbose=False, task="detect") + self.model_warmup() + + if hasattr(self.model, "names"): + self.label_source = [] + for k, v in self.model.names.items(): + self.label_source.append(v) + else: + self.label_source = YOLO_CLASS_NAMES + self.name = name self.source = source self.active = True self.active_streams = active_streams + self.frame_ready_event = asyncio.Event() + self.loop = asyncio.get_event_loop() - # 1. Capture setup - self.cap = cv2.VideoCapture(source, cv2.CAP_FFMPEG) + # Initialize hardware capture and determine stream properties + self.get_valid_video_capture() self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce latency - self.get_fps_and_framecnt() + self.get_fps_and_framecnt(target_fps) self.get_frameWH() - # 2. Performance Tracking + # Configure scaling for 8K-to-Model coordinate mapping + self.resize_h, self.resize_w = [MODEL_H, MODEL_W] + self.scale_x = self.frame_width / MODEL_W + self.scale_y = self.frame_height / MODEL_H + self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + # Determine minimum contour size relative to frame resolution + self.min_contour_area = int( + # (0.005 * self.frame_width) * (0.005 * self.frame_height) + # (0.005 * self.resize_w) * (0.005 * self.resize_h) + (0.01 * self.resize_w) * (0.01 * self.resize_h) + ) # 207 + + # Performance Tracking + self.frame_count = 0 # Frame count for videos self.stat_frame_count = 0 self.stat_fps = 0 self.latest_processed_frame = None self.last_frame_id = 0 + self.last_delivered_frame_id = -1 # Track what was actually sent + # Video Clipping self.video_writer = None self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v self.clip_id = 0 @@ -380,36 +441,60 @@ def __init__(self, source, name, active_streams): self.clip_key = "" self.tmp_file = "" - self.resize_h, self.resize_w = [MODEL_H, MODEL_W] - self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - self.numFrames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) - - self.scale_x = self.frame_width / MODEL_W - self.scale_y = self.frame_height / MODEL_H - self.min_contour_area = int( - (0.005 * self.frame_width) * (0.005 * self.frame_height) - ) # 207 - + # Default Kernels self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) self.dilate_kernel_for_enhanced_mask = np.ones((21, 21), np.uint8) # Device based setup if DEVICE == "GPU": self.prepare_gpu_pipeline() - self.warmup() + if len(self.active_streams) == 0: + self.warmup() else: self.operation_device_map = PipelineMapping( detection_device="cpu" ) # No CUDA HERE self.prepare_cpu_pipeline() - # 3. Start dedicated inference thread - self.model_warmup() + # Start dedicated inference thread and timers self.stat_start_time = time.perf_counter() self.last_heartbeat = time.time() self.setup_threads() + def get_valid_video_capture(self, connection_timeout=180): + # Robust Capture Setup with 3-minute Retry Logic + # connection_timeout = 180 # 3 minutes in seconds + start_connect_time = time.time() + retry_interval = 5 # Wait 5s between attempts + + self.cap = None + print(f"📡 [CONNECTING] {self.name} | Source: {self.source}") + + while time.time() - start_connect_time < connection_timeout: + self.cap = cv2.VideoCapture(self.source, cv2.CAP_FFMPEG) + + if self.cap.isOpened(): + # Quick check: can we actually grab a frame? + ret, _ = self.cap.read() + if ret: + print(f"✅ [CONNECTED] {self.name} established successfully.") + break + + # If we get here, connection failed (e.g., 404 Not Found) + self.cap.release() + print( + f"⚠️ [RETRYING] {self.name} | Stream not ready, retrying in {retry_interval}s..." + ) + time.sleep(retry_interval) + + # Final Connection Check + if not self.cap or not self.cap.isOpened(): + print( + f"❌ [FAILED] {self.name} could not connect after 3 minutes. Aborting." + ) + self.active = False + return # Exit early to prevent downstream FPS 0.0 crashes + def setup_threads(self): self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) self.process_thread = threading.Thread( @@ -469,48 +554,94 @@ def prepare_cpu_pipeline(self, method="knn"): else: raise ValueError(f"Provided method ({method}) is not available.") - def allocate_gpu(self, bkgd_mask_queue_size=3): - self.resized_frame = cv2.cuda_GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC3) + def cleanup_cpu(self): + """ + Purges large 8K NumPy buffers and CPU-based AI resources. + """ + # Nullify specific class references to allow Garbage Collection + self.executor = None + self.reader = None + self.latest_processed_frame = None - self.stream = cv2.cuda.Stream() + # 1. Clear the Ping-Pong buffers (up to 200MB of RAM) + if hasattr(self, "encode_buffers"): + self.encode_buffers.clear() - self.gpu_fullres_frame = cv2.cuda_GpuMat( - self.frame_height, self.frame_width, cv2.CV_8UC3 - ) + # 2. Clear the 10s video clip buffer + if hasattr(self, "frame_buffer"): + self.frame_buffer.clear() - self.pinned_downloaded_resizedframe_np = cv2.cuda.createContinuous( - self.resize_h, self.resize_w, cv2.CV_8UC3 - ) + # 3. Explicitly nullify large arrays to trigger Garbage Collection + self.resized_frame = None + self.fgMask = None + self.prev_bkgd = None - self.fgMask = cv2.cuda_GpuMat( - self.resize_h, self.resize_w, cv2.CV_8UC1 - ) # For resize + # 4. Clear the BGS history + if hasattr(self, "mask_history"): + self.mask_history.clear() - self.prev_bkgd = cv2.cuda_GpuMat( - self.resize_h, self.resize_w, cv2.CV_8UC1 - ) # For resize + def allocate_gpu(self, bkgd_mask_queue_size=3): + """ + Allocates persistent GpuMat buffers and CUDA streams to + enable zero-copy GPU processing. + """ + self.stream = cv2.cuda.Stream() + self.gpu_fullres_frame = cv2.cuda.GpuMat( + self.frame_height, self.frame_width, cv2.CV_8UC3 + ) + self.resized_frame = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC3) + self.fgMask = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + self.prev_bkgd = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) self.prev_bkgd.setTo((255,)) - self.mask_history = deque(maxlen=bkgd_mask_queue_size) self.mask_history.append(self.prev_bkgd) + self.gpu_threshold_dst_frame = cv2.cuda.GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + self.gpu_morphed_frame = cv2.cuda.GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) - self.gpu_threshold_dst_frame = cv2.cuda_GpuMat( + # Create continuous buffers to prevent stride artifacts during 8K downloads + self.pinned_downloaded_resizedframe_np = cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC3 + ) + self.pinned_downloaded_frame_np = cv2.cuda.createContinuous( self.resize_h, self.resize_w, cv2.CV_8UC1 ) cv2.cuda.createContinuous( self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_threshold_dst_frame ) - - self.gpu_morphed_frame = cv2.cuda_GpuMat( - self.resize_h, self.resize_w, cv2.CV_8UC1 - ) # For resize cv2.cuda.createContinuous( self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_morphed_frame ) - self.pinned_downloaded_frame_np = cv2.cuda.createContinuous( - self.resize_h, self.resize_w, cv2.CV_8UC1 - ) + # self.fgMask = cv2.cuda.GpuMat( + # self.resize_h, self.resize_w, cv2.CV_8UC1 + # ) # For resize + # # self.fgMask = cv2.cuda.createContinuous( + # # self.resize_h, self.resize_w, cv2.CV_8UC1 + # # ) + + # self.prev_bkgd = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + # self.prev_bkgd.setTo((255,)) + + # self.mask_history = deque(maxlen=bkgd_mask_queue_size) + # self.mask_history.append(self.prev_bkgd) + + # self.gpu_threshold_dst_frame = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + # cv2.cuda.createContinuous(self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_threshold_dst_frame) + # self.gpu_threshold_dst_frame = cv2.cuda.createContinuous(self.resize_h, self.resize_w, cv2.CV_8UC1) + + # self.gpu_morphed_frame = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + # cv2.cuda.createContinuous(self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_morphed_frame) + # self.gpu_morphed_frame = cv2.cuda.createContinuous( + # self.resize_h, self.resize_w, cv2.CV_8UC1 + # ) + + # self.pinned_downloaded_frame_np = cv2.cuda.createContinuous( + # self.resize_h, self.resize_w, cv2.CV_8UC1 + # ) def prepare_gpu_pipeline(self): self.operation_device_map = PipelineMapping( @@ -546,42 +677,122 @@ def prepare_gpu_pipeline(self): cv2.MORPH_DILATE, cv2.CV_8UC1, self.dilate_kernel_for_enhanced_mask ) + def cleanup_gpu(self): + """ + Explicitly releases all GPU-allocated memory to prevent + VRAM leaks in 8K concurrent streams. + """ + # Iterate through class attributes to explicitly release VRAM. + for attr_name in list(self.__dict__.keys()): + attr_value = getattr(self, attr_name) + + # 2. Check if the attribute is a GpuMat + if isinstance(attr_value, cv2.cuda.GpuMat): + # 🏎️ Force the NVIDIA driver to deallocate this specific memory segment + attr_value.release() + setattr(self, attr_name, None) + print(f"✅ Released GpuMat: {attr_name}") + + if hasattr(self, "gpu_fullres_frame") and self.gpu_fullres_frame is not None: + try: + self.gpu_fullres_frame.release() + except Exception: + self.gpu_fullres_frame = None + + if hasattr(self, "gpu_encoder_8k_buf") and self.gpu_encoder_8k_buf is not None: + try: + self.gpu_encoder_8k_buf.release() + except Exception: + self.gpu_encoder_8k_buf = None + + if hasattr(self, "gpu_display_frame") and self.gpu_display_frame is not None: + try: + self.gpu_display_frame.release() + except Exception: + self.gpu_display_frame = None + + self.pinned_downloaded_resizedframe_np = None + self.gpu_threshold_dst_frame = None + self.gpu_morphed_frame = None + self.pinned_downloaded_frame_np = None + + # 3. Handle specific buffers (like your Ping-Pong lists) + if hasattr(self, "encode_buffers"): + self.encode_buffers.clear() + + # 4. Clear the BGS history + if hasattr(self, "mask_history"): + self.mask_history.clear() + + # 4. Optional: Final flush of the CUDA caching allocator + if torch.cuda.is_available(): + torch.cuda.empty_cache() + def warmup(self): # WARM UP (Crucial for first-run latency) # JIT kernels are compiled on the first call - self.gpu_warmup_frame = cv2.cuda_GpuMat( - self.frame_height, self.frame_width, cv2.CV_8U - ) - self.gpu_warmup_input_frame = cv2.cuda_GpuMat( - self.frame_height, self.frame_width, cv2.CV_8U - ) - self.gpu_warmup_input_frame_np = cv2.cuda.createContinuous( - self.frame_height, self.frame_width, cv2.CV_8UC3 - ) - self.gpu_warmup_input_frame_np[:] = [255, 0, 0] - cv2.cuda.createContinuous( - self.frame_height, self.frame_width, cv2.CV_8U, self.gpu_warmup_frame - ) - cv2.cuda.createContinuous( - self.frame_height, self.frame_width, cv2.CV_8U, self.gpu_warmup_input_frame - ) + h, w = self.resize_h, self.resize_w + + self.gpu_warmup_input_frame_np = cv2.cuda.createContinuous(h, w, cv2.CV_8UC3) + + if self.gpu_warmup_input_frame_np is not None: + self.gpu_warmup_input_frame_np[:] = [255, 0, 0] + + gpu_warmup_input_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + gpu_warmup_input_frame.upload(self.gpu_warmup_input_frame_np) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_warmup_input_frame) + + # Trigger compiler + gpu_warmup_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_warmup_frame) + + # cv2.cuda.cvtColor( + # gpu_warmup_input_frame, + # cv2.COLOR_BGR2GRAY, + # stream=self.stream, + # dst=gpu_warmup_frame, + # ) + cv2.cuda.resize( + gpu_warmup_input_frame, + (self.resize_w, self.resize_h), + stream=self.stream, + dst=gpu_warmup_frame, + interpolation=cv2.INTER_NEAREST, + ) + # Thresholding + gpu_threshold_dst_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_threshold_dst_frame) + cv2.cuda.threshold( + gpu_warmup_frame, + MASK_THRESHOLD_VALUE, + MASK_MAX_VALUE, + cv2.THRESH_BINARY, + gpu_threshold_dst_frame, + self.stream, + ) - self.gpu_warmup_input_frame.upload(self.gpu_warmup_input_frame_np) - cv2.cuda.cvtColor( - self.gpu_warmup_input_frame, - cv2.COLOR_BGR2GRAY, - stream=self.stream, - dst=self.gpu_warmup_frame, - ) - self.stream.waitForCompletion() + gpu_morphed_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_morphed_frame) + self.dilate_filter.apply( + gpu_threshold_dst_frame, gpu_morphed_frame, self.stream + ) + self.stream.waitForCompletion() def model_warmup(self): - print("Starting warmup...") - dummy_input = torch.zeros((1, 3, self.resize_h, self.resize_w)).to( - self.device_input - ) # Match your benchmark size - for _ in range(20): - _ = self.model(dummy_input, verbose=False) + """Run warmup in a separate thread to prevent FastAPI lockup.""" + + def _warmup(iterations=5): + with model_lock: # Use the global lock + print(f"Starting warmup for {self.name}...") + dummy_input = torch.zeros((1, 3, self.resize_h, self.resize_w)).to( + self.device_input + ) + for _ in range(iterations): + _ = self.model(dummy_input, verbose=False) + print(f"Warmup complete for {self.name}") + + # Run in the background so the dashboard loads instantly + threading.Thread(target=_warmup, daemon=True).start() def get_executor_backlog(self): """Returns the number of tasks currently waiting in the thread pool queue.""" @@ -612,28 +823,74 @@ def stop(self): # self.update_thread.join(timeout=1.0) # self.process_thread.join(timeout=1.0) - def apply_background_subtraction_gpu(self, include_history=True, method="and"): + def apply_background_subtraction_cpu( + self, include_history=True, method="and", stream=None + ): + self.fgMask = self.backSub.apply( + self.cpu_resized_frame, learningRate=float(self.lr) + ) + + if include_history: + # If this is the first run, clone the mask instead of ANDing with an empty/white buffer + # if len(self.mask_history) < 1: + # self.prev_bkgd.setTo(0, stream) # Clear the initial white buffer + + for m in list(self.mask_history): + # Dilate the historical mask on CPU + dilated = cv2.dilate( + m, self.dilate_kernel_for_enhanced_mask, iterations=1 + ) + + if method == "or": + # Bitwise OR on CPU + cv2.bitwise_or(self.prev_bkgd, dilated, dst=self.prev_bkgd) + else: + # Bitwise AND on CPU + cv2.bitwise_and(self.prev_bkgd, dilated, dst=self.prev_bkgd) + + self.mask_history.append(self.fgMask.copy()) + + if ( + self.prev_bkgd.max() != self.prev_bkgd.min() + and self.prev_bkgd.max() > 0 + ): + combined_mask_bool = (self.fgMask > 0) | (self.prev_bkgd > 0) + self.fgMask = combined_mask_bool.astype(np.uint8) * 255 + + def apply_background_subtraction_gpu( + self, include_history=True, method="and", stream=None + ): self.fgMask = self.backSub.apply( - self.resized_frame, float(self.lr), stream=self.stream + self.resized_frame, float(self.lr), stream=stream ) if include_history: + # If this is the first run, clone the mask instead of ANDing with an empty/white buffer + if len(self.mask_history) < 1: + self.prev_bkgd.setTo(0, stream) # Clear the initial white buffer + for m in list(self.mask_history): # Dilate the historical mask on GPU - dilated = self.dilate_filter_for_enhanced_mask.apply(m) + dilated = self.dilate_filter_for_enhanced_mask.apply(m, stream=stream) if method == "or": # Bitwise OR on GPU - cv2.cuda.bitwise_or(self.prev_bkgd, dilated, self.prev_bkgd) + cv2.cuda.bitwise_or( + self.prev_bkgd, dilated, self.prev_bkgd, stream=stream + ) else: # Bitwise AND on GPU - cv2.cuda.bitwise_and(self.prev_bkgd, dilated, self.prev_bkgd) + cv2.cuda.bitwise_and( + self.prev_bkgd, dilated, self.prev_bkgd, stream=stream + ) self.mask_history.append(self.fgMask.clone()) min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) - if max_val != min_val: - self.fgMask = cv2.cuda.bitwise_or(self.fgMask, self.prev_bkgd) + if max_val != min_val and max_val > 0: + self.fgMask = cv2.cuda.bitwise_or( + self.fgMask, self.prev_bkgd, stream=stream + ) def check_disk_usage(self, path, min_gb=0.5): """Returns True if there is at least min_gb available at path.""" @@ -647,12 +904,20 @@ def check_disk_usage(self, path, min_gb=0.5): return False # Gets video fps and framecount - def get_fps_and_framecnt(self): + def get_fps_and_framecnt(self, target_fps): self.input_fps = int(self.cap.get(cv2.CAP_PROP_FPS)) # hardware fps + # print(f"in fps: {sself.input_fps} target fps: {target_fps}") if self.input_fps == 0: # Case when FPS isn't available - self.input_fps = manual_fps_calculation(self.name, num_frames=10) + self.input_fps = manual_fps_calculation(self.source, num_frames=10) + print(f"new in fps: {self.input_fps}") + + self.target_fps = ( + target_fps + if target_fps not in [None, 0] and self.input_fps > target_fps + else self.input_fps + ) + print(f"in fps: {self.input_fps} self.target fps: {self.target_fps}") - self.target_fps = TARGET_FPS if self.input_fps > TARGET_FPS else self.input_fps self.frame_skip = int(self.input_fps / self.target_fps) if self.frame_skip < 1: self.frame_skip = 1 @@ -665,11 +930,6 @@ def get_fps_and_framecnt(self): print(f"FPS of {self.name} input stream: {self.input_fps}", flush=True) print(f"FPS of {self.name} output mp4: {self.target_fps}", flush=True) - # Frame count for videos - self.frame_count = None - if "://" not in str(self.source): - self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) - # Gets frame W and H details def get_frameWH(self): input_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) @@ -688,8 +948,8 @@ def get_frameWH(self): def update_frame(self): self.stat_frame_count += 1 elapsed = time.perf_counter() - self.stat_start_time - if elapsed > 1.0: - self.stat_fps = self.stat_frame_count / elapsed + if elapsed > 0.5: + self.stat_fps = round(self.stat_frame_count / elapsed, 1) def run_model(self, frame, batch=1, device_input="cuda", stream=True): results = self.model.predict( @@ -708,231 +968,383 @@ def run_realtime_inference(self): pass -class VideoStreamHandler(BaseHandler): +class VideoStreamHandler_WIP(BaseHandler): + """ + Advanced handler optimized for 8K resolution at 15FPS. + Implements Ping-Pong buffering and background JPEG encoding to bypass the GIL. + """ + + def __init__(self, source, name, active_streams, **kwargs): + """ + Initializes the 8K pipeline and pre-allocates isolated memory buffers. + + Args: + source (str): The RTSP URL or local file path. + name (str): Unique identifier for the stream. + active_streams (dict): Global dictionary tracking all running handlers. + """ + # Initialize BaseHandler to set up cap, frame dimensions, and model + super().__init__(source, name, active_streams, **kwargs) + self.reader = HybridReader(source=self.source, target_fps=self.target_fps) + + # Isolated memory buffers to prevent the Producer loop from overwriting + # frames currently being encoded by the background worker. + self.encode_buffers = [ + np.zeros((self.frame_height, self.frame_width, 3), dtype=np.uint8), + np.zeros((self.frame_height, self.frame_width, 3), dtype=np.uint8), + ] + self.buf_idx = 0 + + # 3. Enhanced synchronization for 8K/15FPS + self.frame_buffer = deque( + maxlen=self.MAX_FRAMES_PER_CLIP + ) # 10s buffer for video clips + self.buffer_lock = threading.Lock() + self.disp_w, self.disp_h = DISPLAY_FRAME_SIZE + + if DEVICE == "GPU": + # This prevents the AI thread from overwriting the encoder's data. + self.gpu_encoder_8k_buf = cv2.cuda.createContinuous( + self.frame_height, self.frame_width, cv2.CV_8UC3 + ) + + # Continuous allocation prevents stride/padding artifacts + self.gpu_display_frame = cv2.cuda.createContinuous( + self.disp_h, self.disp_w, cv2.CV_8UC3 + ) + + # Create a dedicated background stream for encoding tasks + self.encode_stream = cv2.cuda.Stream() + + def setup_threads(self): + self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) # 1) + self.process_thread = threading.Thread( + target=self.run_realtime_inference, daemon=True + ) + + def start(self): + """ + Starts the decoupled ingestion and inference threads in the correct order. + """ + # 1. Start the hardware-decoupled reader first + self.reader.start() + + # 2. Small delay to allow the reader's deque to populate + time.sleep(0.1) + + # 3. Start the main inference producer loop + if not self.process_thread.is_alive(): + self.process_thread.start() + + return self + + def stop(self): + """ + Comprehensive resource release. Stops threads, shuts down the pool, + and purges VRAM to prevent leaks in concurrent 8K environments. + """ + # Signal threads to stop + self.active = False + + # Stop reader thread first + if hasattr(self, "reader"): + self.reader.stop() + + # if self.process_thread.is_alive(): + # self.process_thread.join(timeout=1.0) + + # Shutdown the executor to stop background JPEG encoding + # wait=True ensures no 'zombie' threads are left accessing GpuMats + if hasattr(self, "executor"): + self.executor.shutdown(wait=True) + + if self.cap: + self.cap.release() + self.cap = None + + # Purge HW Buffers + if DEVICE == "GPU": + self.cleanup_gpu() + else: + self.cleanup_cpu() + + # Final Reset of the FastAPI event + self.frame_ready_event.set() # Unblock any generators waiting on this stream + def async_yolo_task(self, data): - """Heavy lifting moved to ThreadPoolExecutor""" + """ + Orchestrates the AI pipeline for a single frame. + + Steps: + 1. (if GPU) Synchronizes CUDA stream and downloads the mask. + 2. Executes contour-based YOLO detection. + 3. Handoffs the frame to the background JPEG encoder. + """ + # return_bytes=False try: + frameNum = data["frameNum"] + + # Use pre-allocated pinned memory from BaseHandler for 8K mask download if self.device_input == "cuda": + self.stream.waitForCompletion() + # Ensure the download uses the instance-specific CUDA stream self.pinned_downloaded_frame_np = data["mask"].download(self.stream) - frame_bytes = self.contour2predictions( - data["frameNum"], - self.pinned_downloaded_frame_np, - data["full_frame"], - device_input=self.device_input, - repeat_count=data["repeat_count"], - ) - else: - frame_bytes = self.contour2predictions( - data["frameNum"], - data["mask"], - data["full_frame"], - device_input=self.device_input, - repeat_count=data["repeat_count"], - ) - self.latest_processed_frame = frame_bytes - self.last_heartbeat = time.time() - self.last_frame_id += 1 + # data["mask"].download(self.stream, self.pinned_downloaded_frame_np) + + # Run contour-based YOLO logic and draw overlays + frame_bytes = self.contour2predictions( + frameNum, + self.pinned_downloaded_frame_np + if self.device_input == "cuda" + else data["mask"], + data["full_frame"], + device_input=self.device_input, + repeat_count=data["repeat_count"], + return_bytes=return_bytes, + ) + + # Thread-safe update of the latest frame for FastAPI StreamingResponse + if return_bytes and frameNum > self.last_delivered_frame_id: + if frame_bytes: + self.latest_processed_frame = frame_bytes + self.last_delivered_frame_id = frameNum + self.last_frame_id = frameNum + self.last_heartbeat = time.time() + # Signal the FastAPI generator that a new frame is ready + self.loop.call_soon_threadsafe(self.frame_ready_event.set) + + # Offload JPEG encoding to a background worker to release the GIL + if not return_bytes: + # Rotate between buffers so the encoder has a 'locked' memory space + self.buf_idx = (self.buf_idx + 1) % 2 + target_buf = self.encode_buffers[self.buf_idx] + + # Perform a deep memory copy into the isolated buffer + # np.copyto(target_buf, data["full_frame"]) + # np.copyto(target_buf, data["full_frame"], casting='unsafe') + # target_buf[:] = data["full_frame"] + target_buf[:] = np.ascontiguousarray(data["full_frame"]) + + # Submit to background worker to bypass the GIL + self.executor.submit(self._encode_and_signal, target_buf, frameNum) + except Exception: - e = traceback.format_exc() - print(f"Async YOLO Error: {e}") + logging.error(f"Async YOLO Error in {self.name}: {traceback.format_exc()}") + + def _encode_and_signal(self, pixels, frame_num): + """Worker task for JPEG encoding to bypass GIL during stream delivery.""" + if not pixels.any(): + logging.warning( + f"⚠️ [DEBUG] {self.name} | Frame {frame_num}: Buffer is ALL BLACK (0.0). Check memory copy." + ) + + # Downscale for display FIRST to make encoding faster + # display_frame = cv2.resize(pixels, DISPLAY_FRAME_SIZE, interpolation=cv2.INTER_NEAREST) + if DEVICE == "GPU": + # 🚀 GPU OPTIMIZED PATH: Avoids CPU-RAM bus saturation + self.gpu_encoder_8k_buf.upload(pixels, stream=self.encode_stream) + + # Downscale for dashboard BEFORE downloading to CPU + cv2.cuda.resize( + self.gpu_encoder_8k_buf, + DISPLAY_FRAME_SIZE, + stream=self.encode_stream, + dst=self.gpu_display_frame, + ) + + # Ensure resize is complete before CPU download + self.encode_stream.waitForCompletion() + display_frame = self.gpu_display_frame.download(self.encode_stream) + else: + # CPU Fallback + display_frame = cv2.resize( + pixels, DISPLAY_FRAME_SIZE, interpolation=cv2.INTER_NEAREST + ) + + # Standard JPEG compression + success, buffer = cv2.imencode( + ".jpg", display_frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] + ) + if not success: + logging.error( + f"❌ [DEBUG] {self.name} | Frame {frame_num}: JPEG Encoding Failed." + ) + return + + # Update state and signal FastAPI + self.latest_processed_frame = buffer.tobytes() + self.last_delivered_frame_id = frame_num + self.last_frame_id = frame_num + self.last_heartbeat = time.time() + + # Thread-safe event set for the FastAPI loop + self.loop.call_soon_threadsafe(self.frame_ready_event.set) + + def update_ui_fallback(self, frame, frame_num): + # FALLBACK: If AI is busy, worker thread encodes raw frame for the UI. + # This offloads the 40ms CPU cost from the main Producer loop. + display_frame = cv2.resize( + frame, (self.disp_w, self.disp_h), interpolation=cv2.INTER_LINEAR + ) + _, buffer = cv2.imencode( + ".jpg", display_frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] + ) + + # print(f"DEFAULT DISP frameNum/last_frame_id {frame_num} self.last_delivered_frame_id: {self.last_delivered_frame_id}", flush=True) #\n\tframe_bytes: {frame_bytes}", flush=True) + self.latest_processed_frame = buffer.tobytes() + self.last_delivered_frame_id = frame_num + self.last_frame_id = frame_num + self.last_heartbeat = time.time() + # Signal the FastAPI generator that a new frame is ready + self.loop.call_soon_threadsafe(self.frame_ready_event.set) def process_frame_async(self, frame, frame_num, repeat_count=1): - """ - Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) - in the background without blocking the video reader. - """ + """Worker task: Handles BGS, YOLO, or Raw Fallback.""" try: - # Calls your existing Page 22 logic (run_pipeline) - # inf_data = self.run_pipeline(frame, frame_num + 1) - if self.device_input == "cpu": - inf_data = self.test_full_cpu_detection_gpu( - frame, frame_num + 1, repeat_count=repeat_count - ) - else: - inf_data = self.test_rbtdc_detection_gpu_optimized3( - frame, frame_num + 1, repeat_count=repeat_count - ) + # Check if we should run AI or just provide a raw preview to keep 15 FPS + # Use a backlog of 2 to allow for slight GPU jitter + if self.get_executor_backlog() < 5: # 2: + if self.device_input == "cuda": + inf_data = self.test_rbtdc_detection_gpu_optimized3( + frame, frame_num, repeat_count=repeat_count + ) + else: + inf_data = self.test_full_cpu_detection_gpu( + frame, frame_num, repeat_count=repeat_count + ) - if inf_data: - # Calls your Page 20 async_yolo_task to handle mask download/inference - self.async_yolo_task(inf_data) + if inf_data: + self.async_yolo_task(inf_data) + return # Exit after successful AI update + + # else: + # # FAST FALLBACK PATH: Update UI with raw frame to keep 15 FPS + # self.update_ui_fallback(frame, frame_num) except Exception: - e = traceback.format_exc() - print(f"ERROR: process_frame_async failed for {self.name}: {e}") + logging.error(f"Pipeline failure for {self.name}: {traceback.format_exc()}") def run_realtime_inference(self): """ - Main loop: Initializes the model in this thread to fix CUDA context issues. + Main producer loop. Fetches frames from the reader and + dispatches them to the AI pipeline. """ - print(f"Inference thread started for {self.name}...") - - # --- CRITICAL: Initialize model INSIDE the thread --- - # This binds the GPU context to this thread specifically. - # import torch - # self.model = YOLO(model_path, verbose=False, task="detect") - # self.model.to('cuda') # Explicitly move to GPU in this thread - - target_interval = 1.0 / self.target_fps - last_process_time = time.time() - while self.active: - # 1. REAL-TIME SYNC: Clear stale frames from buffer - # while True: - grabbed = self.cap.grab() - if not grabbed: - self.active = False - break + # Get frame from the reader's deque + frame = self.reader.read() - now = time.time() - if now - last_process_time < target_interval: - continue + if frame is not None: + self.frame_count += 1 - success, frame = self.cap.retrieve() - if not success or frame is None: - continue + # Re-allocate only if needed, otherwise pass pointer + with self.buffer_lock: + self.frame_buffer.append(frame.copy()) - last_process_time = now + # Offload to AI or Fallback + self.executor.submit(self.process_frame_async, frame, self.frame_count) + self.update_frame() + self.last_heartbeat = time.time() - # 3. DECOUPLED AI: Only submit to AI if the worker queue is not backed up - # This prevents 'lag' if the AI is slower than the video feed - if self.get_executor_backlog() < MAX_WORKERS: - # Move the heavy 'run_pipeline' call into a background worker - self.executor.submit( - self.process_frame_async, frame.copy(), self.stat_frame_count - ) + # Only submit to AI if the executor is not overwhelmed. + # 8K frames are ~100MB each; a backlog of 5 = 500MB RAM usage. + # if self.get_executor_backlog() < 5: + # self.executor.submit( + # self.process_frame_async, + # frame, + # self.frame_count + # ) + # else: + # # 🏎️ FALLBACK: AI is busy. Push raw frame to UI immediately. + # # This ensures the 'Live' view stays at 15 FPS. + # self.update_ui_fallback(frame, self.frame_count) + + # self.update_frame() else: - # If AI is busy, still update the display with the raw frame - # so the dashboard video stays smooth and fluid - _, buffer = cv2.imencode( - ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] - ) - self.latest_processed_frame = buffer.tobytes() - self.last_frame_id += 1 # Ensure the generator sees this 'clean' frame + # Prevent CPU pinning if camera is slow + time.sleep(0.001) + self.stop() + + def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): + """ + GPU-Accelerated Motion Detection Pipeline (Producer). - self.update_frame() - self.last_heartbeat = time.time() + This function performs high-speed background subtraction (BGS) on a downscaled + version of the 8K frame to identify regions of interest (ROIs). - self.stop() - # Add this line to remove it from the dashboard immediately: - if self.name in self.active_streams: # noqa: F821 - del self.active_streams[self.name] # noqa: F821 + Args: + frame (np.ndarray): The raw 8K input frame. + frameNum (float): Chronological timestamp or ID. - def test_rbtd_detection_gpu(self, frame, frameNum, repeat_count=1): + Returns: + dict: Contains the frame ID, the GPU-resident motion mask, and the original 8K frame. + """ + stream = self.stream # Resize directly into the pre-allocated Pinned Memory # This avoids a temporary CPU allocation - H, W = self.resize_h, self.resize_w + # H, W = self.resize_h, self.resize_w # self.cpu_resized_frame = cv2.resize(frame, (W, H)) # self.video_writer.write(self.cpu_resized_frame) - self.gpu_fullres_frame.upload(frame, self.stream) + + # Upload 8K frame to GPU memory + self.gpu_fullres_frame.upload(frame, stream=stream) + + # Downscale to MODEL_W/H (e.g., 640x640) for fast BGS analysis cv2.cuda.resize( self.gpu_fullres_frame, - (W, H), - stream=self.stream, - dst=self.resized_frame, + (self.resize_w, self.resize_h), + stream=stream, + dst=self.resized_frame, interpolation=cv2.INTER_NEAREST, ) + if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): - self.pinned_downloaded_resizedframe_np = self.resized_frame.download( - self.stream - ) + self.pinned_downloaded_resizedframe_np = self.resized_frame.download(stream) # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) for _ in range(repeat_count): # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) self.video_writer.write(self.pinned_downloaded_resizedframe_np) - # Background Subtraction on GPU - self.fgMask = self.backSub.apply( - self.resized_frame, float(self.lr), stream=self.stream + # Apply Background Subtraction on GPU + self.apply_background_subtraction_gpu( + include_history=True, method="and", stream=stream ) - for m in list(self.mask_history): - # Dilate the historical mask on GPU - dilated = self.dilate_filter_for_enhanced_mask.apply(m) - # Bitwise AND on GPU - cv2.cuda.bitwise_and(self.prev_bkgd, dilated, self.prev_bkgd) - # dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) - # cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) - self.mask_history.append(self.fgMask.clone()) - min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) - - if max_val != min_val: - self.fgMask = cv2.cuda.bitwise_or(self.fgMask, self.prev_bkgd) - - # Thresholding + # Clean up the motion mask using Thresholding and Morphology (Dilation) cv2.cuda.threshold( self.fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY, - self.gpu_threshold_dst_frame, - self.stream, + dst=self.gpu_threshold_dst_frame, + stream=stream, ) - - # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) self.dilate_filter.apply( - self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream + self.gpu_threshold_dst_frame, dst=self.gpu_morphed_frame, stream=stream ) return { "frameNum": frameNum, - "mask": self.gpu_morphed_frame, - "full_frame": frame, # Original for cropping + "mask": self.gpu_morphed_frame, # GpuMat pointer to cleaned mask + "full_frame": frame, # Kept for high-res cropping "repeat_count": repeat_count, } - def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation - # H, W = self.resize_h, self.resize_w - # self.cpu_resized_frame = cv2.resize(frame, (W, H)) - # self.video_writer.write(self.cpu_resized_frame) - self.gpu_fullres_frame.upload(frame, self.stream) - - cv2.cuda.resize( - self.gpu_fullres_frame, - (self.resize_w, self.resize_h), - stream=self.stream, - dst=self.resized_frame, - interpolation=cv2.INTER_NEAREST, - ) - if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): - self.pinned_downloaded_resizedframe_np = self.resized_frame.download( - self.stream - ) - # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) - for _ in range(repeat_count): - # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) - self.video_writer.write(self.pinned_downloaded_resizedframe_np) - - # Background Subtraction on GPU - self.apply_background_subtraction_gpu(include_history=True, method="and") - - # Thresholding - cv2.cuda.threshold( - self.fgMask, - MASK_THRESHOLD_VALUE, - MASK_MAX_VALUE, - cv2.THRESH_BINARY, - self.gpu_threshold_dst_frame, - self.stream, - ) + def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): + """ + CPU-Based Motion Detection Pipeline (Producer). - # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - self.dilate_filter.apply( - self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream - ) + Performs background subtraction on the CPU to identify moving objects. + Ideal for saving VRAM or for environments without high-end NVIDIA GPUs. - return { - "frameNum": frameNum, - "mask": self.gpu_morphed_frame, - "full_frame": frame, # Original for cropping - "repeat_count": repeat_count, - } + Args: + frame (np.ndarray): The raw 8K input frame. + frameNum (float): Unique ID for the current frame. - def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation + Returns: + dict: Motion data containing the frame ID, CPU-based mask, and original 8K frame. + """ + # Resize the 8K frame to a smaller 'model' size (e.g., 640x640) + # Using INTER_NEAREST as it is the fastest CPU interpolation method. H, W = self.resize_h, self.resize_w self.cpu_resized_frame = cv2.resize( frame, (W, H), interpolation=cv2.INTER_NEAREST @@ -941,39 +1353,44 @@ def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): for _ in range(repeat_count): self.video_writer.write(self.cpu_resized_frame) - # Background Subtraction on CPU - fgMask = self.backSub.apply(self.cpu_resized_frame, learningRate=self.lr) - - prev_bkgd = np.ones_like(fgMask) # AND - for m in self.mask_history: - # Dilate the historical mask - dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) - cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) - self.mask_history.append(fgMask) - - if prev_bkgd.max() != prev_bkgd.min(): - combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) - - # Convert the boolean array back to uint8 with 0 and 255 values - fgMask = combined_mask_bool.astype(np.uint8) * 255 + # Apply Background Subtraction on CPU + self.apply_background_subtraction_cpu(include_history=True, method="and") - # Thresholding + # Clean up the motion mask using Thresholding and Morphology (Dilation) _, mask = cv2.threshold( - fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY + self.fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY ) - mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) return { "frameNum": frameNum, "mask": mask, - "full_frame": frame, # Original for cropping + "full_frame": frame, # Kept for high-res cropping "repeat_count": repeat_count, } def get_detections_for_contours_bbs( - self, frameNum, foi, contours, thickness=2, device_input="cuda" + self, + frameNum, + foi, + contours, + thickness=2, + device_input="cuda", + return_bytes=True, ): + """ + Motion-Triggered YOLO Inference Logic. + + Instead of running YOLO on a massive 8K frame, this function: + 1. Extracts bounding boxes from the motion contours. + 2. Merges nearby boxes into optimal 640x640 crops. + 3. Runs a single batch inference on those crops. + 4. Maps detection coordinates back to the original 8K space. + + Args: + foi (np.ndarray): 'Frame of Interest' (the 8K raw frame). + contours (list): Contours extracted from the motion mask. + """ # global active_streams # source = self.source stream_name = self.name @@ -985,34 +1402,45 @@ def get_detections_for_contours_bbs( H, W = foi.shape[:2] # Unpack once bbs_full_res = [] - # Filter and Sort in one go (Minimize Python-to-C++ crossings) + if not contours: + # if return_bytes: + frame_bytes = get_display_frame_in_bytes( + foi, + display_size=DISPLAY_FRAME_SIZE, + quality=DISPLAY_FRAME_QUALITY, + return_bytes=return_bytes, + ) + return metadata, frame_bytes # num_objs, predictions + + # Filter small noise and convert contours to 8K-space bounding boxes raw_bbs = [] padding = 64 for c in contours: area = cv2.contourArea(c) - x1, y1, w, h = cv2.boundingRect(c) - if ( - area > self.min_contour_area - ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect + if area > self.min_contour_area: + x1, y1, w, h = cv2.boundingRect(c) + + # Scale coordinates from 640p BGS-space to 8K-space xx1 = max(0, int((x1 * self.scale_x)) - padding) yy1 = max(0, int((y1 * self.scale_y)) - padding) xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) + + # Merge overlapping boxes into batches (Capped at 64 for TensorRT stability) bbs_full_res = sorted( - [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], + [pair[1] for pair in raw_bbs], key=lambda x: x[0], reverse=True, - )[:MAX_DETECTIONS] + ) # [:MAX_DETECTIONS] dist_thresh = min(0.05 * W, 0.05 * H) merged = merge_boxes_limit( - bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + bbs_full_res, dist_threshold=dist_thresh, size_limit=MODEL_W ) - merged = filter_contained_boxes(merged, containment_thresh=0.9) - # for cnt, area in merged: + # Extract crops at full-resolution for x1, y1, x2, y2 in merged: if ( x2 > x1 @@ -1021,36 +1449,52 @@ def get_detections_for_contours_bbs( and (y2 - y1) < self.frame_height ): crop = foi[y1:y2, x1:x2] - if crop.size > 0: + if crop.size > 0 and crop.shape[0] > 31 and crop.shape[1] > 31: cropped_imgs.append(crop) cropped_coords.append((x1, y1)) + if len(cropped_imgs) == MODEL_MAX_BATCH_SIZE: + # logging.warning( + # f"⚠️ [LIMIT] {self.name} found {len(cropped_imgs)} contours. Capping to 64 for TensorRT." + # ) + # cropped_imgs = cropped_imgs[:MODEL_MAX_BATCH_SIZE] + # cropped_coords = cropped_coords[:MODEL_MAX_BATCH_SIZE] + break + if not cropped_imgs: frame_bytes = get_display_frame_in_bytes( foi, - self.frame_width, display_size=DISPLAY_FRAME_SIZE, quality=DISPLAY_FRAME_QUALITY, + return_bytes=return_bytes, ) return metadata, frame_bytes # num_objs, predictions - # 2. Inference (Keep stream=False as it is stable) - results = self.model.predict( - cropped_imgs, - imgsz=MODEL_W, - batch=len(cropped_imgs), - device=device_input, - verbose=False, - stream=True, - max_det=MAX_DETECTIONS, - # classes=[0], # only "person", - # conf=0.45, - ) - - label_source = ( - self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES - ) + # if len(cropped_imgs) > MODEL_MAX_BATCH_SIZE: + # logging.warning( + # f"⚠️ [LIMIT] {self.name} found {len(cropped_imgs)} contours. Capping to 64 for TensorRT." + # ) + # cropped_imgs = cropped_imgs[:MODEL_MAX_BATCH_SIZE] + # cropped_coords = cropped_coords[:MODEL_MAX_BATCH_SIZE] + + # Run Inference (Keep stream=False as it is stable) + with model_lock: # Use the global lock + results = self.model.predict( + cropped_imgs, + imgsz=MODEL_W, + batch=len(cropped_imgs), + device=device_input, + verbose=False, + stream=True, + max_det=MAX_DETECTIONS, + # classes=[0], # only "person", + # conf=0.45, + ) + # Convert generator to list while still inside the lock + # to ensure results aren't overwritten by another thread. + results = list(results) + # Process results and draw 8K-space overlays for ridx, r in enumerate(results): if r.boxes is None or len(r.boxes) == 0: continue @@ -1067,7 +1511,7 @@ def get_detections_for_contours_bbs( abs_x1, abs_y1 = off_x + bx1, off_y + by1 abs_x2, abs_y2 = off_x + bx2, off_y + by2 class_id = clss[j] - class_name = label_source[class_id] + class_name = self.label_source[class_id] confidence = confs[j] if confidence > DETECTION_THRESHOLD: if not OMIT_DETECTIONS_FLAG: @@ -1142,1026 +1586,62 @@ def get_detections_for_contours_bbs( }, } - # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) + # Queue frame for display (reduce quality for 8K bandwidth) frame_bytes = get_display_frame_in_bytes( foi, - self.frame_width, display_size=DISPLAY_FRAME_SIZE, quality=DISPLAY_FRAME_QUALITY, + return_bytes=return_bytes, ) return metadata, frame_bytes def contour2predictions( - self, frameNum, mask, frame, device_input="cpu", repeat_count=1 + self, + frameNum, + mask, + frame, + device_input="cpu", + repeat_count=1, + return_bytes=True, ): - # source = self.source - # stream_name = self.name + """ + The 'Glue' function that connects Motion Detection to AI Inference. + + Args: + mask (GpuMat/np.ndarray): The motion mask (from BGS). + frame (np.ndarray): The original 8K frame. + """ + # Extract contours from the motion mask contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - # 3. Write frame + # Write frame # self.video_writer.write(frame) # if self.video_writer: # for _ in range(repeat_count): # self.video_writer.write(self.cpu_resized_frame) - # num_objs = 0 - # predictions = [] + # Pass contours to the YOLO detection logic metadata = dict() - if contours: - metadata, frame_bytes = self.get_detections_for_contours_bbs( - frameNum, frame, contours, thickness=2, device_input=device_input - ) - - if metadata: - all_metadata.setdefault( - self.clip_key, - { - "object": {}, - "face": {}, - }, - ) - all_metadata[self.clip_key]["object"].update(metadata) - # all_metadata[clip_key]["face"].update(metadata_face) - return frame_bytes - - -class VideoStreamHandler2(BaseHandler): - def __init__(self, source, name, active_streams): - super().__init__(source, name, active_streams) - self.cap.release() - - def setup_threads(self): - self.torch_stream = torch.cuda.ExternalStream(self.stream.cudaPtr()) - self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) - self.reader = FastPinnedReader( - self.source, self.frame_height, self.frame_width, maxlen=2 - ) # .start() - self.process_thread = threading.Thread( - target=self.run_realtime_inference, daemon=True - ) - # self.process_thread.start() - - def start(self): - self.reader.start() - self.process_thread.start() - - def stop(self): - self.active = False # Signals the while loops to exit - self.reader.stop() - # self.process_thread.join() - - # Close the OpenCV capture - if self.cap: - self.cap.release() - - def run_realtime_inference(self): - print(f"Inference thread started for {self.name}...") - - # --- CRITICAL: Initialize model INSIDE the thread --- - # This binds the GPU context to this thread specifically. - # import torch - # self.model = YOLO(model_path, verbose=False, task="detect") - # self.model.to('cuda') # Explicitly move to GPU in this thread - - target_interval = 1.0 / self.target_fps - last_process_time = time.time() - - while self.active: # and (not self.reader.stopped or self.reader.frame_queue): - # 1. REAL-TIME SYNC: Clear stale frames from buffer - # while True: - # grabbed = self.cap.grab() - # if not grabbed: - # self.active = False - # break - - if self.device_input == "cuda": - now = time.time() - # if now - last_process_time < target_interval: - # continue - frame, f_idx = self.reader.read() - if frame is None: - # time.sleep(0.001) # Wait for reader thread - continue - last_process_time = now - else: - grabbed = self.cap.grab() - if not grabbed: - self.active = False - - now = time.time() - if now - last_process_time < target_interval: - continue - - success, frame = self.cap.retrieve() - if not success or frame is None: - continue - - last_process_time = now - - # 3. DECOUPLED AI: Only submit to AI if the worker queue is not backed up - # This prevents 'lag' if the AI is slower than the video feed - if self.get_executor_backlog() < MAX_WORKERS: - # Move the heavy 'run_pipeline' call into a background worker - self.executor.submit( - self.process_frame_async, frame.copy(), self.stat_frame_count + 1 - ) - else: - # If AI is busy, still update the display with the raw frame - # so the dashboard video stays smooth and fluid - _, buffer = cv2.imencode( - ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] - ) - self.latest_processed_frame = buffer.tobytes() - self.last_frame_id += 1 # Ensure the generator sees this 'clean' frame - - self.update_frame() - self.last_heartbeat = time.time() - - # 2. SKIP frames (Light Bitstream Parsing) - # This is the "Compute Saver" - it bypasses the decoder for N frames - for _ in range(self.skip_count): - if self.reader.read()[0] is None: - self.reader.stopped = True - break - - self.stop() - # Add this line to remove it from the dashboard immediately: - if self.name in self.active_streams: # noqa: F821 - del self.active_streams[self.name] # noqa: F821 - - def process_frame_async(self, frame, frame_num, repeat_count=1): - """ - Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) - in the background without blocking the video reader. - """ - try: - repeat_count = 1 - # Calls your existing Page 22 logic (run_pipeline) - # inf_data = self.run_pipeline(frame, frame_num + 1) - if self.device_input == "cpu": - raise ValueError("CPU pipeline not added") - # inf_data = self.test_full_cpu_detection_gpu( - # frame, frame_num + 1, repeat_count=repeat_count - # ) - else: - # inf_data = self.test_rbtdc_detection_gpu_optimized3(frame, frame_num + 1, repeat_count=repeat_count) - # print(f"inf_data: {inf_data}") - self.test_rbtdc_detection_gpu_optimized3( - frame, frame_num + 1, repeat_count=repeat_count - ) - # if inf_data: - # # Calls your Page 20 async_yolo_task to handle mask download/inference - # self.async_yolo_task(inf_data) - - except Exception: - e = traceback.format_exc() - print(f"ERROR: process_frame_async failed for {self.name}: {e}") - - def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation - # H, W = self.resize_h, self.resize_w - # self.cpu_resized_frame = cv2.resize(frame, (W, H)) - # self.video_writer.write(self.cpu_resized_frame) - self.gpu_fullres_frame.upload(frame, self.stream) - - cv2.cuda.resize( - self.gpu_fullres_frame, - (self.resize_w, self.resize_h), - stream=self.stream, - dst=self.resized_frame, - interpolation=cv2.INTER_NEAREST, - ) - if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): - self.pinned_downloaded_resizedframe_np = self.resized_frame.download( - self.stream - ) - # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) - for _ in range(repeat_count): - # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) - self.video_writer.write(self.pinned_downloaded_resizedframe_np) - - # Background Subtraction on GPU - self.apply_background_subtraction_gpu(include_history=True, method="and") - - # Thresholding - cv2.cuda.threshold( - self.fgMask, - MASK_THRESHOLD_VALUE, - MASK_MAX_VALUE, - cv2.THRESH_BINARY, - self.gpu_threshold_dst_frame, - self.stream, - ) - - # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - self.dilate_filter.apply( - self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream - ) - - # return { - # "frameNum": frameNum, - # "mask": self.gpu_morphed_frame, - # "full_frame": frame, # Original for cropping - # "repeat_count": repeat_count, - # } - cropped_imgs, cropped_coords = [], [] - H, W = frame.shape[:2] # Unpack once - bbs_full_res = self.get_sorted_contours_gpu(self.gpu_morphed_frame, H, W) - # if not bbs_full_res: - # return num_objs - dist_thresh = min(0.05 * W, 0.05 * H) - merged = merge_boxes_limit( - bbs_full_res, dist_threshold=dist_thresh, size_limit=640 + metadata, frame_bytes = self.get_detections_for_contours_bbs( + frameNum, + frame, + contours, + thickness=2, + device_input=device_input, + return_bytes=return_bytes, ) - merged = filter_contained_boxes(merged, containment_thresh=0.9) - for x1, y1, x2, y2 in merged: - if ( - x2 > x1 - and y2 > y1 - and (x2 - x1) < self.frame_width - and (y2 - y1) < self.frame_height - ): - crop = frame[y1:y2, x1:x2] - if crop.size > 0: - cropped_imgs.append(crop) - cropped_coords.append((x1, y1)) - - if cropped_imgs: - with torch.cuda.stream(self.torch_stream): - results = self.run_model( - cropped_imgs, batch=len(cropped_imgs), stream=True - ) - if results: - # for r in results: # Consume generator [0.1.38, Line 2431] - # total_objs += len(r.boxes) - metadata, frame_bytes = self.extract_metadata_from_results( - results, - frame, - cropped_coords, - frameNum, - self.frame_height, - self.frame_width, - ) - else: - frame_bytes = get_display_frame_in_bytes( - frame, - self.frame_width, - display_size=DISPLAY_FRAME_SIZE, - quality=DISPLAY_FRAME_QUALITY, + # Update global metadata for storage (Database/JSON) + if metadata: + all_metadata.setdefault( + self.clip_key, + { + "object": {}, + "face": {}, + }, ) + all_metadata[self.clip_key]["object"].update(metadata) - self.latest_processed_frame = frame_bytes - self.last_heartbeat = time.time() - self.last_frame_id += 1 - - def async_yolo_task(self, data): - """Heavy lifting moved to ThreadPoolExecutor""" - try: - if self.device_input == "cuda": - frameNum = data["frameNum"] - gpu_morphed_frame = data["mask"] - frame = data["full_frame"] - # self.pinned_downloaded_frame_np = data["mask"].download(self.stream) - # frame_bytes = self.contour2predictions( - # data["frameNum"], - # self.pinned_downloaded_frame_np, - # data["full_frame"], - # device_input=self.device_input, - # repeat_count=data["repeat_count"], - # ) - cropped_imgs, cropped_coords = [], [] - H, W = frame.shape[:2] # Unpack once - bbs_full_res = self.get_sorted_contours_gpu(gpu_morphed_frame, H, W) - # if not bbs_full_res: - # return num_objs - dist_thresh = min(0.05 * W, 0.05 * H) - merged = merge_boxes_limit( - bbs_full_res, dist_threshold=dist_thresh, size_limit=640 - ) - - merged = filter_contained_boxes(merged, containment_thresh=0.9) - for x1, y1, x2, y2 in merged: - if ( - x2 > x1 - and y2 > y1 - and (x2 - x1) < self.frame_width - and (y2 - y1) < self.frame_height - ): - crop = frame[y1:y2, x1:x2] - if crop.size > 0: - cropped_imgs.append(crop) - cropped_coords.append((x1, y1)) - - if cropped_imgs: - with torch.cuda.stream(self.torch_stream): - results = self.run_model( - cropped_imgs, batch=len(cropped_imgs), stream=True - ) - if results: - # for r in results: # Consume generator [0.1.38, Line 2431] - # total_objs += len(r.boxes) - metadata, frame_bytes = self.extract_metadata_from_results( - results, - frame, - cropped_coords, - frameNum, - self.frame_height, - self.frame_width, - ) - else: - frame_bytes = get_display_frame_in_bytes( - frame, - self.frame_width, - display_size=DISPLAY_FRAME_SIZE, - quality=DISPLAY_FRAME_QUALITY, - ) - else: - frame_bytes = self.contour2predictions( - data["frameNum"], - data["mask"], - data["full_frame"], - device_input=self.device_input, - repeat_count=data["repeat_count"], - ) - self.latest_processed_frame = frame_bytes - self.last_heartbeat = time.time() - self.last_frame_id += 1 - except Exception: - e = traceback.format_exc() - print(f"Async YOLO Error: {e}") - - def extract_metadata_from_results( - self, results, foi, cropped_coords, frameNum, H, W, thickness=2 - ): - num_objs = 0 - # predictions = [] - metadata = dict() - label_source = ( - self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES - ) - - for ridx, r in enumerate(results): - if r.boxes is None or len(r.boxes) == 0: - continue - - # Move to CPU in one bulk operation per crop - boxes = r.boxes.xyxy.cpu().numpy().astype(int) - clss = r.boxes.cls.cpu().numpy().astype(int) - confs = r.boxes.conf.cpu().numpy() - off_x, off_y = cropped_coords[ridx][:2] - - for j in range(len(boxes)): - num_objs += 1 - bx1, by1, bx2, by2 = boxes[j] - abs_x1, abs_y1 = off_x + bx1, off_y + by1 - abs_x2, abs_y2 = off_x + bx2, off_y + by2 - class_id = clss[j] - class_name = label_source[class_id] - confidence = confs[j] - if confidence > DETECTION_THRESHOLD: - bb_color = get_detection_color(class_id, is_bgr=True) - - foi = cv2.rectangle( - foi, - (abs_x1, abs_y1), - (abs_x2, abs_y2), - bb_color, - thickness, - ) - label = f"{class_name} {confidence:.2f}" - draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) - - height = min(abs_y2, H) - max(0, abs_y1) - width = min(abs_x2, W) - max(0, abs_x1) - # object_res = [ - # abs_x1, - # abs_y1, - # height, - # width, - # class_name, - # confidence, - # H, - # W, - # ] - - # Resized - scale_x = self.resize_w / W - scale_y = self.resize_h / H - object_res = [ - int(abs_x1 * scale_x), - int(abs_y1 * scale_y), - int(height * scale_y), - int(width * scale_x), - class_name, - confidence, - int(self.resize_h), - int(self.resize_w), - ] - - framenum_str = f"{frameNum:04d}_{j:04d}" - - # Full Res - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": { - "x": int(object_res[0]), - "y": int(object_res[1]), - "height": int(object_res[2]), - "width": int(object_res[3]), - "object": str(object_res[4]), - "object_det": { - "confidence": float(object_res[5]), - "frameH": int(object_res[6]), - "frameW": int(object_res[7]), - }, - }, - } - - # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) - frame_bytes = get_display_frame_in_bytes( - foi, - self.frame_width, - display_size=DISPLAY_FRAME_SIZE, - quality=DISPLAY_FRAME_QUALITY, - ) - - return metadata, frame_bytes - - def get_sorted_contours(self, morphed_frame, H, W): - contours, _ = cv2.findContours( - morphed_frame, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE - ) - raw_bbs = [] - padding = 64 - for c in contours: - area = cv2.contourArea(c) - x1, y1, w, h = cv2.boundingRect(c) - if ( - area > self.min_contour_area - ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect - xx1 = max(0, int((x1 * self.scale_x)) - padding) - yy1 = max(0, int((y1 * self.scale_y)) - padding) - xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) - yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) - raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) - bbs_full_res = sorted( - [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], - key=lambda x: x[0], - reverse=True, - )[:MAX_DETECTIONS] - return bbs_full_res - - def get_sorted_contours_gpu(self, morphed_frame, H, W): - # 1. Zero-Copy Bridge - gpu_frame_cp = gpumat2cupy(morphed_frame) - - # 2. Fast Labeling (Still required for connectivity) - label_image, num_labels = cucim_label(gpu_frame_cp, return_num=True) - if num_labels == 0: - return [] - - # cupy.bincount counts occurrences of each label (index 0 is background) - areas = cupy.bincount(label_image.ravel()) - mask = areas > self.min_contour_area - mask[0] = False - if not cupy.any(mask): - return [] - - # 3. Pre-allocate BBox buffer on GPU - # Format: [num_labels, 4] -> [min_y, min_x, max_y, max_x] - # Initialize with extreme values for min/max logic - bboxes_gpu = cupy.full((num_labels + 1, 4), -1, dtype=cupy.int32) - bboxes_gpu[:, :2] = 99999 # Initial min values - - # Pass the width (morphed_frame.size()[0]) to the kernel - mask_w = morphed_frame.size()[0] - - # 4. Run the Fast BBox Kernel - bbox_kernel(label_image, mask_w, bboxes_gpu) - - # 5. Filter by Area & Constraints on GPU - # (Optional: Use cupy.bincount to get areas if needed for area filtering) - # Move ONLY valid bboxes to CPU in one bulk operation - valid_bboxes = bboxes_gpu[mask].get() - - # 6. Final Coordinate Scaling - padding = 64 - bbs_full_res = [] - for y1, x1, y2, x2 in valid_bboxes[:MAX_DETECTIONS]: - xx1 = max(0, int(x1 * self.scale_x) - padding) - yy1 = max(0, int(y1 * self.scale_y) - padding) - xx2 = min(W, int(x2 * self.scale_x) + padding) - yy2 = min(H, int(y2 * self.scale_y) + padding) - bbs_full_res.append([xx1, yy1, xx2, yy2]) - - return bbs_full_res - - -class VideoStreamHandler3(BaseHandler): - def setup_threads(self): - self.torch_stream = torch.cuda.ExternalStream(self.stream.cudaPtr()) - self.executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) - self.process_thread = threading.Thread( - target=self.run_realtime_inference, daemon=True - ) - - def async_yolo_task(self, data): - """Heavy lifting moved to ThreadPoolExecutor""" - try: - if self.device_input == "cuda": - self.pinned_downloaded_frame_np = data["mask"].download(self.stream) - frame_bytes = self.contour2predictions( - data["frameNum"], - self.pinned_downloaded_frame_np, - data["full_frame"], - device_input=self.device_input, - repeat_count=data["repeat_count"], - ) - else: - frame_bytes = self.contour2predictions( - data["frameNum"], - data["mask"], - data["full_frame"], - device_input=self.device_input, - repeat_count=data["repeat_count"], - ) - self.latest_processed_frame = frame_bytes - self.last_heartbeat = time.time() - self.last_frame_id += 1 - except Exception: - e = traceback.format_exc() - print(f"Async YOLO Error: {e}") - - def process_frame_async(self, frame, frame_num, repeat_count=1): - """ - Worker function to run heavy AI tasks (Resize, Bkgd Sub, YOLO) - in the background without blocking the video reader. - """ - try: - # Calls your existing Page 22 logic (run_pipeline) - # inf_data = self.run_pipeline(frame, frame_num + 1) - if self.device_input == "cpu": - inf_data = self.test_full_cpu_detection_gpu( - frame, frame_num + 1, repeat_count=repeat_count - ) - else: - inf_data = self.test_rbtd_detection_gpu( - frame, frame_num + 1, repeat_count=repeat_count - ) - - # if inf_data: - # # Calls your Page 20 async_yolo_task to handle mask download/inference - # self.async_yolo_task(inf_data) - if inf_data and "mask" in inf_data: - # Calls your Page 20 async_yolo_task to handle mask download/inference - self.async_yolo_task(inf_data) - else: - frame_bytes = get_display_frame_in_bytes( - frame, - self.frame_width, - display_size=DISPLAY_FRAME_SIZE, - quality=DISPLAY_FRAME_QUALITY, - ) - self.latest_processed_frame = frame_bytes - self.last_heartbeat = time.time() - self.last_frame_id += 1 - - except Exception: - e = traceback.format_exc() - print(f"ERROR: process_frame_async failed for {self.name}: {e}") - - def run_realtime_inference(self): - """ - Main loop: Initializes the model in this thread to fix CUDA context issues. - """ - print(f"Inference thread started for {self.name}...") - - # --- CRITICAL: Initialize model INSIDE the thread --- - # This binds the GPU context to this thread specifically. - # import torch - # self.model = YOLO(model_path, verbose=False, task="detect") - # self.model.to('cuda') # Explicitly move to GPU in this thread - - target_interval = 1.0 / self.target_fps - last_process_time = time.time() - - while self.active: - # 1. REAL-TIME SYNC: Clear stale frames from buffer - # while True: - grabbed = self.cap.grab() - if not grabbed: - self.active = False - break - - now = time.time() - if now - last_process_time < target_interval: - continue - - success, frame = self.cap.retrieve() - if not success or frame is None: - continue - - last_process_time = now - - # 3. DECOUPLED AI: Only submit to AI if the worker queue is not backed up - # This prevents 'lag' if the AI is slower than the video feed - if self.get_executor_backlog() < MAX_WORKERS: - # Move the heavy 'run_pipeline' call into a background worker - self.executor.submit( - self.process_frame_async, frame.copy(), self.stat_frame_count - ) - else: - # If AI is busy, still update the display with the raw frame - # so the dashboard video stays smooth and fluid - _, buffer = cv2.imencode( - ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, DISPLAY_FRAME_QUALITY] - ) - self.latest_processed_frame = buffer.tobytes() - self.last_frame_id += 1 # Ensure the generator sees this 'clean' frame - - self.update_frame() - self.last_heartbeat = time.time() - - self.stop() - # Add this line to remove it from the dashboard immediately: - if self.name in self.active_streams: # noqa: F821 - del self.active_streams[self.name] # noqa: F821 - - def test_rbtd_detection_gpu(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation - H, W = self.resize_h, self.resize_w - # self.cpu_resized_frame = cv2.resize(frame, (W, H)) - # self.video_writer.write(self.cpu_resized_frame) - self.gpu_fullres_frame.upload(frame, self.stream) - cv2.cuda.resize( - self.gpu_fullres_frame, - (W, H), - stream=self.stream, - dst=self.resized_frame, - interpolation=cv2.INTER_NEAREST, - ) - if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): - self.pinned_downloaded_resizedframe_np = self.resized_frame.download( - self.stream - ) - # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) - for _ in range(repeat_count): - # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) - self.video_writer.write(self.pinned_downloaded_resizedframe_np) - - # Background Subtraction on GPU - self.fgMask = self.backSub.apply( - self.resized_frame, float(self.lr), stream=self.stream - ) - - for m in list(self.mask_history): - # Dilate the historical mask on GPU - dilated = self.dilate_filter_for_enhanced_mask.apply(m) - # Bitwise AND on GPU - cv2.cuda.bitwise_and(self.prev_bkgd, dilated, self.prev_bkgd) - # dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) - # cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) - self.mask_history.append(self.fgMask.clone()) - min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) - - if max_val != min_val: - self.fgMask = cv2.cuda.bitwise_or(self.fgMask, self.prev_bkgd) - - # Thresholding - cv2.cuda.threshold( - self.fgMask, - MASK_THRESHOLD_VALUE, - MASK_MAX_VALUE, - cv2.THRESH_BINARY, - self.gpu_threshold_dst_frame, - self.stream, - ) - - # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - self.dilate_filter.apply( - self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream - ) - - return { - "frameNum": frameNum, - "mask": self.gpu_morphed_frame, - "full_frame": frame, # Original for cropping - "repeat_count": repeat_count, - } - - def test_rbtdc_detection_gpu_optimized3(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation - # H, W = self.resize_h, self.resize_w - # self.cpu_resized_frame = cv2.resize(frame, (W, H)) - # self.video_writer.write(self.cpu_resized_frame) - self.gpu_fullres_frame.upload(frame, self.stream) - - cv2.cuda.resize( - self.gpu_fullres_frame, - (self.resize_w, self.resize_h), - stream=self.stream, - dst=self.resized_frame, - interpolation=cv2.INTER_NEAREST, - ) - if ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): - self.pinned_downloaded_resizedframe_np = self.resized_frame.download( - self.stream - ) - # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) - for _ in range(repeat_count): - # self.video_queue.put((self.video_writer, self.pinned_downloaded_resizedframe_np.copy())) - self.video_writer.write(self.pinned_downloaded_resizedframe_np) - - # Background Subtraction on GPU - self.apply_background_subtraction_gpu(include_history=True, method="and") - - if frameNum - 1 % self.frame_skip: - return { - "frameNum": frameNum, - # "mask": self.gpu_morphed_frame, - "full_frame": frame, # Original for cropping - # "repeat_count": repeat_count, - } - - # Thresholding - cv2.cuda.threshold( - self.fgMask, - MASK_THRESHOLD_VALUE, - MASK_MAX_VALUE, - cv2.THRESH_BINARY, - self.gpu_threshold_dst_frame, - self.stream, - ) - - # mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - self.dilate_filter.apply( - self.gpu_threshold_dst_frame, self.gpu_morphed_frame, self.stream - ) - - return { - "frameNum": frameNum, - "mask": self.gpu_morphed_frame, - "full_frame": frame, # Original for cropping - "repeat_count": repeat_count, - } - - def test_full_cpu_detection_gpu(self, frame, frameNum, repeat_count=1): - # Resize directly into the pre-allocated Pinned Memory - # This avoids a temporary CPU allocation - H, W = self.resize_h, self.resize_w - self.cpu_resized_frame = cv2.resize( - frame, (W, H), interpolation=cv2.INTER_NEAREST - ) - if ENABLE_QUERYING: - for _ in range(repeat_count): - self.video_writer.write(self.cpu_resized_frame) - - # Background Subtraction on CPU - fgMask = self.backSub.apply(self.cpu_resized_frame, learningRate=self.lr) - - prev_bkgd = np.ones_like(fgMask) # AND - for m in self.mask_history: - # Dilate the historical mask - dilated = cv2.dilate(m, self.dilate_kernel_for_enhanced_mask, iterations=1) - cv2.bitwise_and(prev_bkgd, dilated, dst=prev_bkgd) - self.mask_history.append(fgMask) - - if prev_bkgd.max() != prev_bkgd.min(): - combined_mask_bool = (fgMask > 0) | (prev_bkgd > 0) - - # Convert the boolean array back to uint8 with 0 and 255 values - fgMask = combined_mask_bool.astype(np.uint8) * 255 - - # Thresholding - _, mask = cv2.threshold( - fgMask, MASK_THRESHOLD_VALUE, MASK_MAX_VALUE, cv2.THRESH_BINARY - ) - - mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) - - return { - "frameNum": frameNum, - "mask": mask, - "full_frame": frame, # Original for cropping - "repeat_count": repeat_count, - } - - def get_detections_for_contours_bbs( - self, frameNum, foi, contours, thickness=2, device_input="cuda" - ): - # global active_streams - # source = self.source - stream_name = self.name - num_objs = 0 - # predictions = [] - metadata = dict() - # frame_bytes = 'b' - cropped_imgs, cropped_coords = [], [] - H, W = foi.shape[:2] # Unpack once - bbs_full_res = [] - - # Filter and Sort in one go (Minimize Python-to-C++ crossings) - raw_bbs = [] - padding = 64 - for c in contours: - area = cv2.contourArea(c) - x1, y1, w, h = cv2.boundingRect(c) - if ( - area > self.min_contour_area - ): # and area / (w*h) >=0.3: # and 0.5 < (w / h) < 2.0: # w/ solidity & aspect - xx1 = max(0, int((x1 * self.scale_x)) - padding) - yy1 = max(0, int((y1 * self.scale_y)) - padding) - xx2 = min(W, int(((x1 + w) * self.scale_x)) + padding) - yy2 = min(H, int(((y1 + h) * self.scale_y)) + padding) - raw_bbs.append([area, [xx1, yy1, xx2, yy2]]) - bbs_full_res = sorted( - [pair[1] for pair in raw_bbs if pair[0] > self.min_contour_area], - key=lambda x: x[0], - reverse=True, - )[:MAX_DETECTIONS] - - dist_thresh = min(0.05 * W, 0.05 * H) - merged = merge_boxes_limit( - bbs_full_res, dist_threshold=dist_thresh, size_limit=640 - ) - - merged = filter_contained_boxes(merged, containment_thresh=0.9) - - # for cnt, area in merged: - for x1, y1, x2, y2 in merged: - if ( - x2 > x1 - and y2 > y1 - and (x2 - x1) < self.frame_width - and (y2 - y1) < self.frame_height - ): - crop = foi[y1:y2, x1:x2] - if crop.size > 0: - cropped_imgs.append(crop) - cropped_coords.append((x1, y1)) - - if not cropped_imgs: - frame_bytes = get_display_frame_in_bytes( - foi, - self.frame_width, - display_size=DISPLAY_FRAME_SIZE, - quality=DISPLAY_FRAME_QUALITY, - ) - return metadata, frame_bytes # num_objs, predictions - - # 2. Inference (Keep stream=False as it is stable) - results = self.model.predict( - cropped_imgs, - imgsz=MODEL_W, - batch=len(cropped_imgs), - device=device_input, - verbose=False, - stream=True, - max_det=MAX_DETECTIONS, - # classes=[0], # only "person", - # conf=0.45, - ) - - label_source = ( - self.model.names if hasattr(self.model, "names") else YOLO_CLASS_NAMES - ) - - for ridx, r in enumerate(results): - if r.boxes is None or len(r.boxes) == 0: - continue - - # Move to CPU in one bulk operation per crop - boxes = r.boxes.xyxy.cpu().numpy().astype(int) - clss = r.boxes.cls.cpu().numpy().astype(int) - confs = r.boxes.conf.cpu().numpy() - off_x, off_y = cropped_coords[ridx] - - for j in range(len(boxes)): - num_objs += 1 - bx1, by1, bx2, by2 = boxes[j] - abs_x1, abs_y1 = off_x + bx1, off_y + by1 - abs_x2, abs_y2 = off_x + bx2, off_y + by2 - class_id = clss[j] - class_name = label_source[class_id] - confidence = confs[j] - if confidence > DETECTION_THRESHOLD: - if not OMIT_DETECTIONS_FLAG: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - print( - # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", - f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", - flush=True, - ) - - bb_color = get_detection_color(class_id, is_bgr=True) - - foi = cv2.rectangle( - foi, - (abs_x1, abs_y1), - (abs_x2, abs_y2), - bb_color, - thickness, - ) - label = f"{class_name} {confidence:.2f}" - draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) - - height = min(abs_y2, H) - max(0, abs_y1) - width = min(abs_x2, W) - max(0, abs_x1) - # object_res = [ - # abs_x1, - # abs_y1, - # height, - # width, - # class_name, - # confidence, - # H, - # W, - # ] - - # Resized - scale_x = self.resize_w / W - scale_y = self.resize_h / H - object_res = [ - int(abs_x1 * scale_x), - int(abs_y1 * scale_y), - int(height * scale_y), - int(width * scale_x), - class_name, - confidence, - int(self.resize_h), - int(self.resize_w), - ] - - framenum_str = f"{frameNum:04d}_{j:04d}" - if DEBUG_FLAG: - meta_str = ",".join( - [str(o) for o in object_res + [framenum_str]] - ) - print(f"[{stream_name} METADATA],{meta_str}", flush=True) - - # Full Res - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": { - "x": int(object_res[0]), - "y": int(object_res[1]), - "height": int(object_res[2]), - "width": int(object_res[3]), - "object": str(object_res[4]), - "object_det": { - "confidence": float(object_res[5]), - "frameH": int(object_res[6]), - "frameW": int(object_res[7]), - }, - }, - } - - # Queue frame for display (reduce quality slightly to 80 for 8K bandwidth) - frame_bytes = get_display_frame_in_bytes( - foi, - self.frame_width, - display_size=DISPLAY_FRAME_SIZE, - quality=DISPLAY_FRAME_QUALITY, - ) - - return metadata, frame_bytes - - def contour2predictions( - self, frameNum, mask, frame, device_input="cpu", repeat_count=1 - ): - # source = self.source - # stream_name = self.name - contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - # 3. Write frame - # self.video_writer.write(frame) - # if self.video_writer: - # for _ in range(repeat_count): - # self.video_writer.write(self.cpu_resized_frame) - - # num_objs = 0 - # predictions = [] - metadata = dict() - if contours: - metadata, frame_bytes = self.get_detections_for_contours_bbs( - frameNum, frame, contours, thickness=2, device_input=device_input - ) - - if metadata: - all_metadata.setdefault( - self.clip_key, - { - "object": {}, - "face": {}, - }, - ) - all_metadata[self.clip_key]["object"].update(metadata) - # all_metadata[clip_key]["face"].update(metadata_face) + # frame_bytes returned even if no metadata available return frame_bytes diff --git a/fastapi/include/utils.py b/fastapi/include/utils.py index e424471..8e2ba87 100644 --- a/fastapi/include/utils.py +++ b/fastapi/include/utils.py @@ -56,6 +56,7 @@ def str2bool(in_val): ] YOLO_BATCH_SIZE = 1 +ENABLE_VDMS = os.getenv("ENABLE_VDMS", True) DBPORT = 55555 DETECTION_THRESHOLD = 0.25 DEVICE_OV = "AUTO" @@ -128,7 +129,8 @@ def return_connection(self, conn): self.pool.put(conn) -VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) +if ENABLE_VDMS: # == True: + VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) LOCKTIMEOUT_RETRIES = 5 ERR_KEYWORDS = [ @@ -540,19 +542,26 @@ def get_models(model_tag: str, model_dir=PROJECT_PATH / "models"): # , _st_side return model, model_path, labels -# -def get_display_frame_in_bytes(foi, frame_width, display_size=(1280, 720), quality=50): - if frame_width > display_size[0]: - display_frame = cv2.resize(foi, display_size, interpolation=cv2.INTER_NEAREST) +def get_display_frame_in_bytes( + foi, display_size=(960, 540), quality=50, return_bytes=True, device="CPU" +): + H, W = foi.shape[:2] + dH, dW = display_size + if H == dH and W == dW: ret, buffer = cv2.imencode( - ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + ".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), quality] ) + # print(f"[get_display_frame_in_bytes] display_size: {foi.shape}", flush=True) else: + display_frame = cv2.resize(foi, display_size, interpolation=cv2.INTER_NEAREST) ret, buffer = cv2.imencode( - ".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality] ) - if ret: + # print(f"[get_display_frame_in_bytes] display_size: {display_frame.shape}", flush=True) + if ret and return_bytes: frame_bytes = buffer.tobytes() + elif ret: + frame_bytes = buffer else: frame_bytes = None @@ -861,90 +870,176 @@ def release_clip_and_reencode(clip_key, _out_vid, clip_filename, tmp_file, targe return _out_vid +# def merge_boxes_limit(bbs_full_res, dist_threshold=50, size_limit=640): +# """ +# boxes: list of [x1, y1, x2, y2] +# dist_threshold: max distance between boxes to consider them 'connected' +# size_limit: max width/height for a merged box +# """ +# if len(bbs_full_res) == 0: +# return [] + +# rects = np.array(bbs_full_res) +# num_boxes = len(rects) +# parent = list(range(num_boxes)) + +# def find(i): +# if parent[i] == i: +# return i +# parent[i] = find(parent[i]) +# return parent[i] + +# def union(i, j): +# root_i, root_j = find(i), find(j) +# if root_i != root_j: +# # Check if merging exceeds size limit +# temp_x1 = min(rects[root_i][0], rects[root_j][0]) +# temp_y1 = min(rects[root_i][1], rects[root_j][1]) +# temp_x2 = max(rects[root_i][2], rects[root_j][2]) +# temp_y2 = max(rects[root_i][3], rects[root_j][3]) + +# if (temp_x2 - temp_x1 <= size_limit) and (temp_y2 - temp_y1 <= size_limit): +# parent[root_i] = root_j +# # Update the root rectangle to the new merged bounds +# rects[root_j] = [temp_x1, temp_y1, temp_x2, temp_y2] + +# # 2. Compare boxes (Optimized: only check nearby ones if sorted by X) +# for i in range(num_boxes): +# for j in range(i + 1, num_boxes): +# # Proximity check (Manhattan distance or check if boxes are 'close') +# dx = max(0, max(rects[i][0], rects[j][0]) - min(rects[i][2], rects[j][2])) +# dy = max(0, max(rects[i][1], rects[j][1]) - min(rects[i][3], rects[j][3])) + +# if dx < dist_threshold and dy < dist_threshold: +# union(i, j) + +# # 3. Extract unique merged boxes +# final_boxes = [] +# unique_roots = set() +# for i in range(num_boxes): +# root = find(i) +# if root not in unique_roots: +# unique_roots.add(root) +# final_boxes.append(rects[root]) + +# return final_boxes + + def merge_boxes_limit(bbs_full_res, dist_threshold=50, size_limit=640): - """ - boxes: list of [x1, y1, x2, y2] - dist_threshold: max distance between boxes to consider them 'connected' - size_limit: max width/height for a merged box - """ - if len(bbs_full_res) == 0: + if not bbs_full_res: return [] - rects = np.array(bbs_full_res) - num_boxes = len(rects) - parent = list(range(num_boxes)) - - def find(i): - if parent[i] == i: - return i - parent[i] = find(parent[i]) - return parent[i] - - def union(i, j): - root_i, root_j = find(i), find(j) - if root_i != root_j: - # Check if merging exceeds size limit - temp_x1 = min(rects[root_i][0], rects[root_j][0]) - temp_y1 = min(rects[root_i][1], rects[root_j][1]) - temp_x2 = max(rects[root_i][2], rects[root_j][2]) - temp_y2 = max(rects[root_i][3], rects[root_j][3]) - - if (temp_x2 - temp_x1 <= size_limit) and (temp_y2 - temp_y1 <= size_limit): - parent[root_i] = root_j - # Update the root rectangle to the new merged bounds - rects[root_j] = [temp_x1, temp_y1, temp_x2, temp_y2] - - # 2. Compare boxes (Optimized: only check nearby ones if sorted by X) + # 🏎️ Optimization 1: Sort by X to allow early exit + bbs_full_res = sorted(bbs_full_res, key=lambda x: x[0]) + num_boxes = len(bbs_full_res) + merged = [] + used = [False] * num_boxes + for i in range(num_boxes): + if used[i]: + continue + + curr = bbs_full_res[i] + used[i] = True + + # Greedy merge with neighbors for j in range(i + 1, num_boxes): - # Proximity check (Manhattan distance or check if boxes are 'close') - dx = max(0, max(rects[i][0], rects[j][0]) - min(rects[i][2], rects[j][2])) - dy = max(0, max(rects[i][1], rects[j][1]) - min(rects[i][3], rects[j][3])) + if used[j]: + continue - if dx < dist_threshold and dy < dist_threshold: - union(i, j) + # 🏎️ Optimization 2: Early Exit + # If the next box starts further away than the threshold, + # no subsequent boxes can possibly merge. + if bbs_full_res[j][0] - curr[2] > dist_threshold: + break - # 3. Extract unique merged boxes - final_boxes = [] - unique_roots = set() - for i in range(num_boxes): - root = find(i) - if root not in unique_roots: - unique_roots.add(root) - final_boxes.append(rects[root]) + other = bbs_full_res[j] + # Check Y proximity + dy = max(0, max(curr[1], other[1]) - min(curr[3], other[3])) + + if dy < dist_threshold: + # Calculate potential merge + nx1, ny1 = min(curr[0], other[0]), min(curr[1], other[1]) + nx2, ny2 = max(curr[2], other[2]), max(curr[3], other[3]) - return final_boxes + if (nx2 - nx1 <= size_limit) and (ny2 - ny1 <= size_limit): + curr = [nx1, ny1, nx2, ny2] + used[j] = True + merged.append(curr) + return merged + + +# def filter_contained_boxes(boxes, containment_thresh=0.90): +# """ +# Deletes redundant boxes that are mostly inside another larger box. +# """ +# if not boxes: +# return [] + +# # 1. Sort by area (Largest boxes first) +# boxes = sorted(boxes, key=lambda b: (b[2] - b[0]) * (b[3] - b[1]), reverse=True) +# keep = [] + +# for child in boxes: +# is_contained = False +# for parent in keep: +# # Intersection coordinates +# ix1, iy1 = max(child[0], parent[0]), max(child[1], parent[1]) +# ix2, iy2 = min(child[2], parent[2]), min(child[3], parent[3]) + +# if ix2 > ix1 and iy2 > iy1: +# inter_area = (ix2 - ix1) * (iy2 - iy1) +# child_area = (child[2] - child[0]) * (child[3] - child[1]) + +# # If child is 90% inside a larger box, it's redundant +# if inter_area / child_area >= containment_thresh: +# is_contained = True +# break + +# if not is_contained: +# keep.append(child) + +# return keep def filter_contained_boxes(boxes, containment_thresh=0.90): - """ - Deletes redundant boxes that are mostly inside another larger box. - """ - if not boxes: - return [] + if len(boxes) < 2: + return boxes + + # Convert to NumPy for vectorized math + objs = np.array(boxes) + areas = (objs[:, 2] - objs[:, 0]) * (objs[:, 3] - objs[:, 1]) + + # Sort by area descending + order = areas.argsort()[::-1] + objs = objs[order] + areas = areas[order] - # 1. Sort by area (Largest boxes first) - boxes = sorted(boxes, key=lambda b: (b[2] - b[0]) * (b[3] - b[1]), reverse=True) keep = [] + idx_list = np.arange(len(objs)) + + while len(idx_list) > 0: + i = idx_list[0] + keep.append(objs[i].tolist()) + if len(idx_list) == 1: + break - for child in boxes: - is_contained = False - for parent in keep: - # Intersection coordinates - ix1, iy1 = max(child[0], parent[0]), max(child[1], parent[1]) - ix2, iy2 = min(child[2], parent[2]), min(child[3], parent[3]) + # Vectorized Intersection over Union (IoU) / Containment + others = objs[idx_list[1:]] + ix1 = np.maximum(objs[i, 0], others[:, 0]) + iy1 = np.maximum(objs[i, 1], others[:, 1]) + ix2 = np.minimum(objs[i, 2], others[:, 2]) + iy2 = np.minimum(objs[i, 3], others[:, 3]) - if ix2 > ix1 and iy2 > iy1: - inter_area = (ix2 - ix1) * (iy2 - iy1) - child_area = (child[2] - child[0]) * (child[3] - child[1]) + iw = np.maximum(0, ix2 - ix1) + ih = np.maximum(0, iy2 - iy1) + inter_area = iw * ih - # If child is 90% inside a larger box, it's redundant - if inter_area / child_area >= containment_thresh: - is_contained = True - break + # Calculate how much 'others' are contained within 'i' + containment = inter_area / areas[idx_list[1:]] - if not is_contained: - keep.append(child) + # Only keep boxes that are NOT mostly contained within the current box + idx_list = idx_list[1:][containment < containment_thresh] return keep diff --git a/fastapi/main.py b/fastapi/main.py index ea92383..ca5165f 100644 --- a/fastapi/main.py +++ b/fastapi/main.py @@ -1,26 +1,31 @@ +import warnings + +warnings.filterwarnings("ignore", message="The value of the smallest subnormal for") + + import asyncio import logging import os import sys - -# Force FFmpeg to use more threads for decoding import time +from include.handlers import VideoStreamHandler_WIP as VideoStreamHandler +from include.handlers import lifespan +from include.utils import DEBUG, StreamRequest + from fastapi import FastAPI, HTTPException, Request from fastapi.responses import StreamingResponse from fastapi.templating import Jinja2Templates -os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( - "rtsp_transport;tcp|hwaccel;cuda|threads;1|probesize;32|analyzeduration;0" -) - -from include.handlers import VideoStreamHandler, lifespan - -# from include.handlers import VideoStreamHandler2 as VideoStreamHandler, lifespan -# from include.handlers import VideoStreamHandler3 as VideoStreamHandler, lifespan -from include.utils import DEBUG, StreamRequest +# from include.handlers import VideoStreamHandler, lifespan # Choppy replay; up to 11 fps +# from include.handlers import VideoStreamHandler1 as VideoStreamHandler, lifespan # Choppy replay; up to 11 fps +# from include.handlers import VideoStreamHandler2 as VideoStreamHandler, lifespan # Really choppy replay w/ slight rewind; up to 15 fps +# from include.handlers import VideoStreamHandler3 as VideoStreamHandler, lifespan # Choppy replay likw 1; up to 10.5 fps +# from include.handlers import VideoStreamHandler4 as VideoStreamHandler, lifespan # Choppy; up to 11.4 fps +# from include.handlers import VideoStreamHandler5 as VideoStreamHandler, lifespan +# from include.handlers import VideoStreamHandler6 as VideoStreamHandler, lifespan -# ----- SETUP LOGGING ----- +# ----- LOGGING CONFIGURATION ----- logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", @@ -31,76 +36,128 @@ uvicorn_logger.setLevel(logging.INFO) -# --------------- APP ------------------- +# ----- APPLICATION INITIALIZATION ----- +# The lifespan parameter handles startup (model loading) and shutdown (memory cleanup) app = FastAPI(lifespan=lifespan) templates = Jinja2Templates(directory="templates") @app.get("/") async def index(request: Request): - """Renders the dashboard.""" + """ + Renders the main monitoring dashboard. + Passes current active stream IDs to the frontend for UI synchronization. + """ + curr_keys = list(request.app.state.active_streams.keys()) if DEBUG == "1": - print(f"Active Streams: {app.state.active_streams.keys()}") - curr_keys = list(app.state.active_streams.keys()) - # return templates.TemplateResponse( - # "index.html", {"request": request, "cameras": curr_keys} - # ) + print(f"Active Streams: {curr_keys}") + return templates.TemplateResponse( request=request, name="index.html", context={"cameras": curr_keys} ) @app.post("/stream") -async def stream_video(data: StreamRequest): +async def stream_video(data: StreamRequest, request: Request): + """ + Initializes a new VideoStreamHandler for a specific source. + If the stream is not already active, it starts a background processing thread. + """ url, name = data.url, data.name - # Start background thread - if name not in app.state.active_streams: + active_streams = request.app.state.active_streams + + if name not in active_streams: print(f"Starting background worker for {name}...") - app.state.active_streams[name] = VideoStreamHandler( - url, name, app.state.active_streams - ) # , model=app.state.model, lock=app.state.model_lock) - app.state.active_streams[name].start() - # DEBUG START + handler = VideoStreamHandler( + url, + name, + active_streams, + model=app.state.model, + ) + handler.start() + + # Register the handler in the global state for cross-endpoint access + app.state.active_streams[name] = handler + curr_keys = list(app.state.active_streams.keys()) if DEBUG == "1": print( f"stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" ) - # DEBUG END - return {"status": "started", "keys": list(app.state.active_streams.keys())} + return {"status": "started", "keys": curr_keys} + + +@app.get("/view_stream", name="view_stream") +async def view_stream(name: str, request: Request): + """ + High-bandwidth MJPEG streaming gateway. + Uses an asynchronous generator to pipe processed JPEG frames to the browser. + """ + active_streams = request.app.state.active_streams + if name not in active_streams: + raise HTTPException(status_code=404, detail="Stream not found") -@app.get("/debug_frame/{name}") -async def debug_frame(name: str): - streamer = app.state.active_streams.get(name) + streamer = active_streams.get(name) if not streamer: - return {"error": "not found"} - # DEBUG START - curr_keys = list(app.state.active_streams.keys()) - if DEBUG == "1": - print( - f"debug_frame DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" - ) - # DEBUG END - return { - "active": streamer.active, - "has_frame": streamer.latest_processed_frame is not None, - "frame_size": len(streamer.latest_processed_frame) - if streamer.latest_processed_frame - else 0, - } + raise HTTPException(status_code=404) + + async def frame_generator(streamer, request: Request): + """ + Yields frames only when the background worker signals a new frame is ready. + Ensures strict chronological order using frame IDs. + """ + last_sent_id = -1 + try: + while streamer.active: + # Stop the generator immediately if the browser tab is closed + if await request.is_disconnected(): + main_app_logger.info(f"Client disconnected from {name}") + break + + # Wait for the background thread to signal that AI processing is complete + await streamer.frame_ready_event.wait() + streamer.frame_ready_event.clear() # Reset for the next frame + + # Sequence check: skip if the frame is older than what we just sent + if streamer.last_frame_id > last_sent_id: + if streamer.latest_processed_frame: + frame_bytes = streamer.latest_processed_frame + streamer.last_heartbeat = time.time() + + # Multipart JPEG delivery with explicit Content-Length for stability + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n" + b"Content-Length: " + + str(len(frame_bytes)).encode() + + b"\r\n\r\n" # <--- Two \r\n + + frame_bytes + + b"\r\n" # <--- One \r\n + ) + last_sent_id = streamer.last_frame_id + + # Yield control to the event loop to prevent blocking + await asyncio.sleep(0.001) + except Exception as e: + main_app_logger.error(f"Generator Error: {e}") + + return StreamingResponse( + frame_generator(streamer, request), + media_type="multipart/x-mixed-replace;boundary=frame", + ) @app.get("/stream_list") async def get_stream_list(request: Request): - """Returns a list of currently active stream names.""" + """Returns a thread-safe list of all currently running stream identifiers.""" return list(request.app.state.active_streams.keys()) @app.get("/stream_stats") async def get_stats(request: Request): - # Return a dict mapping camera_id to its metrics + """Provides granular FPS and frame-count metrics for all running streams.""" return { name: { "fps": round(streamer.stat_fps, 1), @@ -113,79 +170,97 @@ async def get_stats(request: Request): } -@app.get("/status") -async def get_status(request: Request): - # Return a dict mapping camera_id to its metrics - return {"status": app.state.status if hasattr(app.state, "status") else "Loading"} - +@app.get("/dashboard_stats") +async def dashboard_stats(request: Request): + """ + Returns real-time performance metrics for the dashboard overlay, + including FPS and the background processing backlog. + """ + stats = {} + active_streams = request.app.state.active_streams + for name, streamer in active_streams.items(): + stats[name] = { + "current_fps": round(streamer.stat_fps, 2), + "reencode_backlog": streamer.get_executor_backlog(), + "total_frames": streamer.stat_frame_count, + } -@app.get("/view_stream", name="view_stream") -async def view_stream(name: str, request: Request): - if name not in request.app.state.active_streams: - raise HTTPException(status_code=404, detail="Stream not found") - streamer = request.app.state.active_streams.get(name) - if not streamer: - raise HTTPException(status_code=404) + # is_alive = getattr(streamer, "process_thread", None) and streamer.process_thread.is_alive() + + # Safely get the buffer size under lock to avoid race conditions + # with streamer.buffer_lock: + # buffer_backlog = len(streamer.frame_buffer) + + # stats[name] = { + # "status": "Active" if streamer.active else "Inactive", + # "thread_alive": is_alive, + # "fps": round(streamer.stat_fps, 2), + # "total_frames": streamer.stat_frame_count, + # "ai_backlog": streamer.get_executor_backlog(), # Tasks in ThreadPool + # "display_buffer_size": buffer_backlog, # Frames waiting for sequence + # "next_expected_frame": streamer.next_display_id, + # "last_heartbeat": round(time.time() - streamer.last_heartbeat, 2) + # } + return stats - async def frame_generator(): - # try: - while streamer.active: - if await request.is_disconnected(): - break - # 2. Update Heartbeat for Auto-Cleanup - streamer.last_heartbeat = time.time() - # 3. Only send a frame if a NEW one is ready - if streamer.latest_processed_frame: - # streamer.latest_processed_frame must be raw JPEG bytes - yield ( - b"--frame\r\n" - b"Content-Type: image/jpeg\r\n\r\n" - + streamer.latest_processed_frame - + b"\r\n" - ) - - # Tiny sleep (1ms) to prevent 100% CPU usage while waiting - # for the next unique frame to arrive from the detector. - await asyncio.sleep(0.01) - - # finally: - # if name in request.app.state.active_streams: - # del request.app.state.active_streams[name] - return StreamingResponse( - frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame" - ) +@app.get("/status") +async def get_status(request: Request): + """Returns the overall system status (e.g., 'Ready', 'Loading', or 'Error').""" + return { + "status": request.app.state.status + if hasattr(request.app.state, "status") + else "Loading" + } -@app.post("/stop_stream/{name}") # or @app.delete +@app.post("/stop_stream/{name}") async def stop_stream(name: str, request: Request): - """Gracefully stops a background stream and cleans up memory.""" + """ + Gracefully stops a single stream and releases its hardware/VRAM resources. + The blocking cleanup logic is offloaded to a separate thread to prevent API hang. + """ streamer = request.app.state.active_streams.get(name) if not streamer: raise HTTPException(status_code=404, detail=f"Stream '{name}' not found.") - # 1. Trigger the internal stop (releases CV2 cap and joins threads) - streamer.stop() + # Execute the heavy hardware/thread cleanup off the main event loop + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, streamer.stop) - # 2. Remove from the shared state - del request.app.state.active_streams[name] + # Remove reference from global state to allow Garbage Collection + app.state.active_streams.pop(name, None) if DEBUG == "1": print(f"--- CLEANUP | Stream '{name}' stopped and removed. ---") + return {"status": "stopped", "camera": name} -@app.get("/dashboard_stats") -async def dashboard_stats(request: Request): - stats = {} - for name, streamer in request.app.state.active_streams.items(): - stats[name] = { - "current_fps": round(streamer.stat_fps, 2), - "reencode_backlog": streamer.get_executor_backlog(), - "total_frames": streamer.stat_frame_count, - } - return stats +@app.post("/stop_all") +async def stop_all_streams(): + """ + Stops all active cameras and purges hardware resources. + Uses a list snapshot to safely iterate while modifying the dictionary. + """ + active_streams = app.state.active_streams + active_names = list(active_streams.keys()) + + if not active_names: + return {"status": "success", "message": "No active streams to stop"} + + for name in active_names: + streamer = active_streams.get(name) + if streamer: + streamer.stop() + app.state.active_streams.pop(name, None) + + return { + "status": "success", + "stopped_count": len(active_names), + "cleared_streams": active_names, + } if __name__ == "__main__": diff --git a/fastapi/nginx.conf b/fastapi/nginx.conf index 333185c..debe316 100644 --- a/fastapi/nginx.conf +++ b/fastapi/nginx.conf @@ -3,6 +3,7 @@ events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; + client_max_body_size 100M; server { listen 80; @@ -13,7 +14,8 @@ http { proxy_pass http://fastapi-service:8000; proxy_http_version 1.1; - proxy_set_header Connection ""; # Keep connection open + # proxy_set_header Connection ""; # Keep connection open + proxy_set_header Connection "keep-alive"; # Absolute zero buffering proxy_buffering off; # DISBALE Nginx buffering @@ -28,6 +30,8 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + chunked_transfer_encoding on; + # Increase timeouts for long-running video streams proxy_read_timeout 3600s; # Keep stream open for up to 1 hour proxy_send_timeout 3600s; diff --git a/fastapi/templates/index.html b/fastapi/templates/index.html index 44ca092..d47683f 100644 --- a/fastapi/templates/index.html +++ b/fastapi/templates/index.html @@ -5,6 +5,7 @@ AI Detection Dashboard