diff --git a/.github/resources/sdl/.trivyignore.yaml b/.github/resources/sdl/.trivyignore.yaml index 56c27783d..2a33e121f 100644 --- a/.github/resources/sdl/.trivyignore.yaml +++ b/.github/resources/sdl/.trivyignore.yaml @@ -11,19 +11,19 @@ misconfigurations: # Helm chart - id: AVD-KSV-0005 paths: - - "kubernetes/scenescape-chart/templates/web-dep.yaml" + - "kubernetes/scenescape-chart/templates/web-app/deployment.yaml" statement: Current implementation requires admin capabilities - id: AVD-KSV-0014 paths: - - "kubernetes/scenescape-chart/templates/camcalibration-dep.yaml" - - "kubernetes/scenescape-chart/templates/pgserver-dep.yaml" - - "kubernetes/scenescape-chart/templates/queuing-dep.yaml" - - "kubernetes/scenescape-chart/templates/retail-dep.yaml" - - "kubernetes/scenescape-chart/templates/vdms-dep.yaml" - - "kubernetes/scenescape-chart/templates/web-dep.yaml" - - "kubernetes/scenescape-chart/templates/kubeclient-dep.yaml" + - "kubernetes/scenescape-chart/templates/camcalibration/deployment.yaml" + - "kubernetes/scenescape-chart/templates/pgserver/deployment.yaml" + - "kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/queuing-deployment.yaml" + - "kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/retail-deployment.yaml" + - "kubernetes/scenescape-chart/templates/vdms/deployment.yaml" + - "kubernetes/scenescape-chart/templates/web-app/deployment.yaml" + - "kubernetes/scenescape-chart/templates/kubeclient/deployment.yaml" statement: Current implementation requires these containers to write to filesystem - id: AVD-KSV-0017 paths: - - "kubernetes/scenescape-chart/templates/web-dep.yaml" + - "kubernetes/scenescape-chart/templates/web-app/deployment.yaml" statement: Current implementation requires privileged container diff --git a/.github/resources/sdl/trivy_config.yml b/.github/resources/sdl/trivy_config.yml index d3a624bd8..86ab4e92c 100644 --- a/.github/resources/sdl/trivy_config.yml +++ b/.github/resources/sdl/trivy_config.yml @@ -14,3 +14,4 @@ misconfiguration: helm: set: - supass=demo + - pgserver.password=demo diff --git a/Makefile b/Makefile index 0fbc98e13..737cbb136 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,8 @@ SHELL := /bin/bash # Build folders COMMON_FOLDER := scene_common -IMAGE_FOLDERS := autocalibration controller manager mapping model_installer cluster_analytics +CORE_IMAGE_FOLDERS := autocalibration controller manager model_installer +IMAGE_FOLDERS := $(CORE_IMAGE_FOLDERS) mapping cluster_analytics # Build flags EXTRA_BUILD_FLAGS := @@ -24,7 +25,7 @@ COMPOSE_PROJECT_NAME ?= scenescape # - User can adjust build output folder (defaults to $PWD/build) BUILD_DIR ?= $(PWD)/build # - User can adjust folders being built (defaults to all) -FOLDERS ?= $(IMAGE_FOLDERS) +FOLDERS ?= $(CORE_IMAGE_FOLDERS) # - User can adjust number of parallel jobs (defaults to CPU count) JOBS ?= $(shell nproc) # - User can adjust the target branch @@ -45,8 +46,8 @@ DLSTREAMER_DOCKER_COMPOSE_FILE := ./sample_data/docker-compose-dl-streamer-examp # Test variables TESTS_FOLDER := tests TEST_DATA_FOLDER := test_data -TEST_IMAGE_FOLDERS := autocalibration controller manager mapping -TEST_IMAGES := $(addsuffix -test, camcalibration controller manager mapping) +TEST_IMAGE_FOLDERS := autocalibration controller manager mapping cluster_analytics +TEST_IMAGES := $(addsuffix -test, camcalibration controller manager mapping cluster_analytics) DEPLOYMENT_TEST ?= 0 # Observability variables @@ -59,10 +60,13 @@ CONTROLLER_TRACING_SAMPLE_RATIO ?= 1.0 # ========================= Default Target =========================== -default: build-all +default: build-core + +.PHONY: build-core +build-core: init-secrets build-core-images install-models .PHONY: build-all -build-all: init-secrets build-images install-models +build-all: init-secrets build-all-images install-models # ============================== Help ================================ @@ -72,13 +76,16 @@ help: @echo "Intel® SceneScape version $(VERSION)" @echo "" @echo "Available targets:" - @echo " build-all (default) Build secrets, all images, and install models" - @echo " build-images Build all microservice images in parallel" + @echo " build-core (default) Build secrets, core images (excluding mapping and cluster_analytics), and install models" + @echo " build-all Build secrets, all images, and install models" + @echo " build-core-images Build core microservice images (excluding mapping and cluster_analytics) in parallel" + @echo " build-all-images Build all microservice images in parallel" @echo " init-secrets Generate secrets and certificates" @echo " Build a specific microservice image (autocalibration, controller, etc.)" @echo "" - @echo " demo Start the SceneScape demo using Docker Compose" - @echo " (the demo target requires the SUPASS environment variable to be set" + @echo " demo (default) Start the SceneScape demo with core services using Docker Compose" + @echo " demo-all Start the SceneScape demo with all services using Docker Compose" + @echo " (the demo targets require the SUPASS environment variable to be set" @echo " as the super user password for logging into Intel® SceneScape)" @echo " demo-k8s Start the SceneScape demo using Kubernetes" @echo "" @@ -89,11 +96,15 @@ help: @echo " upgrade-database Backup and upgrade database to a newer PostgreSQL version" @echo " (automatically transfers data to Docker volumes)" @echo "" - @echo " rebuild Clean and build all images" + @echo " rebuild-core Clean and build core images and create secrets and volumes" + @echo " rebuild-core-images Clean and build core images" @echo " rebuild-all Clean and build everything including secrets and volumes" + @echo " rebuild-all-images Clean and build all images" @echo "" - @echo " clean Clean images and build artifacts (logs etc.)" + @echo " clean-core Clean core images and remove secrets, volumes and models" + @echo " clean-core-images Clean core images" @echo " clean-all Clean everything including volumes, secrets and models" + @echo " clean-images Clean all images" @echo " clean-volumes Remove all project Docker volumes" @echo " clean-secrets Remove all generated secrets" @echo " clean-models Remove all installed models" @@ -173,42 +184,81 @@ $(IMAGE_FOLDERS): @echo "DONE ====> Building folder $@" # Dependency on the common base image -autocalibration controller manager mapping: build-common +autocalibration controller manager mapping cluster_analytics: build-common + +# Helper function to build images in parallel +define parallel-build + @echo "==> Running parallel builds of folders: $(1)" + @set -e; trap 'grep --color=auto -i -r --include="*.log" "^error" $(BUILD_DIR) || true' EXIT; \ + $(MAKE) -j$(JOBS) $(1) + @echo "DONE ==> Parallel builds of folders: $(1)" +endef # Parallel wrapper handles parallel builds of folders specified in FOLDERS variable -.PHONY: build-images -build-images: $(BUILD_DIR) - @echo "==> Running parallel builds of folders: $(FOLDERS)" +.PHONY: build-all-images +build-all-images: $(BUILD_DIR) + $(call parallel-build, $(IMAGE_FOLDERS)) + +# Parallel wrapper for core images (excluding mapping and cluster_analytics) +.PHONY: build-core-images +build-core-images: $(BUILD_DIR) + $(call parallel-build, $(CORE_IMAGE_FOLDERS)) + +# Parallel wrapper for core images (excluding mapping and cluster_analytics) +.PHONY: build-core-images +build-core-images: $(BUILD_DIR) + @echo "==> Running parallel builds of core folders: $(CORE_IMAGE_FOLDERS)" # Use a trap to catch errors and print logs if any error occurs in parallel build @set -e; trap 'grep --color=auto -i -r --include="*.log" "^error" $(BUILD_DIR) || true' EXIT; \ - $(MAKE) -j$(JOBS) $(FOLDERS) - @echo "DONE ==> Parallel builds of folders: $(FOLDERS)" + $(MAKE) -j$(JOBS) $(CORE_IMAGE_FOLDERS) + @echo "DONE ==> Parallel builds of core folders: $(CORE_IMAGE_FOLDERS)" # ===================== Cleaning and Rebuilding ======================= +.PHONY: rebuild-core-images +rebuild-core-images: clean-core-images build-core-images -.PHONY: rebuild -rebuild: clean build-images +.PHONY: rebuild-core +rebuild-core: clean-core build-core + +.PHONY: rebuild-all-images +rebuild-all-images: clean-images build-all-images .PHONY: rebuild-all rebuild-all: clean-all build-all -.PHONY: clean -clean: +define clean-image-folders @echo "==> Cleaning up all build artifacts..." - @for dir in $(FOLDERS); do \ + @for dir in $(1); do \ $(MAKE) -C $$dir clean 2>/dev/null; \ done @echo "Cleaning common folder..." @$(MAKE) -C $(COMMON_FOLDER) clean 2>/dev/null @-rm -rf $(BUILD_DIR) @echo "DONE ==> Cleaning up all build artifacts" +endef + +.PHONY: clean-core-images +clean-core-images: + $(call clean-image-folders,$(CORE_IMAGE_FOLDERS)) + +.PHONY: clean-images +clean-images: + $(call clean-image-folders,$(IMAGE_FOLDERS)) + +.PHONY: clean-core +clean-core: clean-core-images clean-secrets clean-volumes clean-models clean-tests + $(call clean-artifacts) .PHONY: clean-all -clean-all: clean clean-secrets clean-volumes clean-models clean-tests - @echo "==> Cleaning all..." +clean-all: clean-images clean-secrets clean-volumes clean-models clean-tests + $(call clean-artifacts) + +define clean-artifacts + @echo "==> Cleaning build artifacts..." @-rm -f $(DLSTREAMER_SAMPLE_VIDEOS) @-rm -f docker-compose.yml .env - @echo "DONE ==> Cleaning all" + @echo "DONE ==> Cleaning build artifacts" +endef .PHONY: clean-models clean-models: @@ -286,7 +336,7 @@ install-models: # =========================== Run Tests ============================== .PHONY: setup_tests -setup_tests: build-images init-secrets .env +setup_tests: build-all-images init-secrets .env @echo "Setting up test environment..." for dir in $(TEST_IMAGE_FOLDERS); do \ $(MAKE) -C $$dir test-build; \ @@ -486,14 +536,39 @@ init-sample-data: convert-dls-videos fi @echo "Sample data volume initialized." +# Helper target to start demo with compose +define start_demo + @$(MAKE) docker-compose.yml + @$(MAKE) .env + @if [ -z "$$SUPASS" ]; then \ + echo "Please set the SUPASS environment variable before starting the demo for the first time."; \ + echo "The SUPASS environment variable is the super user password for logging into Intel® SceneScape."; \ + exit 1; \ + fi + docker compose $(1) up -d + @echo "" + @echo "To stop SceneScape, type:" + @echo " docker compose $(1) down" +endef + .PHONY: demo -demo: docker-compose.yml .env init-sample-data +demo: build-core init-sample-data + $(call start_demo,) + +.PHONY: demo-all +demo-all: build-all init-sample-data + $(call start_demo,--profile experimental) + +.PHONY: demo-all +demo-all: build-all init-sample-data + @$(MAKE) docker-compose.yml + @$(MAKE) .env @if [ -z "$$SUPASS" ]; then \ echo "Please set the SUPASS environment variable before starting the demo for the first time."; \ echo "The SUPASS environment variable is the super user password for logging into Intel® SceneScape."; \ exit 1; \ fi - docker compose up -d + docker compose --profile experimental up -d @echo "" @echo "To stop SceneScape, type:" @echo " docker compose down" diff --git a/cluster_analytics/Dockerfile b/cluster_analytics/Dockerfile index ce9ecf3b6..79c57893b 100644 --- a/cluster_analytics/Dockerfile +++ b/cluster_analytics/Dockerfile @@ -40,8 +40,6 @@ RUN : \ && rm -rf scene_common \ && rm -f /tmp/requirements-build.txt -COPY ./tools/waitforbroker /tmp/tools/waitforbroker - # -------------- Cluster Analytics Runtime Stage -------------- FROM scenescape-common-base-24-04 AS scenescape-cluster-analytics-runtime @@ -67,7 +65,6 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN : \ && apt-get update \ && apt-get install -y --no-install-recommends \ - gosu \ libgl1 \ libopencv-contrib406t64 \ libpython3.12 \ @@ -99,10 +96,8 @@ RUN : \ COPY --chown=$WSUSER:$WSUSER --from=scenescape-common-base-24-04 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/fast_geometry /usr/local/lib/python${PYTHON_VERSION}/dist-packages/fast_geometry COPY --chown=$WSUSER:$WSUSER --from=scenescape-common-base-24-04 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/scene_common /usr/local/lib/python${PYTHON_VERSION}/dist-packages/scene_common -COPY --chown=$WSUSER:$WSUSER --from=scenescape-common-base-24-04 /tmp/tools/waitforbroker $SCENESCAPE_HOME/tools/waitforbroker USER $USER_ID:$GROUP_ID -HEALTHCHECK CMD true # Copy source code COPY ./cluster_analytics/src /app diff --git a/cluster_analytics/config/config.json b/cluster_analytics/config/config.json index 4f7d13760..f98d09767 100644 --- a/cluster_analytics/config/config.json +++ b/cluster_analytics/config/config.json @@ -6,8 +6,8 @@ }, "category_specific": { "person": { - "eps": 1, - "min_samples": 3 + "eps": 2, + "min_samples": 2 }, "vehicle": { "eps": 4.0, @@ -30,39 +30,5 @@ "min_samples": 2 } } - }, - "cluster_tracking": { - "state_transitions": { - "frames_to_activate": 3, - "frames_to_stable": 20, - "frames_to_fade": 15, - "frames_to_lost": 10 - }, - "confidence": { - "initial_confidence": 0.5, - "activation_threshold": 0.6, - "stability_threshold": 0.7, - "miss_penalty": 0.1, - "max_miss_penalty": 0.5, - "longevity_bonus_max": 0.2, - "longevity_frames": 100 - }, - "archival": { - "archive_time_threshold": 5.0 - } - }, - "shape_detection": { - "variance_threshold": 0.5, - "quadrant_angle": 1.5707963267948966, - "angle_distribution_threshold": 0.5, - "linear_formation_area_threshold": 0.5 - }, - "movement_analysis": { - "alignment_threshold": 0.5, - "convergence_divergence_ratio_threshold": 0.6 - }, - "velocity_analysis": { - "stationary_threshold": 0.1, - "velocity_coherence_threshold": 0.3 } } diff --git a/cluster_analytics/docs/user-guide/overview.md b/cluster_analytics/docs/user-guide/overview.md index effec36af..882df13c2 100644 --- a/cluster_analytics/docs/user-guide/overview.md +++ b/cluster_analytics/docs/user-guide/overview.md @@ -15,11 +15,11 @@ This service processes real-time object detection data from SceneScape scenes, a #### Using Docker Compose (Recommended) -The cluster analytics service is included in the main SceneScape demo docker-compose stack: +The cluster analytics service is included in the extended SceneScape demo docker-compose stack: ```bash SUPASS=admin123 make -SUPASS=admin123 make demo +SUPASS=admin123 make demo-all ``` ## Architecture diff --git a/cluster_analytics/src/cluster_analytics.py b/cluster_analytics/src/cluster_analytics.py index ba0be9745..82b1f47d3 100644 --- a/cluster_analytics/src/cluster_analytics.py +++ b/cluster_analytics/src/cluster_analytics.py @@ -7,6 +7,7 @@ import os from cluster_analytics_context import ClusterAnalyticsContext +from scene_common import log def build_argparser(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -21,11 +22,11 @@ def build_argparser(): # WebUI is disabled by default, can be enabled via flag parser.add_argument("--webui", action="store_true", default=False, - help="enable WebUI on port 5000 (default: disabled, can be enabled via flag)") + help="enable WebUI on port 9443 (default: disabled, can be enabled via flag)") parser.add_argument("--no-webui", dest="webui", action="store_false", help="disable WebUI") - parser.add_argument("--webui-port", type=int, default=5000, - help="WebUI port (default: 5000)") + parser.add_argument("--webui-port", type=int, default=9443, + help="WebUI port (default: 9443)") parser.add_argument("--webui-certfile", help="path to SSL certificate file for HTTPS WebUI (required when WebUI is enabled)") parser.add_argument("--webui-keyfile", @@ -38,16 +39,16 @@ def main(): # Validate WebUI certificate requirements if args.webui: if not args.webui_certfile or not args.webui_keyfile: - print("ERROR: WebUI is enabled but SSL certificate files are missing.") - print("Please provide both --webui-certfile and --webui-keyfile arguments,") - print("or disable WebUI with --no-webui") + log.error("WebUI is enabled but SSL certificate files are missing. " + "Please provide both --webui-certfile and --webui-keyfile arguments, " + "or disable WebUI with --no-webui") exit(1) - print("Cluster Analytics Container started") + log.info("Cluster Analytics Container started") if args.webui: - print(f"WebUI will be available at https://0.0.0.0:{args.webui_port}") + log.debug(f"WebUI will be available at https://0.0.0.0:{args.webui_port}") else: - print("WebUI is disabled") + log.debug("WebUI is disabled") analytics_context = ClusterAnalyticsContext(args.broker, args.brokerauth, diff --git a/cluster_analytics/src/cluster_analytics_context.py b/cluster_analytics/src/cluster_analytics_context.py index 761597d7b..613b5cebc 100644 --- a/cluster_analytics/src/cluster_analytics_context.py +++ b/cluster_analytics/src/cluster_analytics_context.py @@ -34,62 +34,48 @@ def __init__(self, config_path=None): config_data = json.load(f) log.info(f"Loaded configuration from {config_path}") except FileNotFoundError: - log.error(f"Configuration file not found: {config_path}") + log.error(f"Configuration file not found at expected location") + log.debug(f"Config path attempted: {config_path}") raise except json.JSONDecodeError as e: - log.error(f"Failed to parse configuration file: {e}") + log.error(f"Failed to parse configuration file (invalid JSON)") + log.debug(f"JSON parse error details: {e}") raise - # Load DBSCAN parameters dbscan_config = config_data.get('dbscan', {}) default_params = dbscan_config.get('default', {}) self.DEFAULT_DBSCAN_EPS = default_params.get('eps', 1) self.DEFAULT_DBSCAN_MIN_SAMPLES = default_params.get('min_samples', 3) self.CATEGORY_DBSCAN_PARAMS = dbscan_config.get('category_specific', {}) - # Load shape detection thresholds - shape_config = config_data.get('shape_detection', {}) - self.SHAPE_VARIANCE_THRESHOLD = shape_config.get('variance_threshold', 0.5) - self.QUADRANT_ANGLE = shape_config.get('quadrant_angle', np.pi / 2) - self.ANGLE_DISTRIBUTION_THRESHOLD = shape_config.get('angle_distribution_threshold', 0.5) - self.LINEAR_FORMATION_AREA_THRESHOLD = shape_config.get('linear_formation_area_threshold', 0.5) - - # Load movement analysis thresholds - movement_config = config_data.get('movement_analysis', {}) - self.ALIGNMENT_THRESHOLD = movement_config.get('alignment_threshold', 0.5) - self.CONVERGENCE_DIVERGENCE_RATIO_THRESHOLD = movement_config.get('convergence_divergence_ratio_threshold', 0.6) - - # Load velocity analysis thresholds - velocity_config = config_data.get('velocity_analysis', {}) - self.STATIONARY_THRESHOLD = velocity_config.get('stationary_threshold', 0.1) - self.VELOCITY_COHERENCE_THRESHOLD = velocity_config.get('velocity_coherence_threshold', 0.3) - - # Load cluster tracking parameters - tracking_config = config_data.get('cluster_tracking', {}) - - # State transition parameters - state_config = tracking_config.get('state_transitions', {}) - self.FRAMES_TO_ACTIVATE = state_config.get('frames_to_activate', 3) - self.FRAMES_TO_STABLE = state_config.get('frames_to_stable', 20) - self.FRAMES_TO_FADE = state_config.get('frames_to_fade', 5) - self.FRAMES_TO_LOST = state_config.get('frames_to_lost', 10) - - # Confidence parameters - confidence_config = tracking_config.get('confidence', {}) - self.INITIAL_CONFIDENCE = confidence_config.get('initial_confidence', 0.5) - self.ACTIVATION_THRESHOLD = confidence_config.get('activation_threshold', 0.6) - self.STABILITY_THRESHOLD = confidence_config.get('stability_threshold', 0.7) - self.CONFIDENCE_MISS_PENALTY = confidence_config.get('miss_penalty', 0.1) - self.CONFIDENCE_MAX_MISS_PENALTY = confidence_config.get('max_miss_penalty', 0.5) - self.CONFIDENCE_LONGEVITY_BONUS_MAX = confidence_config.get('longevity_bonus_max', 0.2) - self.CONFIDENCE_LONGEVITY_FRAMES = confidence_config.get('longevity_frames', 100) - - # Archival parameters - archival_config = tracking_config.get('archival', {}) - self.ARCHIVE_TIME_THRESHOLD = archival_config.get('archive_time_threshold', 5.0) + self.SHAPE_VARIANCE_THRESHOLD = 0.5 + self.QUADRANT_ANGLE = np.pi / 2 # 90 degrees, quadrant angle + self.ANGLE_DISTRIBUTION_THRESHOLD = 0.5 + self.LINEAR_FORMATION_AREA_THRESHOLD = 0.5 + + self.ALIGNMENT_THRESHOLD = 0.5 + self.CONVERGENCE_DIVERGENCE_RATIO_THRESHOLD = 0.6 + + self.STATIONARY_THRESHOLD = 0.1 + self.VELOCITY_COHERENCE_THRESHOLD = 0.3 + + self.FRAMES_TO_ACTIVATE = 3 + self.FRAMES_TO_STABLE = 20 + self.FRAMES_TO_FADE = 15 + self.FRAMES_TO_LOST = 10 + + self.INITIAL_CONFIDENCE = 0.5 + self.ACTIVATION_THRESHOLD = 0.6 + self.STABILITY_THRESHOLD = 0.7 + self.CONFIDENCE_MISS_PENALTY = 0.1 + self.CONFIDENCE_MAX_MISS_PENALTY = 0.5 + self.CONFIDENCE_LONGEVITY_BONUS_MAX = 0.2 + self.CONFIDENCE_LONGEVITY_FRAMES = 100 + + self.ARCHIVE_TIME_THRESHOLD = 5.0 class ClusterAnalyticsContext: - def __init__(self, broker, broker_auth, cert, root_cert, enable_webui=True, webui_port=5000, webui_certfile=None, webui_keyfile=None): + def __init__(self, broker, broker_auth, cert, root_cert, enable_webui=True, webui_port=9443, webui_certfile=None, webui_keyfile=None): self.config = ClusterAnalyticsConfig() self.webui_port = webui_port self.webui_certfile = webui_certfile @@ -121,10 +107,12 @@ def __init__(self, broker, broker_auth, cert, root_cert, enable_webui=True, webu self.webUi = WebUI(self) log.info("WebUI initialized successfully") except ImportError as e: - log.warn(f"WebUI dependencies not available: {e}") + log.warn(f"WebUI dependencies not available") + log.debug(f"WebUI import error: {e}") log.info("Cluster Analytics service will continue without WebUI") except Exception as e: - log.error(f"Failed to initialize WebUI: {e}") + log.error(f"Failed to initialize WebUI") + log.debug(f"WebUI initialization error: {e}") log.info("Cluster Analytics service will continue without WebUI") else: log.info("WebUI disabled via command line argument") @@ -132,10 +120,12 @@ def __init__(self, broker, broker_auth, cert, root_cert, enable_webui=True, webu try: self.client = PubSub(broker_auth, cert, root_cert, broker, keepalive=240) self.client.onConnect = self.mqttOnConnect - log.info(f"Attempting to connect to broker: {broker}") + log.info(f"Attempting to connect to MQTT broker") + log.debug(f"Broker address: {broker}") self.client.connect() except Exception as e: - log.error(f"Failed to connect to MQTT broker {broker}: {e}") + log.error(f"Failed to connect to MQTT broker") + log.debug(f"MQTT connection error: {e}") log.info("Cluster Analytics service will continue without MQTT connectivity") self.client = None @@ -156,7 +146,7 @@ def getDbscanParamsForCategory(self, category, scene_id=None): if scene_params: params = scene_params.get(category_lower) if params: - log.info(f"Using scene-specific user-configured DBSCAN parameters for '{category}' in scene '{scene_id}': eps={params['eps']}, min_samples={params['min_samples']}") + log.debug(f"Using scene-specific user-configured DBSCAN parameters for '{category}' in scene '{scene_id}': eps={params['eps']}, min_samples={params['min_samples']}") return params # Return category-specific default parameters if available @@ -169,7 +159,7 @@ def getDbscanParamsForCategory(self, category, scene_id=None): 'eps': self.config.DEFAULT_DBSCAN_EPS, 'min_samples': self.config.DEFAULT_DBSCAN_MIN_SAMPLES } - log.info(f"Using global default DBSCAN parameters for unknown category '{category}': eps={default_params['eps']}, min_samples={default_params['min_samples']}") + log.debug(f"Using global default DBSCAN parameters for unknown category '{category}': eps={default_params['eps']}, min_samples={default_params['min_samples']}") return default_params def setUserDbscanParamsForCategory(self, category, eps, min_samples, scene_id=None): @@ -197,7 +187,7 @@ def setUserDbscanParamsForCategory(self, category, eps, min_samples, scene_id=No if eps_change_ratio > 0.5 or min_samples_changed: cleared_count = self.cluster_tracker.forceClearClustersByCategory(scene_id, category_lower) if cleared_count > 0: - log.info(f"Cleared {cleared_count} existing clusters for '{category}' in scene '{scene_id}' due to significant parameter change") + log.debug(f"Cleared {cleared_count} existing clusters for '{category}' in scene '{scene_id}' due to significant parameter change") # Initialize scene parameters if not exists if scene_id not in self.user_dbscan_params_by_scene: @@ -206,7 +196,7 @@ def setUserDbscanParamsForCategory(self, category, eps, min_samples, scene_id=No # Store parameters for this scene and category self.user_dbscan_params_by_scene[scene_id][category_lower] = new_params - log.info(f"Set scene-specific user-configured DBSCAN parameters for '{category}' in scene '{scene_id}': eps={eps}, min_samples={min_samples}") + log.debug(f"Set scene-specific user-configured DBSCAN parameters for '{category}' in scene '{scene_id}': eps={eps}, min_samples={min_samples}") else: log.warning(f"Cannot set DBSCAN parameters for '{category}': no scene_id provided") @@ -242,16 +232,16 @@ def resetUserDbscanParamsForCategory(self, category, scene_id=None): # Force-clear existing clusters since parameters are changing back to defaults cleared_count = self.cluster_tracker.forceClearClustersByCategory(scene_id, category_lower) if cleared_count > 0: - log.info(f"Cleared {cleared_count} existing clusters for '{category}' in scene '{scene_id}' due to parameter reset") + log.debug(f"Cleared {cleared_count} existing clusters for '{category}' in scene '{scene_id}' due to parameter reset") del scene_params[category_lower] - log.info(f"Reset DBSCAN parameters for '{category}' in scene '{scene_id}' back to defaults") + log.debug(f"Reset DBSCAN parameters for '{category}' in scene '{scene_id}' back to defaults") # Clean up empty scene entries if not scene_params: del self.user_dbscan_params_by_scene[scene_id] else: - log.info(f"No custom DBSCAN parameters found for '{category}' in scene '{scene_id}' to reset") + log.debug(f"No custom DBSCAN parameters found for '{category}' in scene '{scene_id}' to reset") else: log.warning(f"Cannot reset DBSCAN parameters for '{category}': scene '{scene_id}' not found or no scene_id provided") @@ -265,9 +255,8 @@ def mqttOnConnect(self, client, userdata, flags, rc): @return None """ data_regulated_topic = PubSub.formatTopic(PubSub.DATA_REGULATED, scene_id="+") - log.info("Subscribing to " + data_regulated_topic) self.client.addCallback(data_regulated_topic, self.processSceneAnalytics) - log.info("Subscribed " + data_regulated_topic) + log.info("Subscribed to " + data_regulated_topic) return def processSceneAnalytics(self, client, userdata, message): @@ -292,11 +281,13 @@ def processSceneAnalytics(self, client, userdata, message): self.publishAllClusters(scene_id, detection_data, all_clusters) except json.JSONDecodeError as e: - log.error(f"Failed to parse detection data: {e}") + log.error(f"Failed to parse detection data from scene (invalid JSON)") + log.debug(f"JSON parse error details: {e}") except Exception as e: import traceback - log.error(f"Error processing detection data: {e}") - log.error(traceback.format_exc()) + log.error(f"Error processing detection data") + log.debug(f"Error details: {e}") + log.debug(traceback.format_exc()) return def extractCoordinatesFromObjects(self, objects): @@ -393,7 +384,7 @@ def analyzeObjectClusters(self, scene_id, detection_data): n_noise = np.sum(labels == -1) if n_clusters > 0: - log.info(f"Scene {scene_id}: Found {n_clusters} clusters for category '{category}' " + log.debug(f"Scene {scene_id}: Found {n_clusters} clusters for category '{category}' " f"({len(category_objects)} objects, {n_noise} noise points)") # Create detection metadata for each cluster @@ -437,7 +428,7 @@ def analyzeObjectClusters(self, scene_id, detection_data): # Log when no clusters are detected by DBSCAN if len(raw_cluster_detections) == 0: - log.info(f"Scene {scene_id}: No clusters detected by DBSCAN") + log.debug(f"Scene {scene_id}: No clusters detected by DBSCAN") # Clean up old/lost clusters to prevent stale data self.cluster_tracker.memory.cleanupOldClusters(timestamp) @@ -452,7 +443,8 @@ def _publishTrackedClusters(self, scene_id, detection_data): @return None """ if self.client is None or not self.client.isConnected(): - log.warning(f"Cannot publish cluster data for scene {scene_id}: MQTT client not connected") + log.warning(f"Cannot publish cluster data: MQTT client not connected") + log.debug(f"Scene ID: {scene_id}") return # Get active/stable clusters for this scene @@ -483,13 +475,15 @@ def _publishTrackedClusters(self, scene_id, detection_data): result = self.client.publish(topic, payload, qos=1) if result.rc == 0: if len(cluster_dicts) > 0: - log.info(f"Published {len(cluster_dicts)} clusters for scene {scene_id}") + log.debug(f"Published {len(cluster_dicts)} clusters for scene {scene_id}") else: - log.info(f"Published empty cluster batch for scene {scene_id} (no active clusters)") + log.debug(f"Published empty cluster batch for scene {scene_id} (no active clusters)") else: - log.error(f"Failed to publish cluster batch for scene {scene_id}: rc={result.rc}") + log.error(f"Failed to publish cluster batch (MQTT error)") + log.debug(f"Scene ID: {scene_id}, return code: {result.rc}") except Exception as e: - log.error(f"Error publishing cluster batch for scene {scene_id}: {e}") + log.error(f"Error publishing cluster batch") + log.debug(f"Scene ID: {scene_id}, error: {e}") return def publishAllClusters(self, scene_id, detection_data, all_clusters): @@ -803,7 +797,8 @@ def loopForever(self): ) log.info(f"WebUI server started on https://0.0.0.0:{self.webui_port}") except Exception as e: - log.error(f"Failed to start WebUI server: {e}") + log.error(f"Failed to start WebUI server") + log.debug(f"WebUI server error: {e}") if self.client: log.info("Starting MQTT client loop") diff --git a/cluster_analytics/src/cluster_analytics_tracker.py b/cluster_analytics/src/cluster_analytics_tracker.py index 85dc13d90..2fa06300a 100644 --- a/cluster_analytics/src/cluster_analytics_tracker.py +++ b/cluster_analytics/src/cluster_analytics_tracker.py @@ -61,34 +61,23 @@ class TrackedCluster: def __init__(self, scene_id: str, category: str, centroid: Dict[str, float], shape_analysis: Dict, velocity_analysis: Dict, object_ids: List[str], - dbscan_params: Dict, detection_timestamp: float, config=None) -> None: + dbscan_params: Dict, detection_timestamp: float) -> None: """Initialize a new tracked cluster""" - # Store config parameters (with defaults if not provided) - if config: - self.FRAMES_TO_ACTIVATE = config.FRAMES_TO_ACTIVATE - self.FRAMES_TO_STABLE = config.FRAMES_TO_STABLE - self.FRAMES_TO_FADE = config.FRAMES_TO_FADE - self.FRAMES_TO_LOST = config.FRAMES_TO_LOST - self.CONFIDENCE_MISS_PENALTY = config.CONFIDENCE_MISS_PENALTY - self.CONFIDENCE_LONGEVITY_BONUS_MAX = config.CONFIDENCE_LONGEVITY_BONUS_MAX - self.CONFIDENCE_LONGEVITY_FRAMES = config.CONFIDENCE_LONGEVITY_FRAMES - self.ACTIVATION_THRESHOLD = config.ACTIVATION_THRESHOLD - self.STABILITY_THRESHOLD = config.STABILITY_THRESHOLD - self.INITIAL_CONFIDENCE = config.INITIAL_CONFIDENCE - self.CONFIDENCE_MAX_MISS_PENALTY = config.CONFIDENCE_MAX_MISS_PENALTY - else: - # Fallback to hardcoded defaults if no config provided - self.FRAMES_TO_ACTIVATE = 3 - self.FRAMES_TO_STABLE = 20 - self.FRAMES_TO_FADE = 5 - self.FRAMES_TO_LOST = 10 - self.CONFIDENCE_MISS_PENALTY = 0.1 - self.CONFIDENCE_LONGEVITY_BONUS_MAX = 0.2 - self.CONFIDENCE_LONGEVITY_FRAMES = 100 - self.ACTIVATION_THRESHOLD = 0.6 - self.STABILITY_THRESHOLD = 0.7 - self.INITIAL_CONFIDENCE = 0.5 - self.CONFIDENCE_MAX_MISS_PENALTY = 0.5 + # Hardcoded cluster tracking parameters + # State transitions + self.FRAMES_TO_ACTIVATE = 3 + self.FRAMES_TO_STABLE = 20 + self.FRAMES_TO_FADE = 15 + self.FRAMES_TO_LOST = 10 + + # Confidence + self.INITIAL_CONFIDENCE = 0.5 + self.ACTIVATION_THRESHOLD = 0.6 + self.STABILITY_THRESHOLD = 0.7 + self.CONFIDENCE_MISS_PENALTY = 0.1 + self.CONFIDENCE_MAX_MISS_PENALTY = 0.5 + self.CONFIDENCE_LONGEVITY_BONUS_MAX = 0.2 + self.CONFIDENCE_LONGEVITY_FRAMES = 100 # Identity self.uuid = str(uuid.uuid4()) @@ -164,7 +153,7 @@ def update(self, centroid: Dict[str, float], shape_analysis: Dict, self._updatePrediction() if old_state != self.state: - log.info(f"Cluster {self.uuid} state transition: {old_state} -> {self.state}") + log.debug(f"Cluster {self.uuid} state transition: {old_state} -> {self.state}") return def markMissed(self, current_timestamp: float) -> None: @@ -179,7 +168,7 @@ def markMissed(self, current_timestamp: float) -> None: self._updateState() if old_state != self.state: - log.info(f"Cluster {self.uuid} state transition: {old_state} -> {self.state} (missed {self.frames_missed} frames)") + log.debug(f"Cluster {self.uuid} state transition: {old_state} -> {self.state} (missed {self.frames_missed} frames)") return def _updateConfidence(self) -> None: @@ -335,11 +324,8 @@ class ClusterMemory: MAX_ARCHIVED_CLUSTERS = 50 def __init__(self, config=None) -> None: - # Store config parameters - if config: - self.ARCHIVE_TIME_THRESHOLD = config.ARCHIVE_TIME_THRESHOLD - else: - self.ARCHIVE_TIME_THRESHOLD = 5.0 # Default fallback + # Hardcoded archival parameter + self.ARCHIVE_TIME_THRESHOLD = 5.0 # Primary storage self._active_clusters: Dict[str, TrackedCluster] = {} @@ -399,7 +385,7 @@ def archive(self, cluster_uuid: str) -> None: if cluster_uuid in self._clusters_by_category[cluster.category]: self._clusters_by_category[cluster.category].remove(cluster_uuid) - log.info(f"Archived cluster {cluster_uuid} (state: {cluster.state}, lifetime: {cluster.frames_detected} frames)") + log.debug(f"Archived cluster {cluster_uuid} (state: {cluster.state}, lifetime: {cluster.frames_detected} frames)") return def cleanupOldClusters(self, current_time: Optional[float]) -> None: @@ -453,8 +439,10 @@ def forceClearClustersByCategory(self, scene_id: str, category: str) -> int: cluster.state = ClusterState.LOST self.archive(cluster_uuid) cleared_count += 1 - log.info(f"Force-cleared cluster {cluster_uuid} due to parameter change " - f"(scene: {scene_id}, category: {category})") + + if cleared_count > 0: + log.info(f"Cleared {cleared_count} clusters due to parameter change") + log.debug(f"Scene: {scene_id}, Category: {category}") return cleared_count @@ -701,7 +689,7 @@ def _processCategoryDetections(self, scene_id: str, category: str, scene_id, detection, timestamp ) self.memory.add(new_cluster) - log.info(f"Created new cluster {new_cluster.uuid} " + log.debug(f"Created new cluster {new_cluster.uuid} " f"(scene: {scene_id}, category: {category})") # Mark unmatched existing clusters as missed diff --git a/cluster_analytics/tools/webui/README.md b/cluster_analytics/tools/webui/README.md index 8c08f152d..cc1946084 100644 --- a/cluster_analytics/tools/webui/README.md +++ b/cluster_analytics/tools/webui/README.md @@ -14,10 +14,10 @@ The WebUI is **disabled by default**. To enable it, follow the instructions belo ```bash cd /path/to/scenescape -SUPASS=admin123 make demo +SUPASS=admin123 make demo-all ``` -After enabling, access the WebUI at: **https://localhost:5000** +After enabling, access the WebUI at: **https://localhost:9443** ## ⚠️ Important Note @@ -38,7 +38,7 @@ The WebUI is **disabled by default** in `docker-compose.yml`. To enable it, **un # ... other config ... # Uncomment the following lines to enable WebUI: ports: # ✅ Uncomment this line - - "5000:5000" # ✅ Uncomment this line + - "9443:9443" # ✅ Uncomment this line ``` 2. **Uncomment the WebUI command flags:** @@ -77,7 +77,7 @@ If you want to **disable** the WebUI again, **comment out** these lines in `dock ```yaml # ports: - # - "5000:5000" # ❌ Port not exposed + # - "9443:9443" # ❌ Port not exposed ``` 2. **Comment out the WebUI command flags:** @@ -118,22 +118,22 @@ docker compose logs cluster-analytics | grep -i webui # Expected output: # "WebUI initialized successfully" -# "WebUI server started on https://0.0.0.0:5000" +# "WebUI server started on https://0.0.0.0:9443" # Test WebUI endpoint -curl -k https://localhost:5000 +curl -k https://localhost:9443 ``` ## 🌐 Accessing the WebUI -- **URL**: https://localhost:5000 +- **URL**: https://localhost:9443 - **Protocol**: HTTPS only (uses SSL certificates) ## 🛠️ Troubleshooting **WebUI not accessible?** -- Ensure port 5000 is not blocked by firewall +- Ensure port 9443 is not blocked by firewall - Check that SSL certificates are properly mounted - Verify the `--webui` flag is uncommented in docker-compose.yml diff --git a/cluster_analytics/tools/webui/web_ui.py b/cluster_analytics/tools/webui/web_ui.py index 1c811edca..70f615725 100644 --- a/cluster_analytics/tools/webui/web_ui.py +++ b/cluster_analytics/tools/webui/web_ui.py @@ -500,7 +500,7 @@ def updateSceneClusters(self, sceneId, clusters): # Schedule throttled update self.scheduleThrottledUpdate() - def run(self, host='0.0.0.0', port=5000, debug=False, certfile=None, keyfile=None): + def run(self, host='0.0.0.0', port=9443, debug=False, certfile=None, keyfile=None): """Run the Flask-SocketIO server with HTTPS.""" if not certfile or not keyfile: raise ValueError("SSL certificate and key files are required for HTTPS") @@ -515,7 +515,7 @@ def run(self, host='0.0.0.0', port=5000, debug=False, certfile=None, keyfile=Non keyfile=keyfile ) - def runInThread(self, host='0.0.0.0', port=5000, certfile=None, keyfile=None): + def runInThread(self, host='0.0.0.0', port=9443, certfile=None, keyfile=None): """Run the Flask-SocketIO server in a separate thread using eventlet with HTTPS.""" if not certfile or not keyfile: raise ValueError("SSL certificate and key files are required for HTTPS") diff --git a/docs/user-guide/additional-resources/how-to-upgrade.md b/docs/user-guide/additional-resources/how-to-upgrade.md index 067771798..e8e704d8b 100644 --- a/docs/user-guide/additional-resources/how-to-upgrade.md +++ b/docs/user-guide/additional-resources/how-to-upgrade.md @@ -25,7 +25,7 @@ Before You Begin, ensure the following: 2. **Build the New Release**: ```bash - make build-all + make build-core ``` 3. **Run the upgrade-database script**: diff --git a/docs/user-guide/building-a-scene/how-to-create-new-scene.md b/docs/user-guide/building-a-scene/how-to-create-new-scene.md index ddd4d1889..7b020e907 100644 --- a/docs/user-guide/building-a-scene/how-to-create-new-scene.md +++ b/docs/user-guide/building-a-scene/how-to-create-new-scene.md @@ -76,7 +76,7 @@ Refer to [How to Configure DLStreamer Video Pipeline](../other-topics/how-to-con ## Creating a scene floor plan -Creating an accurate floor plan image may be as simple as using a CAD drawing or a satellite map view. The most important aspects are: +Creating an accurate floor plan image may be as simple as using an existing blueprint, a CAD drawing, 3D reconstructed mesh or a satellite map view. The most important aspects are: 1. Making sure that there are details in the map to calibrate cameras against 2. Determining the scale of the image in pixels/meter diff --git a/docs/user-guide/images/ui/sample_region_tripwire.png b/docs/user-guide/images/ui/sample_region_tripwire.png new file mode 100644 index 000000000..b96e1e652 Binary files /dev/null and b/docs/user-guide/images/ui/sample_region_tripwire.png differ diff --git a/docs/user-guide/using-intel-scenescape/working-with-spatial-analytics-data.md b/docs/user-guide/using-intel-scenescape/working-with-spatial-analytics-data.md new file mode 100644 index 000000000..be72c1712 --- /dev/null +++ b/docs/user-guide/using-intel-scenescape/working-with-spatial-analytics-data.md @@ -0,0 +1,872 @@ +# Working with Spatial Analytics Data: ROIs and Tripwires + +This guide provides comprehensive information for developers who want to build applications that consume SceneScape's spatial analytics event data. You'll learn how to subscribe to MQTT events from Regions of Interest (ROIs) and Tripwires to create intelligent applications that respond to object interactions within defined areas, regardless of the sensor modality used for detection. + +## Table of Contents + +1. [Overview](#overview) +2. [Understanding ROIs and Tripwires](#understanding-rois-and-tripwires) +3. [Authentication](#authentication) +4. [Discovering Existing ROIs and Tripwires](#discovering-existing-rois-and-tripwires) +5. [MQTT Event Topics and Data Flow](#mqtt-event-topics-and-data-flow) +6. [Event Data Structures](#event-data-structures) +7. [Code Examples](#code-examples) +8. [Conclusion](#conclusion) + +--- + +## Overview + +SceneScape's spatial analytics system enables you to receive real-time notifications when objects interact with predefined virtual areas and boundaries within monitored scenes. The system supports various sensor modalities including cameras, lidar, radar, and other detection technologies. This guide focuses on consuming these events to build dynamic applications. + +### Multi-Sensor Advantages + +SceneScape's scene-based spatial analytics operate on a unified view that combines data from multiple sensors (cameras, lidar, radar, etc.), offering several key benefits: + +- **Comprehensive Coverage**: Multi-modal sensor fusion provides enhanced accuracy and reliability +- **Resilient Operation**: Sensor redundancy maintains monitoring even if individual sensors fail +- **Sensor-Agnostic Analytics**: ROIs and tripwires use scene-level coordinates, independent of specific sensors +- **Superior Performance**: Leverage strengths of different sensor types for better object tracking + +This approach enables applications with accuracy, coverage, and resilience impossible with single-sensor systems. + +### Key Problem Solutions + +SceneScape's scene-based approach addresses common analytics challenges: + +1. **Multi-Region Management**: Manage multiple regions across the entire scene rather than per-camera, simplifying configuration and maintenance +2. **Large Area Coverage**: Multi-sensor fusion covers areas too large for single sensors or with occlusion issues + +The sensor-agnostic architecture ensures spatial analytics continue working reliably as sensor infrastructure evolves. + +### Use Cases for Event Data + +- **Security and Surveillance**: Monitor restricted areas, detect unauthorized access +- **Traffic Management**: Count vehicles crossing intersections, monitor pedestrian crossings +- **Retail Analytics**: Track customer movement patterns, analyze dwell time in product areas +- **Industrial Safety**: Monitor safety zones, detect personnel in dangerous areas +- **Smart City Applications**: Optimize traffic flow, manage public spaces + +This guide focuses on consuming spatial analytics event data. ROIs and Tripwires are created through the SceneScape UI or REST API—see the [How to Configure Spatial Analytics](../building-a-scene/how-to-configure-spatial-analytics.md) guide for setup instructions. + +--- + +## Understanding ROIs and Tripwires + +![Sample Region and Tripwire](../images/ui/sample_region_tripwire.png) + +The image above shows a practical example from Intel's headquarters parking lot in Santa Clara, CA, demonstrating how spatial analytics elements are deployed in real-world scenarios. In this scene, you can see: + +- **Tripwire (green line)**: A directional tripwire positioned across a one-way lane to monitor vehicle traffic. The tripwire appears as a green line with small circles at each endpoint, and a short perpendicular line extending from the center that acts as a directional flag. This tripwire counts vehicles moving in the correct direction and can trigger alerts when vehicles drive the wrong way, providing both traffic analytics and safety monitoring for one-way traffic enforcement. + +- **Region of Interest (red polygon)**: A polygonal region drawn around the loading zone area. This ROI monitors when vehicles enter and exit the designated loading area, tracks how long they remain (dwell time), and can trigger alerts if the zone becomes occupied for extended periods or if multiple vehicles are present simultaneously. + +This shows how ROIs and tripwires work together to provide comprehensive monitoring: the tripwire captures traffic flow at access points, while the ROI provides detailed occupancy analytics for specific functional areas. + +### Regions of Interest (ROIs) + +ROIs are virtual areas defined within a scene's physical space where you want to monitor object presence and behavior. ROIs are defined at the scene level using world coordinates, making them independent of any specific sensor or viewing angle. Each ROI can track: + +- **Object Entry**: When objects enter the region +- **Object Exit**: When objects leave the region +- **Object Counts**: Number of objects currently in the region +- **Dwell Time**: How long objects remain in the region + +#### ROI Properties + +- **UUID**: Unique identifier for the ROI +- **Name**: Human-readable name for the ROI +- **Points**: Array of (x, y) coordinates defining the polygon boundary in scene world coordinates +- **Volumetric**: Controls how tracked objects are evaluated within the region: + - **When enabled**: Objects are treated as 3D volumes; events trigger if any part of the tracked volume intersects the region + - **When disabled**: Objects are treated as center points; events trigger only when the object's center point enters or leaves the region +- **Height**: Physical height of the ROI in meters +- **Buffer Size**: Additional boundary area around the defined polygon +- **Color Range**: When "Visualize ROIs" is enabled in the 2D UI, color the ROI based on occupancy thresholds + +### Tripwires + +Tripwires are virtual lines defined within a scene's physical space that detect when objects cross them. Like ROIs, tripwires are defined using scene world coordinates, making them independent of any specific sensor or viewing perspective. They are ideal for: + +- **Directional Counting**: Count objects moving in specific directions +- **Boundary Monitoring**: Detect when objects cross important boundaries +- **Flow Analysis**: Monitor traffic flow through specific points + +#### Tripwire Properties + +- **UUID**: Unique identifier for the tripwire +- **Name**: Human-readable name for the tripwire +- **Points**: Array of exactly 2 (x, y) coordinates defining the line endpoints in scene world coordinates + +--- + +## Authentication + +SceneScape supports different authentication approaches depending on your deployment scenario: + +### REST API Authentication + +For REST API access (discovering regions, tripwires, and configuration data): + +```bash +# Request header format +Authorization: Token +``` + +**Getting Your API Token:** +- Access the SceneScape Admin panel: `https:///admin` +- Navigate to **Tokens** section +- Use tokens from `admin` or `scenectrl` user accounts + +### MQTT Authentication Options + +#### Quick Testing & Development +For rapid testing and development, use admin credentials with WebSocket MQTT: + +```python +# WebSocket MQTT (port 443, easy setup) +client = mqtt.Client(transport="websockets") +client.username_pw_set("admin", os.environ['SUPASS']) # Web login password +``` + +**Pros**: Works immediately, no additional configuration +**Cons**: Uses admin credentials, WebSocket overhead + +#### Production Python Applications +For external applications, use dedicated MQTT accounts with direct protocol: + +```python +# Direct MQTT protocol (port 1883, more efficient) +client = mqtt.Client() +client.username_pw_set(mqtt_user, mqtt_password) +client.tls_set_context(ssl_context) +client.connect(host, 1883, 60) +``` + +**Setup Requirements:** +- Expose MQTT port 1883 in deployment configuration +- Create dedicated MQTT user accounts (not admin) +- Use MQTT-specific credentials from secrets management + +#### Web Applications +For browser-based applications, additional security considerations apply: + +```javascript +// Client-side WebSocket MQTT (security considerations required) +const client = mqtt.connect(`wss://${host}/mqtt`, { + username: 'limited_user', // NOT admin + password: 'client_password' // NOT SUPASS +}); +``` + +**Security Considerations:** +- **Never expose admin credentials to client-side code** +- Use limited-privilege MQTT accounts for web clients +- Consider authentication proxies or token-based MQTT access +- Implement proper credential rotation and access controls + +### Environment Setup + +**For Development/Testing:** +```bash +export SCENESCAPE_HOST="scenescape-hostname-or-ip-address" +export SCENESCAPE_TOKEN="your-api-token" # For REST API calls +export SUPASS="your-web-login-password" # For quick MQTT testing +``` + +**For Production:** +```bash +export SCENESCAPE_HOST="scenescape-hostname-or-ip-address" +export SCENESCAPE_TOKEN="your-api-token" +export MQTT_USER="dedicated-mqtt-user" # Production MQTT account +export MQTT_PASS="dedicated-mqtt-password" +``` + +--- + +## Discovering Existing ROIs and Tripwires via API + +Before subscribing to events, discover what ROIs and Tripwires exist in your scenes using the REST API. While this information is available through the SceneScape UI, the API provides complete configuration details, structured metadata, and immediate access without waiting for events. + +**Important**: Handle dynamic configuration changes in your applications. Spatial analytics may be added, removed, or modified during operation—implement periodic API checks rather than hard-coding IDs. + +### API Endpoints + +#### List All Regions +```bash +curl -k -H "Authorization: Token " \ + https://your-scenescape-instance/api/v1/regions +``` + +**Response Example:** +```json +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uid": "5908cfbe-2090-4dc9-b200-6608e2c3be86", + "name": "queue", + "points": [ + [0.40, 3.14], + [0.32, 1.85], + [2.97, 0.57], + [4.62, 2.01] + ], + "scene": "302cf49a-97ec-402d-a324-c5077b280b7b", + "buffer_size": 0.0, + "height": 1.0, + "volumetric": false + } + ] +} +``` + +#### List All Tripwires +```bash +curl -k -H "Authorization: Token " \ + https://your-scenescape-instance/api/v1/tripwires +``` + +**Response Example:** +```json +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uid": "23ae85b3-4b2c-4f38-a0bc-684b774d320c", + "name": "entry", + "points": [ + [2.62, 2.73], + [1.18, 0.26] + ], + "height": 1.0, + "scene": "302cf49a-97ec-402d-a324-c5077b280b7b" + } + ] +} +``` + +#### Get Specific Region +```bash +curl -k -H "Authorization: Token " \ + https://your-scenescape-instance/api/v1/region/{region_id} +``` + +#### Get Specific Tripwire +```bash +curl -k -H "Authorization: Token " \ + https://your-scenescape-instance/api/v1/tripwire/{tripwire_id} +``` + +--- + +## MQTT Event Topics and Data Flow + +SceneScape uses MQTT for real-time event delivery. Understanding the topic structure is crucial for building reactive applications. + +### Object Type Definitions + +Object types are defined dynamically by the class labels from input detection data (e.g., `person`, `vehicle`, `forklift`, `package`, `bicycle`, etc.). The system supports any object class without requiring pre-registration, making it flexible for diverse detection scenarios. + +**Note**: While dynamic object classification works out-of-the-box, tracking performance and accuracy can be improved by pre-defining object classes and their properties (such as expected size dimensions) in the SceneScape Object Library. This allows the system to use more accurate object models for tracking and spatial analytics calculations. + +This dynamic classification applies to all MQTT topics, event data, and API responses throughout the system. + +### Event Topics + +#### Region Events +``` +scenescape/event/region/{scene_id}/{region_id}/{event_type} +``` + +**Event Types:** +- `count` - Object count changes within the region (contains entered/exited arrays) +- `objects` - Any object changes within the region (movement, confidence updates, entry/exit) + +**Purpose**: +- **`count` events**: Trigger when objects enter or exit the region AND the count changes. Provides entry/exit notifications with count updates. +- **`objects` events**: Trigger when objects enter or exit the region (regardless of count changes). Provides entry/exit notifications with full object details. + +**Note**: Both event types typically fire together on entry/exit events. The main difference is that `objects` events may fire in edge cases where objects enter/exit but the total count remains the same (e.g., simultaneous entry and exit). For continuous positional updates of objects within regions, subscribe to `scenescape/data/region/{scene_id}/{region_id}/{object_type}` topics instead. + +#### Tripwire Events +``` +scenescape/event/tripwire/{scene_id}/{tripwire_id}/{event_type} +``` + +**Event Types:** +- `objects` - Objects crossing the tripwire + +#### Region Data Topics +``` +scenescape/data/region/{scene_id}/{region_id}/{object_type} +``` + +**Object Types**: Detected object types (see [Object Type Definitions](#object-type-definitions) above). + +**Purpose**: These topics provide continuous real-time updates for all objects currently within the region, including positional changes, confidence updates, and other dynamic properties. They act as a spatial filter to the larger scene data, delivering streaming updates only for objects inside the specific region. Unlike event topics that fire on entry/exit, these data topics stream continuously while objects remain in the region. + +**Use Case**: Subscribe to these topics when you need continuous tracking of object movement and properties within a region, rather than just entry/exit notifications. + +**Calculating Dwell Time for Active Objects**: To calculate how long an object has been in a region while it's still present, you must use these streaming data topics, not the event topics. Each object contains a `regions` field with the entry timestamp. Calculate current dwell time by subtracting the `entered` timestamp from the current time. This is essential for applications that need to detect when objects have waited too long in a region before they exit - event topics only provide dwell time after an object has already left the region. + +### Example MQTT Subscriptions + +```python +import paho.mqtt.client as mqtt + +# Subscribe to region count events (entry/exit only) for a specific scene +region_count_topic = f"scenescape/event/region/{scene_id}/+/count" +client.subscribe(region_count_topic) + +# Subscribe to tripwire events +tripwire_events_topic = f"scenescape/event/tripwire/{scene_id}/+/objects" +client.subscribe(tripwire_events_topic) + +# Subscribe to streaming data for specific object types in regions (continuous updates) +region_person_data_topic = f"scenescape/data/region/{scene_id}/{region_id}/person" +client.subscribe(region_person_data_topic) + +region_vehicle_data_topic = f"scenescape/data/region/{scene_id}/{region_id}/vehicle" +client.subscribe(region_vehicle_data_topic) + +# Subscribe to streaming data for all object types in a specific region +region_all_objects_data_topic = f"scenescape/data/region/{scene_id}/{region_id}/+" +client.subscribe(region_all_objects_data_topic) + +# Subscribe to all region count events across all scenes +all_regions_count_topic = "scenescape/event/region/+/+/count" +client.subscribe(all_regions_count_topic) + +# Subscribe to all tripwire events across all scenes +all_tripwires_topic = "scenescape/event/tripwire/+/+/objects" +client.subscribe(all_tripwires_topic) + +# Subscribe to ALL events with wildcard pattern (for debugging/exploration) +all_events_topic = "scenescape/event/+/+/+/+" +client.subscribe(all_events_topic) +``` + +--- + +## Event Data Structures + +SceneScape generates three types of events in addition to the usual streaming data available on other topics: + +1. **Region entry events** - triggered when objects enter regions (with entry timestamps) +2. **Region exit events** - triggered when objects leave regions (with dwell time calculations) +3. **Tripwire crossing events** - triggered when objects cross tripwires (with directional information) + +Each event includes object metadata and spatial context. + +### Region Event Structure + +#### Entry Event Example + +**Topic:** `scenescape/event/region/302cf49a.../5908cfbe.../count` + +> **Note**: UUIDs, coordinates, and decimal precision have been simplified for readability. Actual values will be longer and more precise. + +```json +{ + "timestamp": "2025-11-13T20:11:38.971Z", + "scene_id": "302cf49a...", + "scene_name": "Queuing", + "region_id": "5908cfbe...", + "region_name": "queue", + "counts": { + "person": 1 + }, + "objects": [ + { + "category": "person", + "confidence": 0.997, + "id": "82d54b1b...", + "type": "person", + "translation": [4.03, 1.53, 0.0], + "size": [0.5, 0.5, 1.85], + "velocity": [-1.16, 0.57, 0.0], + "rotation": [0, 0, 0, 1], + "visibility": ["atag-qcam1", "atag-qcam2"], + "regions": { + "5908cfbe...": { + "entered": "2025-11-13T20:11:38.971Z" + } + }, + "similarity": null, + "first_seen": "2025-11-13T20:11:35.839Z" + } + ], + "entered": [ + { + "category": "person", + "confidence": 0.997, + "id": "82d54b1b...", + "first_seen": "2025-11-13T20:11:35.839Z" + } + ], + "exited": [], + "metadata": { + "points": [ + [0.40, 3.14], + [0.32, 1.85], + [2.97, 0.57], + [4.62, 2.01] + ], + "title": "queue", + "uuid": "5908cfbe...", + "area": "poly", + "fromSensor": false + } +} +``` + +**Key Properties in Entry Events:** +- **`counts`**: Current object counts by type after the entry occurred +- **`entered` array**: Contains summary information about objects that just entered +- **`objects` array**: Full object details for all objects currently in the region, including the newly entered object with its `regions.{region_id}.entered` timestamp + +#### Exit Event Example + +**Topic:** `scenescape/event/region/302cf49a.../5908cfbe.../count` + +```json +{ + "timestamp": "2025-11-13T20:11:47.128Z", + "scene_id": "302cf49a...", + "scene_name": "Queuing", + "region_id": "5908cfbe...", + "region_name": "queue", + "counts": { + "person": 1 + }, + "objects": [ + { + "category": "person", + "confidence": 0.998, + "id": "a266a0be...", + "type": "person", + "translation": [2.24, 1.70, 0.0], + "size": [0.5, 0.5, 1.85], + "velocity": [-0.01, 0.07, 0.0], + "rotation": [0, 0, 0, 1], + "visibility": ["atag-qcam1", "atag-qcam2"], + "regions": { + "5908cfbe...": { + "entered": "2025-11-13T20:11:27.515Z" + } + }, + "similarity": null, + "first_seen": "2025-11-13T20:11:26.121Z" + } + ], + "entered": [], + "exited": [ + { + "object": { + "category": "person", + "confidence": 0.997, + "id": "f6f8d2a3...", + "type": "person", + "translation": [1.04, 2.30, 0.0], + "size": [0.5, 0.5, 1.85], + "velocity": [0.01, -0.02, 0.0], + "rotation": [0, 0, 0, 1], + "visibility": ["atag-qcam2"], + "regions": {}, + "similarity": null, + "first_seen": "2025-11-13T20:11:26.121Z" + }, + "dwell": 19.6 + } + ], + "metadata": { + "points": [ + [0.40, 3.14], + [0.32, 1.85], + [2.97, 0.57], + [4.62, 2.01] + ], + "title": "queue", + "uuid": "5908cfbe...", + "area": "poly", + "fromSensor": false + } +} +``` + +**Key Properties in Exit Events:** +- **`counts`**: Current object counts by type after the exit occurred +- **`exited` array**: Contains critical `dwell` time data - how long each object spent in the region (essential for situational awareness applications like queue monitoring, loitering detection, and process timing analysis) +- **`objects` array**: Full details for objects still remaining in the region after the exit + +### Tripwire Event Structure + +**Topic:** `scenescape/event/tripwire/302cf49a.../23ae85b3.../objects` + +```json +{ + "timestamp": "2025-11-12T21:03:14.318Z", + "scene_id": "302cf49a...", + "scene_name": "Queuing", + "tripwire_id": "23ae85b3...", + "tripwire_name": "entry", + "counts": { + "person": 1 + }, + "objects": [ + { + "category": "person", + "confidence": 0.743, + "id": "63684acf...", + "type": "person", + "translation": [1.77, 1.23, 0.0], + "size": [0.5, 0.5, 1.85], + "velocity": [0.36, -0.32, 0.0], + "rotation": [0, 0, 0, 1], + "visibility": ["atag-qcam1"], + "regions": { + "5908cfbe...": { + "entered": "2025-11-12T21:02:57.228Z" + } + }, + "similarity": null, + "first_seen": "2025-11-12T21:02:53.720Z", + "direction": -1 + } + ], + "entered": [], + "exited": [], + "metadata": { + "title": "entry", + "points": [ + [2.62, 2.73], + [1.18, 0.26] + ], + "uuid": "23ae85b3..." + } +} +``` + +**Key Properties in Tripwire Events:** +- **`direction` field**: Critical directional indicator (+1 or -1) showing which way each individual object crossed the tripwire relative to the configured directional flag - essential for counting applications, access control, and flow analysis. Each object in the `objects` array has its own direction field (always +1 or -1) +- **`objects` array**: Contains full object details at the moment of crossing, including position, velocity, and confidence +- **`counts`**: Number of objects crossing in this event - almost always 1 (single object crossing), except in rare cases where multiple objects cross simultaneously + +### Event Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | string | ISO 8601 timestamp of the original data frame or sensor input when the object interaction occurred (not when the event was detected or processed) | +| `scene_id` | string | UUID of the scene containing the region/tripwire | +| `scene_name` | string | Human-readable scene name | +| `region_id` / `tripwire_id` | string | UUID of the region or tripwire | +| `region_name` / `tripwire_name` | string | Human-readable region or tripwire name | +| `counts` | object | Current object counts by category | +| `objects` | array | Objects currently in region or crossing tripwire | +| `entered` | array | Objects that entered the region (ROI events only) | +| `exited` | array | Objects that exited the region (ROI events only); includes `object` details and `dwell` time in seconds | +| `metadata` | object | Region/tripwire configuration data | +| `dwell` | number | Time in seconds that an object spent in the region (only in exited events) | +| `id` | string | Object identifier (within object data) | +| `category` / `type` | string | Object classification (person, vehicle, etc.) | +| `confidence` | number | Detection confidence (0.0 - 1.0) | +| `translation` | array | 3D world coordinates [x, y, z] in meters | +| `velocity` | array | Velocity vector [vx, vy, vz] in meters per second | +| `visibility` | array | List of sensors that can detect this object | +| `regions` | object | Region membership and entry times | +| `direction` | number | Crossing direction for tripwire events (-1 or 1) | + +--- + +## Code Examples + +**Prerequisites:** Before running these examples, create at least one region and one tripwire using the SceneScape web interface. In your SceneScape deployment, select a scene and use the Regions and Tripwires tabs to draw spatial analytics elements. The examples below will discover and monitor these configured elements. + +### Prerequisites + +**Ubuntu Setup:** +```bash +sudo apt update && sudo apt install python3-requests python3-paho-mqtt +``` + +**Alternative (using virtual environment):** +```bash +sudo apt update && sudo apt install python3-full +python3 -m venv scenescape-env +source scenescape-env/bin/activate +pip install requests paho-mqtt +``` + +**Environment Variables:** +```bash +export SCENESCAPE_HOST="scenescape-hostname-or-ip-address" +export SCENESCAPE_TOKEN="your-api-token" # Found in SceneScape Admin panel > Tokens (admin or scenectrl user) +export SUPASS="your-web-login-password" +``` + +### Step 1: Discover Your Regions and Tripwires + +**Save as:** `discover.py` + +```python +#!/usr/bin/env python3 +import os, requests, json +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +host = os.environ['SCENESCAPE_HOST'] +token = os.environ['SCENESCAPE_TOKEN'] +headers = {'Authorization': f'Token {token}'} + +print("Discovering regions...") +regions = requests.get(f'https://{host}/api/v1/regions', headers=headers, verify=False).json() +for r in regions['results']: + print(f" {r['name']} ({r['uid']})") + +print("\nDiscovering tripwires...") +tripwires = requests.get(f'https://{host}/api/v1/tripwires', headers=headers, verify=False).json() +for t in tripwires['results']: + print(f" {t['name']} ({t['uid']})") +``` + +**Run:** `python3 discover.py` + +### Step 2: Listen to Live Events + +**Save as:** `listen.py` + +```python +#!/usr/bin/env python3 +import os, json, ssl +import paho.mqtt.client as mqtt + +def on_connect(client, userdata, flags, rc): + if rc == 0: + print("Connected! Listening for events...") + client.subscribe("scenescape/event/+/+/+/+") + else: + print(f"Connection failed: {rc}") + +def on_message(client, userdata, msg): + try: + event = json.loads(msg.payload.decode()) + topic_parts = msg.topic.split('/') + + if topic_parts[2] == "region": + region_name = event.get('region_name') + counts = event.get('counts', {}) + + print(f"Region '{region_name}': {counts}") + if event.get('entered'): + print(f" → {len(event['entered'])} entered") + if event.get('exited'): + for exit_info in event['exited']: + dwell = exit_info.get('dwell', 0) + print(f" ← exited (dwell: {dwell:.1f}s)") + + elif topic_parts[2] == "tripwire": + tripwire_name = event.get('tripwire_name') + for obj in event.get('objects', []): + direction = "→" if obj.get('direction', 0) > 0 else "←" + print(f"Tripwire '{tripwire_name}': {obj.get('category')} {direction}") + + except Exception as e: + print(f"Error: {e}") + +client = mqtt.Client(transport="websockets") +client.username_pw_set("admin", os.environ['SUPASS']) + +ssl_context = ssl.create_default_context() +ssl_context.check_hostname = False +ssl_context.verify_mode = ssl.CERT_NONE +client.tls_set_context(ssl_context) + +client.on_connect = on_connect +client.on_message = on_message + +client.ws_set_options(path="/mqtt") +client.connect(os.environ['SCENESCAPE_HOST'], 443, 60) +client.loop_forever() +``` + +**Run:** `python3 listen.py` + +### Step 3: JavaScript Web Example + +**Save as:** `index.html` + +```html + + + + + SceneScape Events + + + +

SceneScape Live Events

+
Connecting...
+
+ + + + +``` + +**Run:** `python3 -m http.server 8000` then open http://:8000 + +**Important:** Replace `YOUR_SCENESCAPE_HOST` and `YOUR_SUPASS` with your actual values: +- **Host**: Use `localhost` only if your browser and SceneScape are running on the same system, otherwise use the actual hostname or IP address of your SceneScape deployment +- **Password**: Use your SceneScape web interface login password (same as the `SUPASS` environment variable) + +These three simple scripts provide a complete foundation for working with SceneScape spatial analytics data. The tutorial emphasizes immediate testability with minimal setup requirements. + +### Direct MQTT Access (Alternative to WebSockets) + +For applications that need direct MQTT access instead of WebSockets, additional configuration is required: + +**Docker Compose Setup:** +In `docker-compose.yml`, uncomment the broker ports section: +```yaml + broker: + image: eclipse-mosquitto:2.0.22 + ports: + - "1883:1883" # Uncomment this line + # ... rest of broker config +``` + +**Kubernetes Setup:** +Direct MQTT access is configured via NodePort service. Check `kubernetes/scenescape-chart/values.yaml`: +```yaml +mqttService: + nodePort: + enabled: true + nodePort: 31883 # External port for MQTT access +``` + +**MQTT Credentials:** +Use the generated MQTT credentials instead of web login credentials: +```bash +# Read MQTT credentials from secrets file +export MQTT_USER=$(jq -r '.user' manager/secrets/controller.auth) +export MQTT_PASS=$(jq -r '.password' manager/secrets/controller.auth) +``` + +**Python Example for Direct MQTT:** +```python +import os, ssl +import paho.mqtt.client as mqtt + +def on_connect(client, userdata, flags, rc): + if rc == 0: + print("Connected to direct MQTT!") + client.subscribe("scenescape/event/+/+/+/+") + else: + print(f"Connection failed: {rc}") + +def on_message(client, userdata, msg): + # Process events here + print(f"Topic: {msg.topic}") + print(f"Message: {msg.payload.decode()}") + +# Use dedicated MQTT credentials (not admin/SUPASS) +client = mqtt.Client() +client.username_pw_set(os.environ['MQTT_USER'], os.environ['MQTT_PASS']) + +# Configure TLS +ssl_context = ssl.create_default_context() +ssl_context.check_hostname = False +ssl_context.verify_mode = ssl.CERT_NONE +client.tls_set_context(ssl_context) + +client.on_connect = on_connect +client.on_message = on_message + +# Direct MQTT connection with TLS +host = os.environ['SCENESCAPE_HOST'] +client.connect(host, 1883, 60) +client.loop_forever() +``` + +**Environment Setup for Direct MQTT:** +```bash +export SCENESCAPE_HOST="scenescape-hostname-or-ip" # No https:// prefix +export MQTT_USER="dedicated-mqtt-user" +export MQTT_PASS="dedicated-mqtt-password" +``` + +**Note:** WebSocket MQTT works out-of-the-box with standard HTTPS port 443, while direct MQTT requires exposing additional ports. + +--- + +## Conclusion + +SceneScape's spatial analytics provide a powerful abstraction that separates monitoring logic from individual sensor perspectives. By defining regions and tripwires at the scene level using world coordinates, your applications gain a critical advantage: **sensor independence**. + +This architecture means your spatial analytics logic—the regions you define, the business rules you implement, and the applications you build—remain completely unchanged even as your sensor infrastructure evolves. Whether you add new cameras, upgrade to different sensor technologies, or reconfigure your monitoring setup, your ROIs and tripwires continue working seamlessly. + +**Key Benefits:** +- **Future-proof applications**: Analytics logic survives sensor changes and infrastructure upgrades +- **Unified monitoring**: Single API and event stream regardless of underlying sensor types or count +- **Simplified maintenance**: Manage spatial analytics once at the scene level, not per-sensor + +**Getting Started:** +1. Run the tutorial examples (`discover.py`, `listen.py`, `index.html`) +2. Define regions and tripwires that match your monitoring needs +3. Build applications using the REST API and MQTT event streams +4. Scale and adapt your sensor infrastructure independently of your analytics logic + +This sensor-agnostic approach ensures your investment in spatial analytics applications provides long-term value, adapting to new technologies while maintaining consistent monitoring capabilities. diff --git a/kubernetes/Makefile b/kubernetes/Makefile index 35f111447..b79d83169 100644 --- a/kubernetes/Makefile +++ b/kubernetes/Makefile @@ -1,13 +1,16 @@ # SPDX-FileCopyrightText: (C) 2023 - 2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -IMAGE=scenescape-manager +MANAGERIMAGE=scenescape-manager CAMCALIBIMAGE=scenescape-camcalibration CONTROLLERIMAGE=scenescape-controller +# optional image for cluster analytics component +CLUSTERANALYTICSIMAGE=scenescape-cluster-analytics + VERSION:=$(shell cat ../version.txt) NAMESPACE=scenescape WORKINGDIR=$(shell dirname $(shell pwd)) -ORGANIZATION?= +ORGANIZATION?=intel VALIDATION= VALIDATION_FLAG= DEPLOYMENT_TEST= @@ -39,22 +42,14 @@ CHART_DEBUG?=0 # they will be automatically passed to the Helm chart during installation # Chart debug: set CHART_DEBUG=1 to enable chartdebug=true in Helm deployment -# ITEP constants -# REGISTRY=registry.test-public-maestro.edgeorch.net/scenescape -CHARTREPO=registry.test-public-maestro.edgeorch.net/chartrepo/scenescape -# HELM_REPO_USERNAME= -# HELM_REPO_PASSWORD= - # start kind, then install SceneScape with helm -default: install-deps clean-kind kind build-all-tests install +default: install-deps clean-kind kind build-all install # publish to ITEP by building, packaging, then pushing -# must set appropriate REGISTRY, CHARTREPO, HELM_REPO_USERNAME and HELM_REPO_PASSWORD constants -build-and-package-all: build-all install-package-deps package +build-and-package: build-all package -# build init-images, scenescape images and push everything to a registry, then generate Chart.yaml -build-all-tests: build-all -build-all: build push chart.yaml +# build scenescape images and push everything to a registry, then generate Chart.yaml +build-all: build push kind: generate-kind-yaml start-kind install-cert-manager @@ -144,28 +139,24 @@ build: make -C .. CERTPASS=$(CERTPASS) FORCE_VAAPI=$(FORCE_VAAPI) push: - docker tag $(ORGANIZATION)$(IMAGE):$(VERSION) $(REGISTRY)/$(IMAGE):$(VERSION) - docker push $(REGISTRY)/$(IMAGE):$(VERSION) - docker tag $(ORGANIZATION)$(CAMCALIBIMAGE):$(VERSION) $(REGISTRY)/$(CAMCALIBIMAGE):$(VERSION) - docker push $(REGISTRY)/$(CAMCALIBIMAGE):$(VERSION) - docker tag $(ORGANIZATION)$(CONTROLLERIMAGE):$(VERSION) $(REGISTRY)/$(CONTROLLERIMAGE):$(VERSION) - docker push $(REGISTRY)/$(CONTROLLERIMAGE):$(VERSION) + docker tag $(MANAGERIMAGE):$(VERSION) $(REGISTRY)/$(ORGANIZATION)/$(MANAGERIMAGE):$(VERSION) + docker push $(REGISTRY)/$(ORGANIZATION)/$(MANAGERIMAGE):$(VERSION) + docker tag $(CAMCALIBIMAGE):$(VERSION) $(REGISTRY)/$(ORGANIZATION)/$(CAMCALIBIMAGE):$(VERSION) + docker push $(REGISTRY)/$(ORGANIZATION)/$(CAMCALIBIMAGE):$(VERSION) + docker tag $(CONTROLLERIMAGE):$(VERSION) $(REGISTRY)/$(ORGANIZATION)/$(CONTROLLERIMAGE):$(VERSION) + docker push $(REGISTRY)/$(ORGANIZATION)/$(CONTROLLERIMAGE):$(VERSION) + +push-all: push + docker tag $(CLUSTERANALYTICSIMAGE):$(VERSION) $(REGISTRY)/$(ORGANIZATION)/$(CLUSTERANALYTICSIMAGE):$(VERSION) + docker push $(REGISTRY)/$(ORGANIZATION)/$(CLUSTERANALYTICSIMAGE):$(VERSION) # generate Chart.yaml with appropriate version.txt chart.yaml: sed -e "s|{VERSION}|$(VERSION)|g" template/Chart.template > scenescape-chart/Chart.yaml -# packaging dependencies -install-package-deps: - helm plugin install https://github.com/chartmuseum/helm-push || true - helm repo add itep_harbor https://$(CHARTREPO) - # packages and pushes the helm chart -# must set HELM_REPO_USERNAME and HELM_REPO_PASSWORD variables -package: copy-files - PACKAGE=$$(helm package scenescape-chart/ | awk '{print $$NF}'); \ - helm cm-push $$PACKAGE itep_harbor -u=$(HELM_REPO_USERNAME) -p=$(HELM_REPO_PASSWORD); \ - rm $$PACKAGE +package: copy-files chart.yaml + helm package scenescape-chart/ # Query what's in the registry (local) list-registry: @@ -220,7 +211,8 @@ install: copy-files if [ "$(DEPLOYMENT_TEST)" = "1" ]; then \ docker system prune --all --force --volumes; \ fi; \ - helm upgrade $(RELEASE) --install scenescape-chart/ -n $(NAMESPACE) --create-namespace --timeout $(INSTALL_TIMEOUT) $${DEBUG_ARGS} $${SUPASS_ARG} $${PGPASS_ARG} $${VALUES_FILE} $(VALIDATION_FLAG) $(TEST_FLAGS); + REPOSITORY_ARG="--set repository=$(REGISTRY)"; \ + helm upgrade $(RELEASE) --install scenescape-chart/ -n $(NAMESPACE) --create-namespace --timeout $(INSTALL_TIMEOUT) $${DEBUG_ARGS} $${REPOSITORY_ARG} $${SUPASS_ARG} $${PGPASS_ARG} $${VALUES_FILE} $(VALIDATION_FLAG) $(TEST_FLAGS); @if [ -f "/tmp/scenescape-proxy-values.yaml" ]; then \ rm -f /tmp/scenescape-proxy-values.yaml; \ fi diff --git a/kubernetes/README.md b/kubernetes/README.md index 46298d32f..13836e626 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -1,7 +1,5 @@ # Intel® SceneScape on Kubernetes -Intel® SceneScape Kubernetes helm chart - ## Overview This folder contains the helm chart to run Intel® SceneScape on Kubernetes. @@ -10,9 +8,14 @@ This readme goes through a minimal setup for running this on your local developm Advanced users intending to deploy this in production will have to change the default chart values or modify the templates. -## Quick start +There are 2 main ways to install Intel® SceneScape on Kubernetes: + +1. [All-in-one (Kind + Registry + SceneScape)](#all-in-one) - for development and testing using locally built SceneScape images +2. [SceneScape only](#scenescape-only) - for existing clusters and published SceneScape images + +## All-in-one -The easiest way to install Intel® SceneScape on Kubernetes is with a single command: +The easiest way to install Intel® SceneScape on [Kind](https://kind.sigs.k8s.io/) is with a single command: ```sh KUBERNETES=1 ./deploy.sh @@ -21,6 +24,7 @@ KUBERNETES=1 ./deploy.sh This will: - Start a kind cluster and local registry (if needed) +- Install [Cert Manager](https://cert-manager.io/) - Build and push all required images - Autogenerate a strong admin UI password (SUPASS) - Deploy Intel® SceneScape to the cluster using Helm @@ -30,83 +34,46 @@ When the webUI is up, log in with `admin` and the autogenerated password. By def > **Note:** You can retrieve the generated admin password at any time with: > > ```sh -> helm get values scenescape-release-1 -n scenescape +> helm get values scenescape -n scenescape > ``` Save this password for future logins. You can change the admin password later via the web UI after logging in. -## Advanced Installation Options - -### 1. Using Makefile Targets Directly - -You can use the Makefile targets for more control, automation, or development. This approach lets you run each step individually or as a sequence. +### Useful targets -#### Recommended workflow - -#### 1. **Set custom passwords:** - -Set the `SUPASS` environment variable before running the `install` target to specify your own admin password for web application: - -```sh -export SUPASS=your_custom_password -``` - -Set the `PGPASS` environment variable before running the `install` target to specify your own admin password for postgres database: +- Install SceneScape: `make -C kubernetes install` +- Uninstall (leave kind cluster running): `make -C kubernetes uninstall` +- Remove all: `make -C kubernetes clean-all` -```sh -export PGPASS=your_custom_password -``` +## SceneScape Only -**Important:** If you omit setting these passwords, installation will fail. +If you already have a Kubernetes cluster you can use the Helm chart directly. -**How to generate strong passwords:** +**Prerequisites:** -```sh -export SUPASS=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9!@#$%^&*()_+-=[]{}|;:,.<>?/~' | head -c 24) -export PGPASS=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9!@#$%^&*()_+-=[]{}|;:,.<>?/~' | head -c 16) -``` +Install [Cert Manager](https://cert-manager.io/) in your cluster. -#### 2. **Deploy Scenescape:** +**Install with a custom admin password:** ```sh -make -C kubernetes install-deps clean-kind kind build-all install +helm install scenescape scenescape-chart -n --create-namespace \ + --set supass= \ + --set pgserver.password= ``` -This will: - -- Install required dependencies (`kind`, `kubectl`, `k9s`, `helm`) -- Remove any previous kind cluster and registry -- Start a new kind cluster and local registry -- Build and push all required images to the local registry -- Deploy Intel® SceneScape to the cluster using Helm - -**Other useful targets:** - -- Stop: `make -C kubernetes uninstall` -- Remove all: `make -C kubernetes clean-all` - ---- - -### 2. Using the Helm Chart Directly (For Existing Kubernetes Clusters) - -If you already have a Kubernetes cluster and want to deploy Intel® SceneScape without the Makefile or `deploy.sh`, you can use the Helm chart directly. - -**Install with a custom admin password:** +Optionally, prepare updated [values file](scenescape-chart/values.yaml) and save it as `values-custom.yaml`. ```sh -helm install scenescape-release-1 scenescape-chart -n scenescape --create-namespace \ - --set supass=your_custom_password \ - --set pgserver.password=your_custom_password +helm install scenescape scenescape-chart -n --create-namespace \ + --set supass= \ + --set pgserver.password= \ + --values values-custom.yaml ``` -- The `supass` value sets the admin password for the web UI. **If you do not set `supass`, installation will fail.** -- The `pgserver.password` value sets the admin password for the Postgres database. **If you do not set `pgserver.password`, installation will fail.** -- You can set other values with `--set` or a custom `values.yaml` file. - **To uninstall:** ```sh -helm uninstall scenescape-release-1 -n scenescape +helm uninstall scenescape -n ``` ## Environment Variables @@ -135,6 +102,15 @@ These values ensure that all internal cluster communication, including between p The proxy settings will be automatically detected and passed to all Intel® SceneScape containers as environment variables. +### NodePort Services + +By default, Intel® SceneScape exposes its services using ClusterIP type services. If you want to expose them using NodePort services instead, set the following chart value: + +```yaml +nodePort: + enabled: true +``` + ### Chart Debug Mode To enable Helm chart debugging (useful for troubleshooting deployment issues): @@ -156,51 +132,3 @@ make -C kubernetes install ``` This enables additional testing components and configurations. - -## Detailed steps and explanation - -Run from the project directory (e.g. ~/scenescape) - -1. Start up a kind cluster and a local registry. - ```console - $ make -C kubernetes kind - ``` - This uses the template files in kubernetes/template and generates yaml files for kind cluster configuration. It then starts up a registry container, a kind cluster container and adds them to the same Docker network so they can communicate. Run `generate-kind-yaml` and `start-kind` targets separately if you want to keep your edited yaml files. - Leave the kind cluster running or omit this step if you have your own cluster and registry ready. -2. Build Intel® SceneScape images and init-images, then push everything to the local registry. - ```console - $ make -C kubernetes build-all - ``` -3. Install the Intel® SceneScape release with helm. - ```console - $ make -C kubernetes install - ``` -4. Verify that Intel® SceneScape is running. - ```console - kubectl get pods -n scenescape -w - # alternative TUI - k9s - ``` -5. Uninstall the Intel® SceneScape release. - ```console - $ make -C kubernetes uninstall - ``` - -### Additional notes - -- Additionally, to remove the kind cluster, use the `clean-kind` target. The kind registry isn't removed so the images are cached if you wish to pull from it again. To also remove the kind registry, use the `clean-kind-registry` target. -- Use the `clean-all` target to remove all containers. -- **WARNING: Intel® SceneScape data isn't persisted, uninstalling the release will lead to data loss.** -- **NON-SUDO USERS**: The `default` target will run the `install-deps` target to ensure that the `kind`, `kubectl`, `k9s` and `helm` binaries are available and install them to /usr/local/bin with sudo. If your user does not have sudo access, check the comments for the `install-deps` target and edit the Makefile accordingly. - -## FAQ - -- How do I verify that everything is working properly? - Run `k9s` and check that the Intel® SceneScape pods are `Ready` and in the `Running` status. If they're stuck in an error status, refer to the steps in Troubleshooting. - -## Troubleshooting - -- If the scene controller does not seem to be running (no dots moving in the scene), restart the scene deployment. -- If your pods can't pull the images, check to see whether the registry container is on the same docker network as the kind cluster container. - Troubleshoot by running `docker inspect kind`. If they are not, run `docker network connect "kind" "kind-registry"`. -- If you can't access the Intel® SceneScape webUI, make sure Intel® SceneScape on Docker isn't running. diff --git a/kubernetes/scenescape-chart/Chart.yaml b/kubernetes/scenescape-chart/Chart.yaml index 1c571ca26..d714f45cf 100644 --- a/kubernetes/scenescape-chart/Chart.yaml +++ b/kubernetes/scenescape-chart/Chart.yaml @@ -6,4 +6,4 @@ name: scenescape-chart description: A Helm chart for SceneScape type: application version: 1.2.1 -appVersion: "1.5.0-dev" +appVersion: "2025.2-rc1" diff --git a/kubernetes/scenescape-chart/README.md b/kubernetes/scenescape-chart/README.md index 816427671..801ab0af3 100644 --- a/kubernetes/scenescape-chart/README.md +++ b/kubernetes/scenescape-chart/README.md @@ -2,72 +2,6 @@ Scene-based AI software framework. -## Overview +## Docs -Intel® SceneScape is a software framework that enables spatial awareness by integrating data from cameras and other sensors into scenes. It simplifies application development by providing near-real-time, actionable data about the state of the scene, including what, when, and where objects are present, along with their sensed attributes and environment. This scene-based approach makes it easy to incorporate and fuse sensor inputs, enabling analysis of past events, monitoring of current activities, and prediction of future outcomes from scene data. - -Even with a single camera, transitioning to a scene paradigm offers significant advantages. Applications are written against the scene data directly, allowing for flexibility in modifying the sensor setup. You can move, modify, remove, or add cameras and sensors without changing your application or business logic. As you enhance your sensor array, the data quality improves, leading to better insights and decisions without altering your underlying application logic. - -Intel® SceneScape turns raw sensor data into actionable insights by representing objects, people, and vehicles within a scene. Applications can access this information to make informed decisions, such as identifying safety hazards, detecting equipment issues, managing queues, correcting product placements, or responding to emergencies. - -## How It Works - -Intel® SceneScape uses advanced AI algorithms and hardware to process data from cameras and sensors, maintaining a dynamic scene graph that includes 3D spatial information and time-based changes. This enables developers to write applications that interact with a digital version of the environment in near real-time, allowing for responsive and adaptive application behavior based on the latest sensor data. - -The framework leverages the Intel® Distribution of OpenVINO™ toolkit to efficiently handle sensor data, enabling developers to write applications that can be deployed across various Intel® hardware accelerators like CPUs, GPUs, VPUs, FPGAs, and GNAs. This ensures optimized performance and scalability. - -A key goal of Intel® SceneScape is to make writing applications and business logic faster, simpler, and easier. By defining each scene with a fixed local coordinate system, spatial context is provided to sensor data. Scenes can represent various environments, such as buildings, ships, aircraft, or campuses, and can be linked to a global geographical coordinate system if needed. Intel® SceneScape manages: - -- Multiple scenes, each with its own coordinate system. -- A single parent scene for each sensor at any given time. -- The precise location and orientation of cameras and sensors within the scene, stored in the Intel® SceneScape database. This information is crucial for interpreting sensor data correctly. -- Compatibility with glTF scene graph representations. - -Intel® SceneScape is built on a collection of containerized services that work together to deliver comprehensive functionality, ensuring seamless integration and operation. - -![SceneScape architecture diagram](https://github.com/open-edge-platform/scenescape/blob/main/docs/user-guide/images/architecture.png) -Figure 1: Architecture Diagram - -### Scene controller - -System which maintains the current state of the scene, including tracked objects, cameras, and sensors. - -### DLStreamer Pipeline Server - -Deep Learning Streamer Pipeline Server (DL Streamer Pipeline Server) is a Python-based, interoperable containerized microservice for easy development and deployment of video analytics pipelines. It is built on top of GStreamer and Deep Learning Streamer (DL Streamer) , providing video ingestion and deep learning inferencing functionalities. - -### Auto Camera Calibration - -Computes camera parameters utilizing known priors and camera feed. - -### MQTT broker - -Mosquitto MQTT broker which acts as the primary message bus connecting sensors, internal components, and applications, including the web interface. - -### Web server - -Apache web server providing a Django-based web UI which allows users to view updates to the scene graph and manage scenes, cameras, sensors, and analytics. It also serves the Intel® SceneScape REST API. - -### NTP server - -Time server which maintains the reference clock and keeps clients in sync. - -### SQL database - -PostgreSQL database server which stores static information used by the web UI and the scene controller. No video or object location data is stored by Intel® SceneScape. - -## Configuration - -### Proxy Settings - -If you're deploying Intel® SceneScape in an environment that requires proxy access to external resources, use the following best-practice values for `noProxy`: - -```yaml -httpProxy: "http://your-proxy-server:port" -httpsProxy: "https://your-proxy-server:port" -noProxy: "localhost,127.0.0.1,.local,.svc,.svc.cluster.local,10.96.0.0/12,10.244.0.0/16,172.17.0.0/16" -``` - -For a detailed explanation of what to put in `no_proxy` and why, see the [Proxy Configuration section in the top-level README](../README.md#proxy-configuration). - -These settings will be applied to all Intel® SceneScape containers as environment variables, enabling them to access external resources through your corporate proxy. +See the [Documentation](https://docs.openedgeplatform.intel.com/2025.1/scenescape/index.html) for more information. diff --git a/kubernetes/scenescape-chart/templates/broker/configmap.yaml b/kubernetes/scenescape-chart/templates/broker/configmap.yaml index 013edc78e..0a1fbc821 100644 --- a/kubernetes/scenescape-chart/templates/broker/configmap.yaml +++ b/kubernetes/scenescape-chart/templates/broker/configmap.yaml @@ -6,7 +6,6 @@ apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-broker - namespace: {{ .Release.Namespace }} data: mosquitto-secure.conf: | {{ .Files.Get "files/broker/mosquitto-secure.conf" | indent 4 }} diff --git a/kubernetes/scenescape-chart/templates/broker/deployment.yaml b/kubernetes/scenescape-chart/templates/broker/deployment.yaml index 0074ea0fc..8a81ed3b4 100644 --- a/kubernetes/scenescape-chart/templates/broker/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/broker/deployment.yaml @@ -6,7 +6,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-broker - namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-broker spec: @@ -25,6 +24,7 @@ spec: containers: - name: broker image: {{ .Values.broker.image }}:{{ .Values.broker.tag }} + imagePullPolicy: {{ .Values.broker.pullPolicy }} env: {{ include "proxy_envs" . | indent 12 }} ports: diff --git a/kubernetes/scenescape-chart/templates/camcalibration/deployment.yaml b/kubernetes/scenescape-chart/templates/camcalibration/deployment.yaml index 126b4d6e9..3cac35ca7 100644 --- a/kubernetes/scenescape-chart/templates/camcalibration/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/camcalibration/deployment.yaml @@ -24,14 +24,16 @@ spec: runAsGroup: 1000 initContainers: - name: wait-for-web-initcontainer - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} command: ["/bin/sh", "-c", "until wget -q --spider http://web.{{ .Release.Namespace }}.svc.cluster.local; do sleep 1; done"] securityContext: runAsUser: 1000 runAsGroup: 1000 {{ include "defaultContainerSecurityContext" . | indent 12 }} - name: fix-permissions-initcontainer - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} command: ["/bin/sh", "-c", "chown -R 1000:1000 /data/media /data/datasets"] securityContext: runAsUser: 0 @@ -54,6 +56,7 @@ spec: - --ssl-keyfile - /run/secrets/certs/scenescape-camcalibration.key image: {{ .Values.repository }}/{{ .Values.camcalibration.image }}:{{ .Chart.AppVersion }} + imagePullPolicy: {{ .Values.camcalibration.pullPolicy }} name: {{ .Release.Name }}-camcalibration env: - name: EGL_PLATFORM @@ -63,7 +66,6 @@ spec: {{ include "proxy_envs" . | indent 10 }} ports: - containerPort: 8443 - imagePullPolicy: Always securityContext: runAsUser: 1000 runAsGroup: 1000 diff --git a/kubernetes/scenescape-chart/templates/camcalibration/pvc.yaml b/kubernetes/scenescape-chart/templates/camcalibration/pvc.yaml index a37d669fb..a24ec8eec 100644 --- a/kubernetes/scenescape-chart/templates/camcalibration/pvc.yaml +++ b/kubernetes/scenescape-chart/templates/camcalibration/pvc.yaml @@ -6,7 +6,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-datasets-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" diff --git a/kubernetes/scenescape-chart/templates/cluster-analytics/deployment.yaml b/kubernetes/scenescape-chart/templates/cluster-analytics/deployment.yaml new file mode 100644 index 000000000..1b4870084 --- /dev/null +++ b/kubernetes/scenescape-chart/templates/cluster-analytics/deployment.yaml @@ -0,0 +1,72 @@ + +# SPDX-FileCopyrightText: (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +--- +{{- if .Values.clusterAnalytics.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-cluster-analytics + labels: + app: {{ .Release.Name }}-cluster-analytics +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Release.Name }}-cluster-analytics + template: + metadata: + labels: + app: {{ .Release.Name }}-cluster-analytics + spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + containers: + - name: cluster-analytics + image: {{ .Values.repository }}/{{ .Values.clusterAnalytics.image }}:{{ .Chart.AppVersion }} + pullPolicy: {{ .Values.clusterAnalytics.pullPolicy }} + args: + - "--broker" + - "broker.{{ .Release.Namespace }}.svc.cluster.local" + - "--brokerauth" + - "/run/secrets/controller.auth" + {{- if .Values.clusterAnalytics.webUI.enabled }} + - "--webui" + - "--webui-certfile" + - "/run/secrets/certs/scenescape-web.crt" + - "--webui-keyfile" + - "/run/secrets/certs/scenescape-web.key" + ports: + - containerPort: 9443 + {{- end }} + securityContext: + {{ include "defaultContainerSecurityContext" . | indent 12 }} + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: certs + mountPath: /run/secrets/certs/scenescape-ca.pem + subPath: scenescape-ca.pem + readOnly: true + - name: controller-auth + mountPath: /run/secrets/controller.auth + subPath: controller.auth + readOnly: true + {{- if .Values.clusterAnalytics.webUI.enabled }} + - name: certs + mountPath: /run/secrets/certs/scenescape-web.crt + subPath: scenescape-web.crt + readOnly: true + - name: certs + mountPath: /run/secrets/certs/scenescape-web.key + subPath: scenescape-web.key + readOnly: true + {{- end }} + volumes: + - name: controller-auth + secret: + secretName: {{ .Release.Name }}-controller.auth + {{- include "certs_volume" . | nindent 8 }} +{{- end }} diff --git a/kubernetes/scenescape-chart/templates/cluster-analytics/service.yaml b/kubernetes/scenescape-chart/templates/cluster-analytics/service.yaml new file mode 100644 index 000000000..408fba12d --- /dev/null +++ b/kubernetes/scenescape-chart/templates/cluster-analytics/service.yaml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +--- +{{- if .Values.clusterAnalytics.enabled }} +{{- if .Values.clusterAnalytics.webUI.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: cluster-analytics + annotations: {{- toYaml .Values.service.annotations | nindent 4 }} +spec: + selector: + app: {{ .Release.Name }}-cluster-analytics + ports: + - name: "9443" + protocol: TCP + port: 9443 + targetPort: 9443 +{{- if .Values.loadBalancer.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: cluster-analytics-lb + annotations: {{- toYaml .Values.loadBalancer.annotations | nindent 4 }} +spec: + type: LoadBalancer + selector: + app: {{ .Release.Name }}-cluster-analytics + ports: + - name: "9443" + protocol: TCP + port: 9443 + targetPort: 9443 + externalTrafficPolicy: {{ .Values.loadBalancer.externalTrafficPolicy }} + {{- if (default .Values.loadBalancer.loadBalancerIP false) }} + loadBalancerIP: {{ .Values.loadBalancer.loadBalancerIP }} + {{- end }} +{{- end }} +{{- if .Values.nodePort.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: cluster-analytics-np + annotations: {{- toYaml .Values.nodePort.annotations | nindent 4 }} +spec: + type: NodePort + selector: + app: {{ .Release.Name }}-cluster-analytics + ports: + - name: "9443" + protocol: TCP + port: 9443 +{{- end }} +{{- end }} +{{- end }} diff --git a/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/queuing-deployment.yaml b/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/queuing-deployment.yaml index ddcea1500..38c8307c8 100644 --- a/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/queuing-deployment.yaml +++ b/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/queuing-deployment.yaml @@ -6,7 +6,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-queuing-cams - namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-queuing-cams spec: @@ -24,14 +23,15 @@ spec: runAsGroup: 0 initContainers: - name: wait-for-rtsp - image: curlimages/curl:latest + image: {{ .Values.dlspsWaitImage.repository }}/{{ .Values.dlspsWaitImage.name }}:{{ .Values.dlspsWaitImage.tag }} + imagePullPolicy: {{ .Values.dlspsWaitImage.pullPolicy }} command: ["sh", "-c", "until nc -z mediaserver 8554; do echo waiting for mediaserver; sleep 2; done"] securityContext: {{ include "defaultContainerSecurityContext" . | indent 10 }} containers: - name: queuing-cams image: {{ .Values.queuing.cameras.image }}:{{ .Values.queuing.cameras.tag }} - imagePullPolicy: IfNotPresent + imagePullPolicy: {{.Values.queuing.cameras.pullPolicy }} args: ["-nostdin", "-re", "-stream_loop", "-1", "-i", "/data/{{ index .Values.queuing.cameras.files 0 }}", "-re", "-stream_loop", "-1", "-i", "/data/{{ index .Values.queuing.cameras.files 1 }}", "-map", "0:v", "-c", "copy", "-f", "rtsp", "-rtsp_transport", "tcp", "rtsp://mediaserver:8554/queuing-cam1", "-map", "1:v", "-c", "copy", "-f", "rtsp", "-rtsp_transport", "tcp", "rtsp://mediaserver:8554/queuing-cam2"] volumeMounts: - name: sample-data diff --git a/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/retail-deployment.yaml b/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/retail-deployment.yaml index 4e76937da..749607693 100644 --- a/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/retail-deployment.yaml +++ b/kubernetes/scenescape-chart/templates/dl-streamer-pipeline-server/retail-deployment.yaml @@ -6,7 +6,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-retail-cams - namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-retail-cams spec: @@ -24,13 +23,15 @@ spec: runAsGroup: 0 initContainers: - name: wait-for-rtsp - image: curlimages/curl:latest + image: {{ .Values.dlspsWaitImage.repository }}/{{ .Values.dlspsWaitImage.name }}:{{ .Values.dlspsWaitImage.tag }} + imagePullPolicy: {{ .Values.dlspsWaitImage.pullPolicy }} command: ["sh", "-c", "until nc -z mediaserver 8554; do echo waiting for mediaserver; sleep 2; done"] securityContext: {{ include "defaultContainerSecurityContext" . | indent 10 }} containers: - name: retail-cams image: {{ .Values.retail.cameras.image }}:{{ .Values.retail.cameras.tag }} + imagePullPolicy: {{.Values.retail.cameras.pullPolicy }} args: ["-nostdin", "-re", "-stream_loop", "-1", "-i", "/data/{{ index .Values.retail.cameras.files 0 }}", "-re", "-stream_loop", "-1", "-i", "/data/{{ index .Values.retail.cameras.files 1 }}", "-map", "0:v", "-c", "copy", "-f", "rtsp", "-rtsp_transport", "tcp", "rtsp://mediaserver:8554/retail-cam1", "-map", "1:v", "-c", "copy", "-f", "rtsp", "-rtsp_transport", "tcp", "rtsp://mediaserver:8554/retail-cam2"] volumeMounts: - name: sample-data diff --git a/kubernetes/scenescape-chart/templates/kubeclient/deployment.yaml b/kubernetes/scenescape-chart/templates/kubeclient/deployment.yaml index f3cb8d0ba..f2aed3c22 100644 --- a/kubernetes/scenescape-chart/templates/kubeclient/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/kubeclient/deployment.yaml @@ -25,12 +25,14 @@ spec: runAsGroup: 1000 initContainers: - name: wait-for-broker-initcontainer - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} command: ["/bin/sh", "-c", "until nc -vz broker.{{ .Release.Namespace }}.svc.cluster.local 1883; do sleep 1; done"] securityContext: {{ include "defaultContainerSecurityContext" . | indent 10 }} - name: wait-for-web-initcontainer - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} command: ["/bin/sh", "-c", "until wget -q --spider http://web.{{ .Release.Namespace }}.svc.cluster.local; do sleep 1; done"] securityContext: {{ include "defaultContainerSecurityContext" . | indent 10 }} @@ -47,6 +49,7 @@ spec: - --auth - /run/secrets/auth/controller.auth image: {{ .Values.repository }}/{{ .Values.kubeclient.image }}:{{ .Chart.AppVersion }} + imagePullPolicy: {{ .Values.kubeclient.pullPolicy }} name: {{ .Release.Name }}-kubeclient env: - name: MODEL_CONFIGS_FOLDER @@ -61,6 +64,8 @@ spec: value: {{ .Values.kubeclient.vaPipeline.image }} - name: HELM_TAG value: {{ .Values.kubeclient.vaPipeline.tag }} + - name: HELM_PULL_POLICY + value: {{ .Values.kubeclient.vaPipeline.pullPolicy }} {{- with .Values.imagePullSecrets }} {{- range $index, $pullSecret := . }} - name: KUBERNETES_PULL_SECRET_{{ $index }} @@ -68,7 +73,6 @@ spec: {{- end }} {{- end }} {{ include "proxy_envs" . | indent 10 }} - imagePullPolicy: Always readinessProbe: exec: command: diff --git a/kubernetes/scenescape-chart/templates/kubeclient/rbac.yaml b/kubernetes/scenescape-chart/templates/kubeclient/rbac.yaml index c07069b10..f12a3c40a 100644 --- a/kubernetes/scenescape-chart/templates/kubeclient/rbac.yaml +++ b/kubernetes/scenescape-chart/templates/kubeclient/rbac.yaml @@ -7,7 +7,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ .Release.Name }}-kubeclient - namespace: {{ .Release.Namespace }} rules: - apiGroups: ["apps"] resources: ["deployments"] @@ -20,7 +19,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ .Release.Name }}-kubeclient-binding - namespace: {{ .Release.Namespace }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role @@ -33,5 +31,4 @@ apiVersion: v1 kind: ServiceAccount metadata: name: {{ .Release.Name }}-kubeclient-sa - namespace: {{ .Release.Namespace }} {{ end }} diff --git a/kubernetes/scenescape-chart/templates/mediaserver/deployment.yaml b/kubernetes/scenescape-chart/templates/mediaserver/deployment.yaml index 631535213..a60b67cc9 100644 --- a/kubernetes/scenescape-chart/templates/mediaserver/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/mediaserver/deployment.yaml @@ -24,8 +24,8 @@ spec: runAsGroup: 1000 containers: - image: {{ .Values.mediaserver.image }}:{{ .Values.mediaserver.tag }} + imagePullPolicy: {{ .Values.mediaserver.pullPolicy }} name: {{ .Release.Name }}-mediaserver - imagePullPolicy: IfNotPresent securityContext: privileged: false readOnlyRootFilesystem: true diff --git a/kubernetes/scenescape-chart/templates/model-installer/job.yaml b/kubernetes/scenescape-chart/templates/model-installer/job.yaml index ab5b37051..5ff7d1982 100644 --- a/kubernetes/scenescape-chart/templates/model-installer/job.yaml +++ b/kubernetes/scenescape-chart/templates/model-installer/job.yaml @@ -25,11 +25,10 @@ apiVersion: batch/v1 kind: Job metadata: name: {{ .Release.Name }}-models-installer - namespace: {{ .Release.Namespace }} annotations: {{- if .Values.hooks.enabled }} "helm.sh/hook": pre-install - "helm.sh/hook-weight": "1" + "helm.sh/hook-weight": "2" {{- if not .Values.chartdebug }} "helm.sh/hook-delete-policy": hook-succeeded {{- end }} @@ -43,8 +42,8 @@ spec: - | {{ .Files.Get "files/model-installer/entrypoint-k8s.sh" | nindent 10 }} image: {{ .Values.initModels.image.repository }}/{{ .Values.initModels.image.name }}:{{ .Values.initModels.image.tag }} + imagePullPolicy: {{ .Values.initModels.image.pullPolicy }} name: {{ .Release.Name }}-init-models-container - imagePullPolicy: Always env: - name: MODEL_TYPE value: "{{ .Values.initModels.modelType }}" diff --git a/kubernetes/scenescape-chart/templates/ntp/deployment.yaml b/kubernetes/scenescape-chart/templates/ntp/deployment.yaml index cf817a081..41aad7bcf 100644 --- a/kubernetes/scenescape-chart/templates/ntp/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/ntp/deployment.yaml @@ -27,7 +27,8 @@ spec: - sh - -c - touch /tmp/healthy - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} name: init-healthy-file securityContext: readOnlyRootFilesystem: true @@ -36,13 +37,13 @@ spec: - name: healthy-file mountPath: /tmp containers: - - image: {{ .Values.ntpserv.image }} + - image: {{ .Values.ntpserv.image.repository }}/{{ .Values.ntpserv.image.name }}:{{ .Values.ntpserv.image.tag }} + imagePullPolicy: {{ .Values.ntpserv.image.pullPolicy }} name: {{ .Release.Name }}-ntpserv env: - name: NTP_SERVERS value: {{ .Values.ntpserv.ntpServers }} {{ include "proxy_envs" . | indent 10 }} - imagePullPolicy: Always securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/kubernetes/scenescape-chart/templates/pgserver/deployment.yaml b/kubernetes/scenescape-chart/templates/pgserver/deployment.yaml index 1b73977a5..174e1067c 100644 --- a/kubernetes/scenescape-chart/templates/pgserver/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/pgserver/deployment.yaml @@ -27,6 +27,7 @@ spec: containers: - name: {{ .Release.Name }}-pgserver image: {{ .Values.pgserver.repository }}/{{ .Values.pgserver.image }}:{{ .Values.pgserver.tag}} + imagePullPolicy: {{ .Values.pgserver.pullPolicy }} env: - name: POSTGRES_USER value: "scenescape" @@ -38,7 +39,6 @@ spec: - name: POSTGRES_DB value: "scenescape" {{ include "proxy_envs" . | indent 12 }} - imagePullPolicy: Always securityContext: allowPrivilegeEscalation: false ports: diff --git a/kubernetes/scenescape-chart/templates/pgserver/pvc.yaml b/kubernetes/scenescape-chart/templates/pgserver/pvc.yaml index ba12dd1c1..c004d4229 100644 --- a/kubernetes/scenescape-chart/templates/pgserver/pvc.yaml +++ b/kubernetes/scenescape-chart/templates/pgserver/pvc.yaml @@ -6,7 +6,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-pgserver-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" diff --git a/kubernetes/scenescape-chart/templates/sample-data/job.yaml b/kubernetes/scenescape-chart/templates/sample-data/job.yaml index af270a830..661cc0ddb 100644 --- a/kubernetes/scenescape-chart/templates/sample-data/job.yaml +++ b/kubernetes/scenescape-chart/templates/sample-data/job.yaml @@ -6,11 +6,10 @@ apiVersion: batch/v1 kind: Job metadata: name: {{ .Release.Name }}-init-sample-data - namespace: {{ .Release.Namespace }} annotations: {{- if .Values.hooks.enabled }} "helm.sh/hook": pre-install - "helm.sh/hook-weight": "1" + "helm.sh/hook-weight": "2" {{- if not .Values.chartdebug }} "helm.sh/hook-delete-policy": hook-succeeded {{- end }} @@ -22,6 +21,7 @@ spec: - name: download-sample-data command: ["/bin/bash", "-c"] image: {{ .Values.sampleData.downloadDataImage.repository }}/{{ .Values.sampleData.downloadDataImage.name }}:{{ .Values.sampleData.downloadDataImage.tag }} + imagePullPolicy: {{ .Values.sampleData.downloadDataImage.pullPolicy }} env: {{- include "proxy_envs" . | indent 8 }} args: @@ -32,6 +32,7 @@ spec: name: sample-data-storage - name: convert-videos image: {{ .Values.sampleData.convertVideosImage.repository }}/{{ .Values.sampleData.convertVideosImage.name }}:{{ .Values.sampleData.convertVideosImage.tag }} + imagePullPolicy: {{ .Values.sampleData.convertVideosImage.pullPolicy }} command: ["/bin/sh", "-c"] args: - | @@ -41,7 +42,8 @@ spec: name: sample-data-storage containers: - name: {{ .Release.Name }}-init-complete - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} command: ["/bin/sh", "-c"] args: - | diff --git a/kubernetes/scenescape-chart/templates/scene-controller/deployment.yaml b/kubernetes/scenescape-chart/templates/scene-controller/deployment.yaml index d986b0e3d..92b506de8 100644 --- a/kubernetes/scenescape-chart/templates/scene-controller/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/scene-controller/deployment.yaml @@ -23,7 +23,8 @@ spec: shareProcessNamespace: true initContainers: - name: wait-for-web-initcontainer - image: busybox + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} command: ["/bin/sh", "-c", "until wget -q --spider http://web.{{ .Release.Namespace }}.svc.cluster.local; do sleep 1; done"] securityContext: readOnlyRootFilesystem: true @@ -39,12 +40,12 @@ spec: - --ntp - ntpserv.{{ .Release.Namespace }}.svc.cluster.local image: {{ .Values.repository }}/{{ .Values.scene.image }}:{{ .Chart.AppVersion }} + imagePullPolicy: {{ .Values.scene.pullPolicy }} name: {{ .Release.Name }}-scene env: - name: VDMS_HOSTNAME value: vdms.{{ .Release.Namespace }}.svc.cluster.local {{ include "proxy_envs" . | indent 10 }} - imagePullPolicy: Always readinessProbe: exec: command: diff --git a/kubernetes/scenescape-chart/templates/shared-volumes/job.yaml b/kubernetes/scenescape-chart/templates/shared-volumes/job.yaml new file mode 100644 index 000000000..f8e2d8d11 --- /dev/null +++ b/kubernetes/scenescape-chart/templates/shared-volumes/job.yaml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# This job ensures that all shared volumes are created and can be mounted to a single pod +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-init-volumes + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "1" + {{- if not .Values.chartdebug }} + "helm.sh/hook-delete-policy": hook-succeeded + {{- end }} +spec: + template: + spec: + containers: + - name: {{ .Release.Name }}-init-migrations-volume + image: {{ .Values.busyboxImage.repository }}/{{ .Values.busyboxImage.name }}:{{ .Values.busyboxImage.tag }} + imagePullPolicy: {{ .Values.busyboxImage.pullPolicy }} + volumeMounts: + - mountPath: /workspace/migrations + name: migrations-storage + - mountPath: /workspace/sample-data + name: sample-data-storage + - mountPath: /workspace/media + name: media-storage + - mountPath: /workspace/models + name: models-storage + volumes: + - name: migrations-storage + persistentVolumeClaim: + claimName: {{ .Release.Name }}-migrations-pvc + - name: sample-data-storage + persistentVolumeClaim: + claimName: {{ .Release.Name }}-sample-data-pvc + - name: media-storage + persistentVolumeClaim: + claimName: {{ .Release.Name }}-media-pvc + - name: models-storage + persistentVolumeClaim: + claimName: {{ .Release.Name }}-models-pvc + restartPolicy: Never + backoffLimit: 1 diff --git a/kubernetes/scenescape-chart/templates/shared-volumes/pvc.yaml b/kubernetes/scenescape-chart/templates/shared-volumes/pvc.yaml index 8f42fb1a9..6c4f6f9f3 100644 --- a/kubernetes/scenescape-chart/templates/shared-volumes/pvc.yaml +++ b/kubernetes/scenescape-chart/templates/shared-volumes/pvc.yaml @@ -6,7 +6,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-media-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" @@ -22,7 +21,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-sample-data-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" diff --git a/kubernetes/scenescape-chart/templates/tests/pvc.yaml b/kubernetes/scenescape-chart/templates/tests/pvc.yaml index 937ed36c9..5a2309472 100644 --- a/kubernetes/scenescape-chart/templates/tests/pvc.yaml +++ b/kubernetes/scenescape-chart/templates/tests/pvc.yaml @@ -7,7 +7,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-tests-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" diff --git a/kubernetes/scenescape-chart/templates/tests/tests-job.yaml b/kubernetes/scenescape-chart/templates/tests/tests-job.yaml index 18bb198b3..c3f5e699d 100644 --- a/kubernetes/scenescape-chart/templates/tests/tests-job.yaml +++ b/kubernetes/scenescape-chart/templates/tests/tests-job.yaml @@ -7,11 +7,10 @@ apiVersion: batch/v1 kind: Job metadata: name: {{ .Release.Name }}-{{ .Values.tests.image }} - namespace: {{ .Release.Namespace }} annotations: {{- if .Values.hooks.enabled }} "helm.sh/hook": pre-install - "helm.sh/hook-weight": "1" + "helm.sh/hook-weight": "2" {{- if not .Values.chartdebug }} "helm.sh/hook-delete-policy": hook-succeeded {{- end }} diff --git a/kubernetes/scenescape-chart/templates/vdms/deployment.yaml b/kubernetes/scenescape-chart/templates/vdms/deployment.yaml index 0e6679ff4..b323275a0 100644 --- a/kubernetes/scenescape-chart/templates/vdms/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/vdms/deployment.yaml @@ -24,7 +24,8 @@ spec: runAsUser: 0 runAsGroup: 0 containers: - - image: intellabs/vdms:latest + - image: {{ .Values.vdms.image.repository }}/{{ .Values.vdms.image.name }}:{{ .Values.vdms.image.tag }} + imagePullPolicy: {{ .Values.vdms.image.pullPolicy }} name: {{ .Release.Name }}-vdms env: - name: OVERRIDE_ca_file @@ -34,7 +35,6 @@ spec: - name: OVERRIDE_key_file value: /run/secrets/certs/scenescape-vdms-s.key {{- include "proxy_envs" . | indent 10 }} - imagePullPolicy: Always securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/kubernetes/scenescape-chart/templates/web-app/deployment.yaml b/kubernetes/scenescape-chart/templates/web-app/deployment.yaml index e31d79843..d03418076 100644 --- a/kubernetes/scenescape-chart/templates/web-app/deployment.yaml +++ b/kubernetes/scenescape-chart/templates/web-app/deployment.yaml @@ -27,6 +27,7 @@ spec: initContainers: - name: wait-for-postgres image: {{ .Values.pgserver.repository }}/{{ .Values.pgserver.image }}:{{ .Values.pgserver.tag}} + imagePullPolicy: {{ .Values.pgserver.pullPolicy }} env: - name: PGPASSWORD valueFrom: @@ -47,6 +48,7 @@ spec: containers: - name: {{ .Release.Name }}-web image: {{ .Values.repository }}/{{ .Values.web.image }}:{{ .Chart.AppVersion }} + imagePullPolicy: {{ .Values.web.pullPolicy }} args: - webserver - --dbhost @@ -78,7 +80,6 @@ spec: value: {{ .Values.dbroot }} {{- end }} {{ include "proxy_envs" . | indent 10 }} - imagePullPolicy: Always securityContext: privileged: true capabilities: @@ -134,7 +135,6 @@ spec: name: models-storage - mountPath: /workspace/migrations name: migrations-storage - restartPolicy: Always {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -144,6 +144,7 @@ spec: - name: dev-fuse hostPath: path: /dev/fuse + type: CharDevice {{ include "certs_volume" . | indent 6 }} - name: supass secret: diff --git a/kubernetes/scenescape-chart/templates/web-app/pvc.yaml b/kubernetes/scenescape-chart/templates/web-app/pvc.yaml index daf072cda..75fe5a37f 100644 --- a/kubernetes/scenescape-chart/templates/web-app/pvc.yaml +++ b/kubernetes/scenescape-chart/templates/web-app/pvc.yaml @@ -6,7 +6,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-migrations-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" @@ -22,7 +21,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ .Release.Name }}-models-pvc - namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": pre-install "helm.sh/hook-weight": "{{ int .Values.pvc.hookWeight }}" diff --git a/kubernetes/scenescape-chart/templates/web-app/secret.yaml b/kubernetes/scenescape-chart/templates/web-app/secret.yaml index 0b6f84ef8..c201c8164 100644 --- a/kubernetes/scenescape-chart/templates/web-app/secret.yaml +++ b/kubernetes/scenescape-chart/templates/web-app/secret.yaml @@ -6,7 +6,6 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-controller.auth - namespace: {{ .Release.Namespace }} type: Opaque data: controller.auth: {{ printf "{\"user\": \"scenectrl\", \"password\": \"%s\"}" (randAlphaNum 16) | b64enc }} @@ -16,7 +15,6 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-browser.auth - namespace: {{ .Release.Namespace }} type: Opaque data: browser.auth: {{ printf "{\"user\": \"webuser\", \"password\": \"%s\"}" (randAlphaNum 16) | b64enc }} @@ -26,7 +24,6 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-calibration.auth - namespace: {{ .Release.Namespace }} type: Opaque data: calibration.auth: {{ printf "{\"user\": \"calibration\", \"password\": \"%s\"}" (randAlphaNum 16) | b64enc }} @@ -36,7 +33,6 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-supass - namespace: {{ .Release.Namespace }} type: Opaque data: supass: {{ include "supass" . | b64enc }} @@ -46,7 +42,6 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-db - namespace: {{ .Release.Namespace }} type: Opaque data: DATABASE_PASSWORD: {{ include "db-password" . | b64enc }} @@ -56,7 +51,6 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-secrets-py - namespace: {{ .Release.Namespace }} type: Opaque data: secrets.py: |- diff --git a/kubernetes/scenescape-chart/values.yaml b/kubernetes/scenescape-chart/values.yaml index 5c42d4566..cad951ee0 100644 --- a/kubernetes/scenescape-chart/values.yaml +++ b/kubernetes/scenescape-chart/values.yaml @@ -6,7 +6,7 @@ # Declare variables to be passed into your templates. # Image repository -repository: localhost:5001 +repository: docker.io imagePullSecrets: {} # Service values @@ -16,6 +16,7 @@ initModels: repository: docker.io name: python tag: 3.13-slim@sha256:4c2cf9917bd1cbacc5e9b07320025bdb7cdf2df7b0ceaccb55e9dd7e30987419 + pullPolicy: IfNotPresent # Model type to download - MUST be one of: default, ocr, all modelType: default # Model precisions - comma-separated list of: FP32, FP16, INT8 @@ -28,66 +29,115 @@ initModels: limits: memory: "2Gi" cpu: "1000m" + ntpserv: - image: dockurr/chrony + image: + repository: docker.io + name: dockurr/chrony + tag: 4.8 + pullPolicy: IfNotPresent ntpServers: "0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org" + broker: image: eclipse-mosquitto tag: "2.0.22" + pullPolicy: IfNotPresent uid: 1000 gid: 1000 + pgserver: image: postgres tag: 17.6 + pullPolicy: IfNotPresent repository: "docker.io" storage: 500Mi password: "" + web: - image: scenescape-manager + image: intel/scenescape-manager + pullPolicy: IfNotPresent + scene: - image: scenescape-controller + image: intel/scenescape-controller + pullPolicy: IfNotPresent + camcalibration: - image: scenescape-camcalibration -# percebro - deprecated # TODO update kubeclient code to restore the dynamic camera calibration + image: intel/scenescape-camcalibration + pullPolicy: IfNotPresent + kubeclient: enabled: true - image: scenescape-manager + image: intel/scenescape-manager + pullPolicy: IfNotPresent vaPipeline: repository: docker.io image: intel/dlstreamer-pipeline-server tag: "3.1.0-ubuntu24" + pullPolicy: IfNotPresent + retail: repository: docker.io/intel/dlstreamer-pipeline-server - pullPolicy: IfNotPresent tag: "3.1.0-ubuntu24" - storageClassName: "standard" + pullPolicy: IfNotPresent + storageClassName: "" cameras: image: linuxserver/ffmpeg tag: version-8.0-cli + pullPolicy: IfNotPresent files: - apriltag-cam1.ts - apriltag-cam2.ts + queuing: repository: docker.io/intel/dlstreamer-pipeline-server pullPolicy: IfNotPresent tag: "3.1.0-ubuntu24" - storageClassName: "standard" + storageClassName: "" cameras: image: linuxserver/ffmpeg tag: version-8.0-cli + pullPolicy: IfNotPresent files: - qcam1.ts - qcam2.ts + +dlspsWaitImage: + repository: docker.io + name: curlimages/curl + tag: 8.17.0 + pullPolicy: IfNotPresent + mediaserver: image: bluenviron/mediamtx tag: "1.14.0" + pullPolicy: IfNotPresent + video: - image: scenescape # models storage size storage: 50Gi storageClassName: "" + vdms: enabled: true + image: + repository: docker.io + name: intellabs/vdms + tag: v2.11.0 + pullPolicy: IfNotPresent + +clusterAnalytics: + enabled: false + image: intel/scenescape-cluster-analytics + pullPolicy: IfNotPresent + webUI: + enabled: false + +# Used as init container in some deployments to wait for dependent services +busyboxImage: + repository: docker.io + name: busybox + tag: 1.37.0 + pullPolicy: IfNotPresent # media folder pvc details media: @@ -99,12 +149,6 @@ datasets: storage: 500Mi storageClassName: "" -# videos -videos: - storage: 2Gi - storageClassName: "" - -# sample_data sampleData: source: https://raw.githubusercontent.com/open-edge-platform/scenescape sourceDir: sample_data @@ -113,11 +157,12 @@ sampleData: repository: docker.io name: ubuntu tag: 22.04 + pullPolicy: IfNotPresent convertVideosImage: repository: docker.io name: linuxserver/ffmpeg tag: version-8.0-cli - + pullPolicy: IfNotPresent files: - apriltag-cam1.mp4 - apriltag-cam2.mp4 @@ -142,16 +187,6 @@ migrations: storage: 100Mi storageClassName: "" -# controller -controller: - storage: 500Mi - storageClassName: "" - -# user-access-config -userAccessConfig: - storage: 50Mi - storageClassName: "" - # PVC pvc: storageClassName: "" @@ -189,9 +224,6 @@ mqttService: annotations: {} nodePort: 31883 -camcalibrationService: - annotations: {} - # Other parameters certdomain: "" supass: "" diff --git a/manager/config/000-default.conf b/manager/config/000-default.conf index 40d1bc06a..38cc31b5e 100644 --- a/manager/config/000-default.conf +++ b/manager/config/000-default.conf @@ -56,6 +56,6 @@ ServerTokens Prod ServerSignature Off TraceEnable Off -Header set Content-Security-Policy "frame-ancestors 'none'; form-action 'self'; script-src 'self' 'sha256-opencv' 'unsafe-eval' https://*.googleapis.com https://*.mapbox.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://*.mapbox.com https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://*.googleapis.com https://*.mapbox.com https://*.gstatic.com; connect-src 'self' https://*.mapbox.com https://*.googleapis.com; frame-src 'none';" +Header set Content-Security-Policy "frame-ancestors 'none'; form-action 'self'; script-src 'self' 'sha256-opencv' 'unsafe-eval' https://*.googleapis.com https://*.mapbox.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://*.mapbox.com https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https://*.googleapis.com https://*.mapbox.com https://*.gstatic.com; connect-src 'self' data: blob: https://*.mapbox.com https://*.googleapis.com; frame-src 'none';" Header set Strict-Transport-Security "max-age=1024000; includeSubDomains" diff --git a/manager/src/django/kubeclient.py b/manager/src/django/kubeclient.py index 069001420..134e3d950 100644 --- a/manager/src/django/kubeclient.py +++ b/manager/src/django/kubeclient.py @@ -25,6 +25,7 @@ def __init__(self, broker, mqttAuth, mqttCert, mqttRootCert, restURL): self.repo = os.environ.get('HELM_REPO') self.image = os.environ.get('HELM_IMAGE') self.tag = os.environ.get('HELM_TAG') + self.pull_policy = os.environ.get('HELM_PULL_POLICY', 'IfNotPresent') # Get pull secrets self.pull_secrets = [] i = 0 @@ -49,6 +50,31 @@ def __init__(self, broker, mqttAuth, mqttCert, mqttRootCert, restURL): self.restAuth = mqttAuth self.rest = RESTClient(restURL, rootcert=mqttRootCert, auth=self.restAuth) + def getOwnerReference(self): + """! Get owner reference to the kubeclient deployment for garbage collection + @return list of V1OwnerReference or None + """ + try: + # Get the kubeclient deployment itself + owner_deployment = self.api_instance.read_namespaced_deployment( + name=f"{self.release}-kubeclient-dep", + namespace=self.ns + ) + + # Create owner reference + owner_ref = client.V1OwnerReference( + api_version="apps/v1", + kind="Deployment", + name=owner_deployment.metadata.name, + uid=owner_deployment.metadata.uid, + controller=False, + block_owner_deletion=False + ) + return [owner_ref] + except ApiException as e: + log.warn(f"Could not get owner reference: {e}") + return None + def mqttOnConnect(self, client, userdata, flags, rc): """! Subscribes to a list of topics on MQTT. @param client Client instance for this callback. @@ -251,7 +277,7 @@ def generateDeploymentBody(self, deployment_name, container_name, sensor_id, pip security_context=client.V1SecurityContext(privileged=True, run_as_user=0, run_as_group=0), env=env, ports=ports, - image_pull_policy="Always", + image_pull_policy=f"{self.pull_policy}", readiness_probe=client.V1Probe(_exec=client.V1ExecAction( command=["curl", "-I", "-s", "http://localhost:8080/pipelines"] ), period_seconds=10, initial_delay_seconds=10, timeout_seconds=5, failure_threshold=5), @@ -272,12 +298,16 @@ def generateDeploymentBody(self, deployment_name, container_name, sensor_id, pip ) ) ) + # Get owner reference for garbage collection + owner_references = self.getOwnerReference() + deployment = client.V1Deployment( api_version="apps/v1", kind="Deployment", metadata=client.V1ObjectMeta( name=deployment_name, labels={'app': container_name[:63], 'release': self.release, 'sensor-id-hash': self.hash(sensor_id)}, + owner_references=owner_references ), spec=deployment_spec ) @@ -401,7 +431,10 @@ def createPipelineConfigmap(self, deploymentName, pipelineConfig): """ configMapName = deploymentName - metadata = client.V1ObjectMeta(name=configMapName) + # Get owner reference for garbage collection + owner_references = self.getOwnerReference() + + metadata = client.V1ObjectMeta(name=configMapName, owner_references=owner_references) data = {"config.yaml": pipelineConfig} config_map = client.V1ConfigMap(api_version="v1", kind="ConfigMap", metadata=metadata, data=data) diff --git a/manager/src/django/mesh_generator.py b/manager/src/django/mesh_generator.py index 5c221f64b..281aa279c 100644 --- a/manager/src/django/mesh_generator.py +++ b/manager/src/django/mesh_generator.py @@ -9,9 +9,9 @@ import os import threading from typing import Dict + import numpy as np from scipy.spatial.transform import Rotation - from django.core.files.base import ContentFile import paho.mqtt.client as mqtt import trimesh @@ -28,9 +28,9 @@ class CameraImageCollector: def __init__(self): self.collected_images = {} self.image_condition = threading.Condition() - self.max_wait_time = 30 # seconds + self.max_wait_time_per_cam = 5 # seconds - def collectImagesForScene(self, scene, mqtt_client): + def collectImagesForScene(self, cameras, mqtt_client): """ Collect calibration images from all cameras attached to the scene. @@ -41,21 +41,17 @@ def collectImagesForScene(self, scene, mqtt_client): Returns: dict: Dictionary mapping camera_id to base64 image data """ - # Get all cameras for this scene - cameras = scene.sensor_set.filter(type='camera') if not cameras.exists(): raise ValueError("No cameras found in scene") - log.info(f"Found {cameras.count()} cameras in scene {scene.name}") - # Reset collected images self.collected_images = {} # Subscribe to image calibration topics for all cameras for camera in cameras: topic = PubSub.formatTopic(PubSub.IMAGE_CALIBRATE, camera_id=camera.sensor_id) - mqtt_client.addCallback(topic, self._onCalibrationImageReceived) + mqtt_client.addCallback(topic, self._onCalibrationImageReceived, qos=2) log.info(f"Subscribed to calibration images for camera {camera.sensor_id}") # Send getcalibrationimage command to all cameras @@ -74,7 +70,7 @@ def collectImagesForScene(self, scene, mqtt_client): start_time = time.time() while len(self.collected_images) < cameras.count(): elapsed = time.time() - start_time - remaining_time = self.max_wait_time - elapsed + remaining_time = (self.max_wait_time_per_cam * cameras.count()) - elapsed if remaining_time <= 0: break @@ -128,7 +124,7 @@ class MappingServiceClient: def __init__(self): # Get mapping service URL from environment or use default self.base_url = os.environ.get('MAPPING_SERVICE_URL', 'https://mapping.scenescape.intel.com:8444') - self.timeout = 300 # 5 minutes timeout for mesh generation + self.timeout_per_camera = 15 # timeout (in seconds) per camera for mesh generation self.health_timeout = 5 # Short timeout for health checks # Obtain rootcert for HTTPS requests, same logic as models.py @@ -167,7 +163,7 @@ def reconstructMesh(self, images: Dict[str, Dict], mesh_type='mesh'): response = requests.post( f"{self.base_url}/reconstruction", json=request_data, - timeout=self.timeout, + timeout=self.timeout_per_camera * len(image_list), headers={'Content-Type': 'application/json'}, verify=self.rootcert ) @@ -267,12 +263,11 @@ def generateMeshFromScene(self, scene, mesh_type='mesh'): mqtt_client = PubSub(auth, cert, rootcert, broker) mqtt_client.connect() + cameras = scene.sensor_set.filter(type='camera').order_by('id') + # Collect images from all cameras in the scene log.info(f"Starting mesh generation for scene {scene.name}") - images = self.image_collector.collectImagesForScene(scene, mqtt_client) - - # Get scene cameras (in same order as images) - cameras = scene.sensor_set.filter(type='camera').order_by('id') + images = self.image_collector.collectImagesForScene(cameras, mqtt_client) log.info(f"Collected {len(images)} images, calling mapping service") # Call mapping service to generate mesh @@ -288,7 +283,12 @@ def generateMeshFromScene(self, scene, mesh_type='mesh'): # Save the generated mesh to the scene if mapping_result.get('success') and mapping_result.get('glb_data'): - self._saveMeshToScene(scene, mapping_result['glb_data']) + # Save mesh and get the transformation applied during alignment + mesh_transform = self._saveMeshToScene(scene, mapping_result['glb_data']) + + # Apply the same transformation to cameras to maintain relative pose + if mesh_transform is not None: + self._transformCamerasWithMeshAlignment(cameras, mesh_transform) processing_time = time.time() - start_time log.info(f"Mesh generation completed successfully in {processing_time:.2f}s") @@ -373,14 +373,9 @@ def _updateCameraParameters(self, camera, pose_data, intrinsics_matrix): """ try: # Extract pose data - rotation_quat = pose_data['rotation'] # [w, x, y, z] + rotation_quat = pose_data['rotation'] # [x, y, z, w] translation = pose_data['translation'] # [x, y, z] - # Transform from OpenCV coordinates (API output) to SceneScape Z-up coordinates - rotation_quat_scenescape, translation_scenescape = self._transformOpenCVToSceneScapeCoordinates( - rotation_quat, translation - ) - # Extract intrinsics (3x3 matrix -> fx, fy, cx, cy) intrinsics_array = np.array(intrinsics_matrix) fx = intrinsics_array[0, 0] @@ -398,10 +393,9 @@ def _updateCameraParameters(self, camera, pose_data, intrinsics_matrix): # Django QUATERNION format expects: [translation_x, translation_y, translation_z, # rotation_x, rotation_y, rotation_z, rotation_w, # scale_x, scale_y, scale_z] - # Use transformed coordinates and reorder quaternion from [w, x, y, z] to [x, y, z, w] camera.cam.transforms = [ - translation_scenescape[0], translation_scenescape[1], translation_scenescape[2], # translation - rotation_quat_scenescape[1], rotation_quat_scenescape[2], rotation_quat_scenescape[3], rotation_quat_scenescape[0], # quaternion [x, y, z, w] + translation[0], translation[1], translation[2], # translation + rotation_quat[0], rotation_quat[1], rotation_quat[2], rotation_quat[3], # quaternion [x, y, z, w] 1.0, 1.0, 1.0 # scale (default to 1.0) ] camera.cam.transform_type = QUATERNION # Use quaternion transform type @@ -420,77 +414,251 @@ def _saveMeshToScene(self, scene, glb_data_base64): Args: scene: Scene object to update glb_data_base64: Base64 encoded GLB file data + + Returns: + dict: Transformation applied to mesh (rotation matrix, translation, center_offset) """ try: # Decode base64 GLB data glb_bytes = base64.b64decode(glb_data_base64) - # Directly use the decoded bytes without re-exporting unless merging is needed mesh = trimesh.load(BytesIO(glb_bytes), file_type='glb') merged_mesh = mergeMesh(mesh) - filename = f"{scene.name}_generated_mesh.glb" - # Only export if mesh was merged/modified, else use original bytes - if merged_mesh is not mesh: - glb_exported_bytes = merged_mesh.export(file_type='glb') - else: - glb_exported_bytes = glb_bytes + # Align the mesh to XY plane with largest bottom face flat and in first quadrant + log.info(f"Aligning mesh to XY plane in first quadrant") + aligned_mesh, mesh_transform = self.alignMeshToXYPlane(merged_mesh) - log.info(f"Saving generated mesh to scene {scene.name} as {filename}") - # Save to scene's map field using the file-like object - scene.map.save(filename, ContentFile(glb_exported_bytes), save=True) + # Export the aligned mesh as GLB + glb_filename = f"{scene.name}_generated_mesh.glb" + glb_exported_bytes = aligned_mesh.export(file_type='glb') + + log.info(f"Saving aligned mesh to scene {scene.name} as {glb_filename}") + # Save to scene's map field without triggering save yet + scene.map.save(glb_filename, ContentFile(glb_exported_bytes), save=False) # Update the map_processed timestamp scene.map_processed = get_iso_time() - scene.save(update_fields=['map_processed']) + scene._original_map = None + + # Save the scene - this will trigger thumbnail generation via regenerateThumbnail() + scene.save() + + log.info(f"Saved generated mesh to scene {scene.name}") - log.info(f"Saved generated mesh to scene {scene.name} as {filename}") + return mesh_transform except Exception as e: log.error(f"Failed to save mesh to scene: {e}") raise Exception(f"Failed to save mesh file: {e}") - def _transformOpenCVToSceneScapeCoordinates(self, rotation_quat, translation): + def _transformCamerasWithMeshAlignment(self, cameras, mesh_transform): + """ + Apply the same transformation to cameras that was applied to the mesh. + This maintains the relative pose between cameras and mesh. + + Args: + cameras: QuerySet of camera objects to transform + mesh_transform: Dictionary containing: + - 'rotation_matrix': 3x3 rotation matrix applied to mesh + - 'translation': Translation vector applied to mesh after rotation + - 'center_offset': Centering offset applied to mesh """ - Transform camera pose from OpenCV coordinate system to SceneScape Z-up coordinate system. + try: + rotation_matrix = mesh_transform['rotation_matrix'] + translation = mesh_transform['translation'] + center_offset = mesh_transform['center_offset'] + + log.info(f"Transforming {cameras.count()} cameras to match mesh alignment") + + for camera in cameras: + try: + # Get current camera transform (in QUATERNION format) + # Format: [tx, ty, tz, qx, qy, qz, qw, sx, sy, sz] + cam_transforms = camera.cam.transforms + + if not cam_transforms or len(cam_transforms) < 10: + log.warning(f"Camera {camera.sensor_id} has invalid transforms, skipping") + continue + + current_position = np.array([cam_transforms[0], cam_transforms[1], cam_transforms[2]]) + current_quat_xyzw = np.array([cam_transforms[3], cam_transforms[4], cam_transforms[5], cam_transforms[6]]) + current_rotation = Rotation.from_quat(current_quat_xyzw).as_matrix() + + rotated_position = rotation_matrix @ current_position + translated_position = rotated_position + translation + final_position = translated_position - center_offset + final_rotation = rotation_matrix @ current_rotation + final_quat_xyzw = Rotation.from_matrix(final_rotation).as_quat() + + # Update camera transforms + camera.cam.transforms = [ + final_position[0], final_position[1], final_position[2], # translation + final_quat_xyzw[0], final_quat_xyzw[1], final_quat_xyzw[2], final_quat_xyzw[3], # quaternion [x, y, z, w] + cam_transforms[7], cam_transforms[8], cam_transforms[9] # scale (preserve original) + ] + + camera.cam.save() + log.info(f"Transformed camera {camera.sensor_id}") - OpenCV coordinates (API output): - - X: right, Y: down, Z: forward (into scene) + except Exception as e: + log.error(f"Failed to transform camera {camera.sensor_id}: {e}") - SceneScape Z-up coordinates: - - X: right, Y: forward, Z: up (world coordinates) + log.info(f"Successfully transformed all cameras to match mesh alignment") + + except Exception as e: + log.error(f"Failed to transform cameras with mesh alignment: {e}") + raise + + def _extractLargestBottomFaceNormal(self, mesh): + """ + Extract the normal vector of the largest face of the OBB that is oriented towards the negative Z direction. + + Args: + mesh: trimesh object + + Returns: + numpy array: Normal vector of the largest bottom face + """ + to_origin, extents = trimesh.bounds.oriented_bounds(mesh) + + # The to_origin matrix transforms the mesh into OBB coordinates + # We need the inverse to get OBB axes in world coordinates + from_origin = np.linalg.inv(to_origin) + R = from_origin[:3, :3] + obb_center = from_origin[:3, 3] + + log.info(f"OBB center: {obb_center}, extents: {extents}") + + # OBB has 6 faces (pairs of parallel faces along 3 axes) + # Face normals in OBB coordinate system are the 3 axis directions + # We need to find which face is largest and farthest in -ve Z direction + + # The 3 axes of the OBB in world coordinates are the columns of R + # Face areas are products of two extent dimensions + face_areas = [ + extents[1] * extents[2], # Face perpendicular to axis 0 (X-axis of OBB) + extents[0] * extents[2], # Face perpendicular to axis 1 (Y-axis of OBB) + extents[0] * extents[1] # Face perpendicular to axis 2 (Z-axis of OBB) + ] + + # For each axis, we have two faces (positive and negative direction) + # Compute the center of each face and its Z coordinate + faces = [] + for axis_idx in range(3): + # Normal vector in world coordinates for this axis + normal = R[:, axis_idx] + + # Two face centers along this axis + for direction in [-1, 1]: + face_center = obb_center + direction * (extents[axis_idx] / 2.0) * normal + faces.append({ + 'axis_idx': axis_idx, + 'direction': direction, + 'normal': normal * direction, + 'center': face_center, + 'area': face_areas[axis_idx], + 'z_position': face_center[2] + }) + + # Find the largest face that is farthest in the -ve z direction + # Sort by area (descending) then by z_position (ascending for most negative) + faces.sort(key=lambda f: (-f['area'], f['z_position'])) + + target_face = faces[0] + log.info(f"Selected face: axis={target_face['axis_idx']}, area={target_face['area']:.2f}, " + f"z_pos={target_face['z_position']:.2f}, normal={target_face['normal']}") + + # Ensure the normal points upward (+Z direction) + normal = target_face['normal'] + normal = normal / np.linalg.norm(normal) + if normal[2] < 0: + normal = -normal + + return normal + + def _computeAlignmentRotation(self, target_normal): + """ + Compute rotation matrix to align target normal with Z-axis. + """ + z_axis = np.array([0.0, 0.0, 1.0]) + rotation_axis = np.cross(target_normal, z_axis) + rotation_axis_norm = np.linalg.norm(rotation_axis) + + if rotation_axis_norm > 1e-6: + rotation_axis = rotation_axis / rotation_axis_norm + rotation_angle = np.arccos(np.clip(np.dot(target_normal, z_axis), -1.0, 1.0)) + rotation = Rotation.from_rotvec(rotation_angle * rotation_axis) + rotation_matrix = rotation.as_matrix() + else: + # Target normal is already aligned with Z-axis + if target_normal[2] > 0: + rotation_matrix = np.eye(3) + else: + # Need to flip 180 degrees + rotation_matrix = np.diag([1, 1, -1]) + + return rotation_matrix + + def alignMeshToXYPlane(self, mesh_data): + """ + Align mesh such that the largest face farthest in the -ve z direction is flat on the XY plane. + + This method: + 1. Computes the oriented bounding box (OBB) of the mesh + 2. Identifies the largest face of the OBB that is farthest in the negative Z direction + 3. Rotates and translates the mesh so that face lies flat on the XY plane (z=0) + 4. Moves the mesh to the first quadrant (all vertices have x,y,z >= 0) Args: - rotation_quat: Quaternion [w, x, y, z] in OpenCV coordinates - translation: Translation [x, y, z] in OpenCV coordinates + mesh_data: Either a trimesh object or bytes/BytesIO of a mesh file (GLB, PLY, etc.) Returns: - tuple: (transformed_quaternion, transformed_translation) for SceneScape coordinates + tuple: (aligned_mesh, transform_dict) where transform_dict contains: + - 'rotation_matrix': 3x3 rotation matrix applied + - 'translation': Translation vector applied after rotation + - 'center_offset': Centering offset applied (zero in this case) """ - # Create coordinate transformation matrix: OpenCV -> SceneScape Z-up - # OpenCV (X:right, Y:down, Z:forward) -> SceneScape (X:right, Y:forward, Z:up) - coord_transform = np.array([ - [1, 0, 0], # X stays the same (right) - [0, 0, 1], # Y becomes old Z (forward) - [0, -1, 0] # Z becomes old -Y (up) - ]) - - # Transform translation - translation_np = np.array(translation) - translation_scenescape = coord_transform @ translation_np - - # Transform rotation quaternion - # Convert quaternion to rotation matrix, transform, then back to quaternion - - # Convert [w, x, y, z] to scipy format [x, y, z, w] - quat_scipy = [rotation_quat[1], rotation_quat[2], rotation_quat[3], rotation_quat[0]] - rotation_matrix = Rotation.from_quat(quat_scipy).as_matrix() - - # Apply coordinate transformation: R' = T * R * T^-1 - rotation_matrix_scenescape = coord_transform @ rotation_matrix @ coord_transform.T - - # Convert back to quaternion in [w, x, y, z] format - quat_scenescape_scipy = Rotation.from_matrix(rotation_matrix_scenescape).as_quat() - rotation_quat_scenescape = [quat_scenescape_scipy[3], quat_scenescape_scipy[0], - quat_scenescape_scipy[1], quat_scenescape_scipy[2]] - - return rotation_quat_scenescape, translation_scenescape.tolist() + try: + if isinstance(mesh_data, (bytes, BytesIO)): + mesh = trimesh.load(BytesIO(mesh_data) if isinstance(mesh_data, bytes) else mesh_data, file_type='glb') + else: + mesh = mesh_data + + # Get the largest bottom face normal (already normalized and pointing upward) + target_normal = self._extractLargestBottomFaceNormal(mesh) + + # Compute rotation to align target normal with Z-axis + rotation_matrix = self._computeAlignmentRotation(target_normal) + rotation_transform = np.eye(4) + rotation_transform[:3, :3] = rotation_matrix + mesh.apply_transform(rotation_transform) + + # Compute translation to move the mesh entirely to first quadrant (+x, +y) and z=0 + # Find the minimum values along each axis after rotation + bounds = mesh.bounds # [[min_x, min_y, min_z], [max_x, max_y, max_z]] + min_x, min_y, min_z = bounds[0] + + translation = np.array([-min_x, -min_y, -min_z]) + translation_transform = np.eye(4) + translation_transform[:3, 3] = translation + mesh.apply_transform(translation_transform) + + # Verify the mesh is in the first quadrant + final_bounds = mesh.bounds + final_min = final_bounds[0] + final_max = final_bounds[1] + + log.info(f"Mesh aligned to first quadrant: bbox min={final_min}, max={final_max}") + + transform_dict = { + 'rotation_matrix': rotation_matrix, + 'translation': translation, + 'center_offset': np.array([0.0, 0.0, 0.0]) + } + + return mesh, transform_dict + + except Exception as e: + log.error(f"Failed to align mesh to XY plane: {e}") + raise + diff --git a/manager/src/static/js/calibration.js b/manager/src/static/js/calibration.js index 18f5d3da5..cbd5127ee 100644 --- a/manager/src/static/js/calibration.js +++ b/manager/src/static/js/calibration.js @@ -4,11 +4,9 @@ "use strict"; import { APP_NAME, IMAGE_CALIBRATE } from "/static/js/constants.js"; -import { updateElements } from "/static/js/utils.js"; import { ConvergedCameraCalibration } from "/static/js/cameracalibrate.js"; var calibration_strategy; -var advanced_calibration_fields = []; let camera_calibration; // Initialize after DOM is ready @@ -94,12 +92,6 @@ async function initializeCalibration(scene_id, socket) { if (document.getElementById("lock_distortion_k1")) { document.getElementById("lock_distortion_k1").style.visibility = "hidden"; } - advanced_calibration_fields = $("#kubernetes-fields").val().split(","); - updateElements( - advanced_calibration_fields.map((e) => e + "_wrapper"), - "hidden", - true, - ); calibration_strategy = document.getElementById("calib_strategy").value; diff --git a/manager/src/static/js/interactions.js b/manager/src/static/js/interactions.js index 220760be4..234756ddf 100644 --- a/manager/src/static/js/interactions.js +++ b/manager/src/static/js/interactions.js @@ -32,13 +32,16 @@ function SetupInteractions( if (selectedCamera !== obj) { if (selectedCamera) selectedCamera.unselect(); selectedCamera = obj; - } else if (!openFolder) { - selectedCamera.unselect(); - selectedCamera = null; + if (selectedCamera !== null) selectedCamera.onClick(openFolder); + } else { + // Same camera clicked - handle folder toggle or deselect + if (selectedCamera !== null) selectedCamera.onClick(openFolder); + if (!openFolder) { + selectedCamera.unselect(); + selectedCamera = null; + } } - if (selectedCamera !== null) selectedCamera.onClick(openFolder); - return; } diff --git a/manager/src/static/js/scenescape3d.js b/manager/src/static/js/scenescape3d.js index 936d77177..19d04117c 100644 --- a/manager/src/static/js/scenescape3d.js +++ b/manager/src/static/js/scenescape3d.js @@ -152,7 +152,7 @@ function main() { }; // Ambient scene lighting - const ambientColor = 0x707070; // Soft white + const ambientColor = 0xa0a0a0; // Brighter ambient for more vibrant colors const ambientLight = new THREE.AmbientLight(ambientColor); scene.add(ambientLight); diff --git a/manager/src/static/js/thing/controls/thingtransformcontrols.js b/manager/src/static/js/thing/controls/thingtransformcontrols.js index d3be3766b..f029982df 100644 --- a/manager/src/static/js/thing/controls/thingtransformcontrols.js +++ b/manager/src/static/js/thing/controls/thingtransformcontrols.js @@ -96,8 +96,11 @@ let thingTransformControls = { object.rotateZ(Math.PI); }, resetTransformObject() { + const currentVisibility = this.getTransformControlObject3D().visible; this.transformControl.detach(); this.transformControl.attach(this.transformObject); + // Restore visibility state because attach() automatically sets visible = true + this.setTransformControlVisibility(currentVisibility); }, setTransformControlVisibility(enable) { const object = this.getTransformControlObject3D(); diff --git a/manager/src/static/js/thing/scene.js b/manager/src/static/js/thing/scene.js index 301fe5665..b1216ee8c 100644 --- a/manager/src/static/js/thing/scene.js +++ b/manager/src/static/js/thing/scene.js @@ -266,20 +266,17 @@ export default class Scene { this.orthographicCamera.position.set(0, 0, cameraZ); this.orthographicCamera.updateProjectionMatrix(); - // Directional scene lighting + // Directional scene lighting - matching Open3D sun light setup // Check if a directional light already exists and update it, otherwise add a new one let directionalLight = this.scene.getObjectByName("directionalLight"); if (!directionalLight) { - directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); + directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); directionalLight.name = "directionalLight"; this.scene.add(directionalLight); } - // Set the light above the origin to provide reasonable shading - directionalLight.position.set( - -center.x, - -center.y, - this.perspectiveCamera.position.z * 2, - ); + // Match Open3D sun light: direction [0.0, 0.0, -1.0] pointing straight down + // In Three.js, light points FROM position TO origin, so we set position to [0, 0, 1] + directionalLight.position.set(0, 0, 1); // Center orbit controls on the scene this.orbitControls.target.set(center.x, center.y, 1); @@ -288,8 +285,6 @@ export default class Scene { this.orbitControls.saveState(); // Initial reset (sometimes the scene loads rotated otherwise) this.orbitControls.reset(); - - this.scene.add(directionalLight); } // Initialize the floor plane visibility diff --git a/manager/src/static/js/thing/scenecamera.js b/manager/src/static/js/thing/scenecamera.js index c2c42f6e1..5f8ef82f1 100644 --- a/manager/src/static/js/thing/scenecamera.js +++ b/manager/src/static/js/thing/scenecamera.js @@ -500,7 +500,9 @@ export default class SceneCamera extends THREE.Object3D { this.controlsFolder.$title.addEventListener( "click", ((event) => { - camerasFolder.setSelectedCamera(this); + // Check if folder will be open after the click (it toggles, so check current state and invert) + const willBeOpen = !this.controlsFolder._closed; + camerasFolder.setSelectedCamera(this, willBeOpen); }).bind(this), ); @@ -1115,7 +1117,8 @@ export default class SceneCamera extends THREE.Object3D { } onClick(open) { - this.setTransformControlVisibility(true); + // Show transform controls only when folder is opened + this.setTransformControlVisibility(open); this.add(this.calibPoints); this.executeOnControl("calibration points visibility", (control) => { control[0].setValue(true); diff --git a/manager/src/static/js/viewport.js b/manager/src/static/js/viewport.js index 6843211f3..1b0aea6b5 100644 --- a/manager/src/static/js/viewport.js +++ b/manager/src/static/js/viewport.js @@ -65,7 +65,7 @@ class Viewport extends THREE.Scene { // Ambient scene lighting and background this.background = new THREE.Color(0x808080); - const ambientColor = 0x707070; // Soft white + const ambientColor = 0xa0a0a0; // Brighter ambient for more vibrant colors const ambientLight = new THREE.AmbientLight(ambientColor); this.add(ambientLight); @@ -554,12 +554,11 @@ class Viewport extends THREE.Scene { // FIXME: Improve the loading flow so that the calibration points are loaded after the map this.updateCalibrationPointScale(); - const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); - directionalLight.position.set( - -this.perspectiveCamera.position.x, - -this.perspectiveCamera.position.y, - this.perspectiveCamera.position.z * 2, - ); + // Add directional light matching Open3D sun light setup + // Open3D uses direction [0.0, 0.0, -1.0] pointing straight down + // In Three.js, light points FROM position TO origin, so we set position to [0, 0, 1] + const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); + directionalLight.position.set(0, 0, 1); this.add(directionalLight); this.orbitControls.target.set(this.floorWidth / 2, this.floorHeight / 2, 1); diff --git a/mapping/Dockerfile b/mapping/Dockerfile index d0636e5b4..fda59dac4 100644 --- a/mapping/Dockerfile +++ b/mapping/Dockerfile @@ -54,6 +54,7 @@ ENV SCENESCAPE_HOME=/home/$WSUSER/SceneScape # Install runtime system dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ python3 \ python3-pip \ python-is-python3 \ diff --git a/mapping/docs/user-guide/api-docs/mapping-api.yaml b/mapping/docs/user-guide/api-docs/mapping-api.yaml index f92adef44..74b268864 100644 --- a/mapping/docs/user-guide/api-docs/mapping-api.yaml +++ b/mapping/docs/user-guide/api-docs/mapping-api.yaml @@ -214,7 +214,7 @@ paths: native_output: "mesh" supported_outputs: ["mesh", "pointcloud"] camera_pose_format: - rotation: "quaternion [w, x, y, z]" + rotation: "quaternion [x, y, z, w]" translation: "vector [x, y, z]" coordinate_system: "OpenCV (camera-to-world transformation, standard CV coordinates)" @@ -319,7 +319,7 @@ components: format: float minItems: 4 maxItems: 4 - description: Quaternion rotation [w, x, y, z] + description: Quaternion rotation [x, y, z, w] example: [0.98, 0.0, 0.15, 0.0] translation: type: array @@ -381,7 +381,7 @@ components: properties: rotation: type: string - example: "quaternion [w, x, y, z]" + example: "quaternion [x, y, z, w]" translation: type: string example: "vector [x, y, z]" diff --git a/mapping/docs/user-guide/overview.md b/mapping/docs/user-guide/overview.md index 131d5af89..6e26aa3c1 100644 --- a/mapping/docs/user-guide/overview.md +++ b/mapping/docs/user-guide/overview.md @@ -79,7 +79,7 @@ Perform 3D reconstruction from input images. "glb_data": "base64_encoded_glb_file", "camera_poses": [ { - "rotation": [0, 0, 0, 0], // quaternion rotation [w, x, y, z] + "rotation": [0, 0, 0, 0], // quaternion rotation [x, y, z, w] "translation": [0, 0, 0] // 3D translation vector [x, y, z] } ], diff --git a/mapping/src/api_service_base.py b/mapping/src/api_service_base.py index b64516a03..f0a57847b 100644 --- a/mapping/src/api_service_base.py +++ b/mapping/src/api_service_base.py @@ -230,7 +230,7 @@ def listModels(): "model": model_name, "model_info": model_info, "camera_pose_format": { - "rotation": "quaternion [w, x, y, z]", + "rotation": "quaternion [x, y, z, w]", "translation": "vector [x, y, z]", "coordinate_system": "OpenCV (camera-to-world transformation, standard CV coordinates)" } diff --git a/mapping/src/mapanything_model.py b/mapping/src/mapanything_model.py index 065a74e9e..52dc3aeb0 100644 --- a/mapping/src/mapanything_model.py +++ b/mapping/src/mapanything_model.py @@ -295,7 +295,7 @@ def _processOutputs(self, outputs: List[Dict], original_sizes: List[tuple], model_intrinsics_list = [] # Create rotation matrix for 180° around X-axis (applied to all cameras). - # Mesh already comes with + # Mesh already is rotated 180° around x-axis in MapAnything output. rotation_x_180 = np.array([ [1, 0, 0, 0], [0, -1, 0, 0], @@ -333,14 +333,14 @@ def _processOutputs(self, outputs: List[Dict], original_sizes: List[tuple], pose_4x4 = np.eye(4, dtype=np.float32) pose_4x4[:3, :3] = pose_np[:3, :3] pose_4x4[:3, 3] = pose_np[:3, 3] - rotated_pose = rotation_x_180 @ pose_4x4 + rotated_pose = rotation_x_180 @ pose_4x4 #SARAT # Convert rotation matrix to quaternion rotation_matrix = rotated_pose[:3, :3] quaternion = self.rotationMatrixToQuaternion(rotation_matrix) camera_poses.append({ - "rotation": quaternion.tolist(), # [w, x, y, z] + "rotation": quaternion.tolist(), # [x, y, z, w] "translation": rotated_pose[:3, 3].tolist() }) model_intrinsics_list.append(intrinsics_np) diff --git a/mapping/src/model_interface.py b/mapping/src/model_interface.py index a40ea3bc9..c4a98acc4 100644 --- a/mapping/src/model_interface.py +++ b/mapping/src/model_interface.py @@ -222,13 +222,13 @@ def decodeBase64Image(self, image_data: str) -> np.ndarray: def rotationMatrixToQuaternion(self, R: np.ndarray) -> np.ndarray: """ - Convert a 3x3 rotation matrix to a quaternion [w, x, y, z]. + Convert a 3x3 rotation matrix to a quaternion [x, y, z, w]. Args: R: 3x3 rotation matrix (numpy array) Returns: - Quaternion as [w, x, y, z] (numpy array) + Quaternion as [x, y, z, w] (numpy array) """ # Ensure the matrix is valid R = np.array(R, dtype=np.float64) @@ -261,4 +261,4 @@ def rotationMatrixToQuaternion(self, R: np.ndarray) -> np.ndarray: y = (R[1, 2] + R[2, 1]) / s z = 0.25 * s - return np.array([w, x, y, z]) + return np.array([x, y, z, w]) diff --git a/mapping/src/vggt_model.py b/mapping/src/vggt_model.py index 58f54b030..14af306ca 100644 --- a/mapping/src/vggt_model.py +++ b/mapping/src/vggt_model.py @@ -459,7 +459,7 @@ def _processOutputs(self, predictions: Dict[str, Any], original_sizes: List[tupl quaternion = self.rotationMatrixToQuaternion(rotation_matrix) camera_poses.append({ - "rotation": quaternion.tolist(), # [w, x, y, z] + "rotation": quaternion.tolist(), # [x, y, z, w] "translation": camera_to_world[:3, 3].tolist() }) intrinsics_list.append(intrinsic_matrix.tolist()) diff --git a/mapping/tests/test_model_interface.py b/mapping/tests/test_model_interface.py index 0cc82c10b..4ee8c5a60 100644 --- a/mapping/tests/test_model_interface.py +++ b/mapping/tests/test_model_interface.py @@ -202,7 +202,7 @@ def test_rotation_matrix_to_quaternion_identity(self): quat = model.rotationMatrixToQuaternion(R) assert quat.shape == (4,) - # Identity quaternion is [1, 0, 0, 0] (w, x, y, z) + # Identity quaternion is [1, 0, 0, 0] (x, y, z, w) np.testing.assert_array_almost_equal(quat, [1.0, 0.0, 0.0, 0.0], decimal=6) def test_rotation_matrix_to_quaternion_90deg_x(self): diff --git a/sample_data/docker-compose-dl-streamer-example.yml b/sample_data/docker-compose-dl-streamer-example.yml index 84e2c0335..4d0ad224f 100644 --- a/sample_data/docker-compose-dl-streamer-example.yml +++ b/sample_data/docker-compose-dl-streamer-example.yml @@ -39,9 +39,15 @@ secrets: mapping-key: file: ${SECRETSDIR}/certs/scenescape-mapping.key +# Profiles: +# - Default (no profile): Core services including camcalibration +# - experimental: All services including mapping and cluster-analytics +# - mapping: Core services + mapping only +# - cluster-analytics: Core services + cluster-analytics only + services: ntpserv: - image: dockurr/chrony + image: dockurr/chrony:4.8 networks: scenescape: # ports: @@ -50,7 +56,7 @@ services: pids_limit: 1000 broker: - image: eclipse-mosquitto + image: eclipse-mosquitto:2.0.22 # ports: # - "1883:1883" configs: @@ -181,7 +187,7 @@ services: pids_limit: 1000 # vdms: - # image: intellabs/vdms:latest + # image: intellabs/vdms:v2.11.0 # init: true # networks: # scenescape: @@ -349,7 +355,7 @@ services: # Init container to fix volume permissions for camcalibration camcalibration-init: - image: alpine:latest + image: alpine:3.22.2 command: sh -c "chown -R ${UID:-1000}:${GID:-1000} /data/netvlad_models /data/media /data/datasets" volumes: - vol-netvlad_models:/data/netvlad_models @@ -407,7 +413,11 @@ services: pids_limit: 1000 mapping: - image: scenescape-mapping + profiles: + - experimental + - mapping + image: scenescape-mapping:${VERSION:-latest} + init: true user: "${USER_UID:-1000}:${USER_GID:-1000}" networks: scenescape: @@ -442,12 +452,15 @@ services: start_period: 10s cluster-analytics: + profiles: + - experimental + - cluster-analytics image: scenescape-cluster-analytics:${VERSION:-latest} init: true networks: scenescape: # ports: - # - "5000:5000" + # - "9443:9443" command: > --broker broker.scenescape.intel.com --brokerauth /run/secrets/controller.auth diff --git a/sample_data/docker-compose-dls-perf.yml b/sample_data/docker-compose-dls-perf.yml index 699d4421a..3a474fd70 100644 --- a/sample_data/docker-compose-dls-perf.yml +++ b/sample_data/docker-compose-dls-perf.yml @@ -33,7 +33,7 @@ secrets: services: ntpserv: - image: dockurr/chrony + image: dockurr/chrony:4.8 networks: scenescape: # ports: @@ -41,7 +41,7 @@ services: restart: on-failure broker: - image: eclipse-mosquitto + image: eclipse-mosquitto:2.0.22 # ports: # - "1883:1883" volumes: @@ -150,7 +150,7 @@ services: restart: always # vdms: - # image: intellabs/vdms:latest + # image: intellabs/vdms:v2.11.0 # init: true # networks: # scenescape: @@ -234,7 +234,7 @@ services: # Init container to fix volume permissions for camcalibration camcalibration-init: - image: alpine:latest + image: alpine:3.22.2 command: sh -c "chown -R ${UID:-1000}:${GID:-1000} /data/netvlad_models /data/media /data/datasets" volumes: - vol-netvlad_models:/data/netvlad_models diff --git a/scene_common/src/scene_common/glb_top_view.py b/scene_common/src/scene_common/glb_top_view.py index 158bf6446..822519ceb 100644 --- a/scene_common/src/scene_common/glb_top_view.py +++ b/scene_common/src/scene_common/glb_top_view.py @@ -30,10 +30,10 @@ def materialToMaterialRecord(mat): mat_record.ao_rough_metal_img = mat.texture_maps["ao_rough_metal"].to_legacy() return mat_record -def renderTopView(mesh, tensor_mesh, glb_size, res_x, res_y): +def renderTopView(tensor_mesh, glb_size, res_x, res_y): """! Renders the top view of the mesh and returns the capture with specified resolution. - @param mesh Triangle mesh geometry. + @param tensor_mesh List of tensor meshes with materials. @param glb_size mesh dimensions. @param res_x width of capture. @param res_y height of capture. @@ -42,25 +42,56 @@ def renderTopView(mesh, tensor_mesh, glb_size, res_x, res_y): @return pixels_per_meter determined pixels per meter. """ renderer = rendering.OffscreenRenderer(res_x, res_y) - mat_record = materialToMaterialRecord(tensor_mesh[0].material) - renderer.scene.add_geometry("mesh", mesh, mat_record) + + # Add tensor meshes with materials (convert Material -> MaterialRecord) + for i, tmesh in enumerate(tensor_mesh): + mat_record = rendering.MaterialRecord() + mat_record.shader = "defaultLit" + + if hasattr(tmesh, 'material') and tmesh.material is not None: + if hasattr(tmesh.material, 'vector_properties'): + for key, value in tmesh.material.vector_properties.items(): + setattr(mat_record, key, value) + if hasattr(tmesh.material, 'scalar_properties'): + for key, value in tmesh.material.scalar_properties.items(): + setattr(mat_record, key, value) + if hasattr(tmesh.material, 'texture_maps'): + for key, value in tmesh.material.texture_maps.items(): + if key == "albedo": + mat_record.albedo_img = value.to_legacy() + elif key == "normal": + mat_record.normal_img = value.to_legacy() + elif key == "ao_rough_metal": + mat_record.ao_rough_metal_img = value.to_legacy() + + renderer.scene.add_geometry(f"mesh_{i}", tmesh, mat_record) + renderer.scene.scene.set_sun_light(SUNLIGHT_DIRECTION, SUNLIGHT_COLOR, SUNLIGHT_INTENSITY) renderer.scene.scene.enable_sun_light(True) renderer.scene.show_axes(False) + floor_width = glb_size[0] floor_height = glb_size[1] aspect_ratio = res_x / res_y if floor_width / floor_height > aspect_ratio: right = floor_width - top = floor_width / aspect_ratio + top = (floor_width) / aspect_ratio else: - right = floor_height * aspect_ratio + right = (floor_height) * aspect_ratio top = floor_height + + # Position camera at origin looking down + renderer.scene.camera.look_at( + [0.0, 0.0, 0.0], + [0.0, 0.0, glb_size.max()], + [0.0, 1.0, 0.0] + ) + renderer.scene.camera.set_projection(renderer.scene.camera.Projection.Ortho, - 0.0, right, 0.0, top, 0, glb_size.max()) + 0.0, right, 0.0, top, 0.0, glb_size.max()) # We use the vertical resolution as the base for all computations pixels_per_meter = res_y / top img = renderer.render_to_image() @@ -90,8 +121,7 @@ def generateOrthoView(scene_obj, glb_file): triangle_mesh.translate((scene_obj.translation_x, scene_obj.translation_y, 0.0)) glb_size = getMeshSize(triangle_mesh) - img, pixels_per_meter = renderTopView(triangle_mesh, - tensor_mesh, + img, pixels_per_meter = renderTopView(tensor_mesh, glb_size, THUMBNAIL_RESOLUTION['x'], THUMBNAIL_RESOLUTION['y']) diff --git a/tests/scripts/build-time.py b/tests/scripts/build-time.py index 24a4057b3..f004371c3 100644 --- a/tests/scripts/build-time.py +++ b/tests/scripts/build-time.py @@ -36,7 +36,7 @@ def run_command(command, description, timed=False): def main(): parser = argparse.ArgumentParser(description="Measure build time.") parser.add_argument("--time-limit", type=int, required=True, help="Time limit in seconds") - parser.add_argument("--build-cmd", default="make build-all", help="Build command to measure") + parser.add_argument("--build-cmd", default="make build-core", help="Build command to measure") parser.add_argument("--test-name", required=True, help="Name of the test") args = parser.parse_args() diff --git a/tools/ppl_runner/README.md b/tools/ppl_runner/README.md index 1d9b494d4..690b9e9b5 100644 --- a/tools/ppl_runner/README.md +++ b/tools/ppl_runner/README.md @@ -11,7 +11,7 @@ The minimum required steps are: - Sample video files are created with `make init-sample-data`. - Python dependencies from `requirements.txt` are installed. -Building Intel® SceneScape with `make build` will perform the steps related to build (not the Python dependencies). +Building Intel® SceneScape with `make build-core` will perform the steps related to build (not the Python dependencies). ## Basic usage diff --git a/version.txt b/version.txt index 59b9db0c7..88fce5fe3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.5.0-dev +2025.2-rc1