From e85b1d1b55b15679750c9d1d618e4608bab3d2e2 Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 20:53:11 -0600 Subject: [PATCH 01/10] Update Docker deployment instructions in README.md for local image building --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c22be8..0633e8a 100644 --- a/README.md +++ b/README.md @@ -473,7 +473,9 @@ ros2 run depth_anything_3_ros2 depth_anything_3_node --ros-args \ ## Docker Deployment -Docker images are provided for easy deployment on both CPU and GPU systems. +Docker configuration files are provided for building and deploying on both CPU and GPU systems. + +> **Important**: No pre-built Docker images are published to Docker Hub or any container registry. You must build the images locally using `docker-compose build` or `docker-compose up` (which auto-builds). ### Quick Start with Docker Compose From f8492448f776dbef544477ab331085afcf148a60 Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 20:53:18 -0600 Subject: [PATCH 02/10] Update README.md to clarify that no pre-built Docker images are available --- docker/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/README.md b/docker/README.md index 573e0db..9989ff9 100644 --- a/docker/README.md +++ b/docker/README.md @@ -30,6 +30,8 @@ docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi ## Quick Start +> **Important**: No pre-built Docker images are published to Docker Hub or any container registry. You must build the images locally using the commands below. Running `docker-compose pull` will fail. + ### Option 1: Docker Compose (Recommended) #### CPU-Only Mode From 2e270b39b5caf9d8f22b317c6ea61c40f3837fb3 Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 20:58:31 -0600 Subject: [PATCH 03/10] Add detailed Docker installation instructions to README.md --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 0633e8a..a1b84db 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,27 @@ Docker configuration files are provided for building and deploying on both CPU a > **Important**: No pre-built Docker images are published to Docker Hub or any container registry. You must build the images locally using `docker-compose build` or `docker-compose up` (which auto-builds). +### Complete Docker Installation (3 Steps) + +```bash +# Step 1: Clone the repository +git clone https://github.com/GerdsenAI/GerdsenAI-Depth-Anything-3-ROS2-Wrapper.git +cd GerdsenAI-Depth-Anything-3-ROS2-Wrapper + +# Step 2: Build and run (choose GPU or CPU) +docker-compose up -d depth-anything-3-gpu # For GPU (requires nvidia-docker) +# OR +docker-compose up -d depth-anything-3-cpu # For CPU-only + +# Step 3: Enter container and run the node +docker exec -it da3_ros2_gpu bash # For GPU container +# OR +docker exec -it da3_ros2_cpu bash # For CPU container + +# Inside the container: +ros2 run depth_anything_3_ros2 depth_anything_3_node --ros-args -p device:=cuda +``` + ### Quick Start with Docker Compose ```bash From 4f93bf2b978b041765f0fbaa54c95f091a33007d Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 20:58:36 -0600 Subject: [PATCH 04/10] Add comprehensive Docker installation instructions to README.md --- .github/copilot-instructions.md | 112 ++++++++++++++++++++++++++++++++ docker/README.md | 21 ++++++ 2 files changed, 133 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..8fa5f8a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,112 @@ +# Depth Anything 3 ROS2 Wrapper - AI Coding Instructions + +## Project Overview + +Camera-agnostic ROS2 (Humble) wrapper for ByteDance's Depth Anything 3 monocular depth estimation. Targets real-time performance (>30 FPS) on NVIDIA Jetson Orin AGX. + +## Architecture + +``` +depth_anything_3_ros2/ +├── depth_anything_3_node.py # Standard ROS2 node (simpler, flexible) +├── depth_anything_3_node_optimized.py # High-performance node (TensorRT, CUDA streams) +├── da3_inference.py # HuggingFace model wrapper (DepthAnything3.from_pretrained) +├── da3_inference_optimized.py # TensorRT/INT8 optimized inference +├── gpu_utils.py # CUDA-accelerated upsampling (GPUDepthUpsampler) +└── utils.py # Depth normalization, colorization, metrics +``` + +**Data Flow**: Camera `sensor_msgs/Image` → Node subscribes on `~/image_raw` → `DA3InferenceWrapper.inference()` → Publishes depth on `~/depth`, colored on `~/depth_colored`, confidence on `~/confidence` + +## Critical Patterns + +### Camera-Agnostic Design (Mandatory) +- **Never** add camera-specific logic to core modules +- Use ROS2 topic remapping for camera integration: `image_topic:=/camera/image_raw` +- Camera configs exist only in `config/camera_configs/*.yaml` and example launch files + +### ROS2 Node Structure +```python +# Standard patterns used throughout: +self.declare_parameter('param_name', default_value) # Always declare with defaults +qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, ...) # Use BEST_EFFORT for images +self.create_subscription(Image, '~/image_raw', self.callback, qos) # Relative topics with ~ +self.create_publisher(Image, '~/depth', 10) +``` + +### Inference Wrapper Pattern +```python +# da3_inference.py loads model from HuggingFace: +from depth_anything_3.api import DepthAnything3 +self._model = DepthAnything3.from_pretrained(model_name) + +# Returns dict: {'depth': np.ndarray, 'confidence': np.ndarray} +result = wrapper.inference(rgb_image, return_confidence=True) +``` + +## Build & Run Commands + +```bash +# Build (source ROS2 first) +source /opt/ros/jazzy/setup.bash # or humble +colcon build --packages-select depth_anything_3_ros2 --symlink-install +source install/setup.bash + +# Run standard node +ros2 launch depth_anything_3_ros2 depth_anything_3.launch.py \ + image_topic:=/camera/image_raw model_name:=depth-anything/DA3-BASE + +# Run optimized node (>30 FPS) +ros2 launch depth_anything_3_ros2 depth_anything_3_optimized.launch.py \ + backend:=tensorrt_int8 model_input_height:=384 + +# Run tests +colcon test --packages-select depth_anything_3_ros2 +colcon test-result --verbose +``` + +## Code Style Requirements + +- **PEP 8** with 88 char line length (Black) +- **Google-style docstrings** with type hints on all functions +- **No emojis** in code, docs, or commit messages +- Naming: `PascalCase` classes, `snake_case` functions, `_private_methods` + +Example: +```python +def process_image(self, image: np.ndarray, normalize: bool = True) -> Dict[str, np.ndarray]: + """ + Process input image and return depth estimation. + + Args: + image: Input RGB image as numpy array (H, W, 3) + normalize: Whether to normalize depth output + + Returns: + Dictionary containing depth map and confidence + """ +``` + +## Key Configuration + +- **Models**: `depth-anything/DA3-SMALL`, `DA3-BASE`, `DA3-LARGE`, `DA3-GIANT` +- **Parameters** in `config/params.yaml`: `model_name`, `device` (cuda/cpu), `inference_height/width`, `colormap` +- **Performance**: Use 384x384 input with DA3-SMALL + TensorRT INT8 for >30 FPS + +## Testing Patterns + +Tests use `unittest` with mocked model loading: +```python +@patch('depth_anything_3_ros2.da3_inference.DepthAnything3') +def test_inference(self, mock_da3): + mock_model = MagicMock() + mock_da3.from_pretrained.return_value = mock_model + # Test logic... +``` + +## Important Files + +- `launch/depth_anything_3.launch.py` - Primary launch with all configurable args +- `launch/multi_camera.launch.py` - Multi-camera namespace isolation pattern +- `OPTIMIZATION_GUIDE.md` - TensorRT conversion and performance tuning +- `docker/README.md` - Docker deployment (CPU/GPU/dev modes) diff --git a/docker/README.md b/docker/README.md index 9989ff9..f974a46 100644 --- a/docker/README.md +++ b/docker/README.md @@ -32,6 +32,27 @@ docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi > **Important**: No pre-built Docker images are published to Docker Hub or any container registry. You must build the images locally using the commands below. Running `docker-compose pull` will fail. +### Complete Docker Installation (3 Steps) + +```bash +# Step 1: Clone the repository +git clone https://github.com/GerdsenAI/GerdsenAI-Depth-Anything-3-ROS2-Wrapper.git +cd GerdsenAI-Depth-Anything-3-ROS2-Wrapper + +# Step 2: Build and run (choose GPU or CPU) +docker-compose up -d depth-anything-3-gpu # For GPU (requires nvidia-docker) +# OR +docker-compose up -d depth-anything-3-cpu # For CPU-only + +# Step 3: Enter container and run the node +docker exec -it da3_ros2_gpu bash # For GPU container +# OR +docker exec -it da3_ros2_cpu bash # For CPU container + +# Inside the container: +ros2 run depth_anything_3_ros2 depth_anything_3_node --ros-args -p device:=cuda +``` + ### Option 1: Docker Compose (Recommended) #### CPU-Only Mode From 6a6cedfdee73142944fe7c75e2b6036e81a58e0d Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 21:32:48 -0600 Subject: [PATCH 05/10] chore: add development tooling configuration - Add .flake8 with Black-compatible settings (line-length 88) - Add mypy.ini with relaxed type checking for third-party imports - Add pyproject.toml with black/pycodestyle configuration - Update .gitignore to exclude .venv/ virtual environment --- .flake8 | 4 ++++ .gitignore | 1 + mypy.ini | 38 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 11 +++++++++++ 4 files changed, 54 insertions(+) create mode 100644 .flake8 create mode 100644 mypy.ini create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..56cb863 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, W503, Q000, I100, I101, I201, D200, D401 +exclude = .venv, build, install, log, .git, __pycache__ diff --git a/.gitignore b/.gitignore index 6c7bc03..b4d741e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ models/ # Virtual environments venv/ +.venv/ ENV/ env/ diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..857bb4b --- /dev/null +++ b/mypy.ini @@ -0,0 +1,38 @@ +[mypy] +# Ignore missing imports for third-party libraries +# These are installed at runtime but may not be in the dev environment +ignore_missing_imports = True + +# Disable strict optional checking for None types +strict_optional = False + +# Ignore errors in third-party modules +[mypy-numpy.*] +ignore_missing_imports = True + +[mypy-torch.*] +ignore_missing_imports = True + +[mypy-PIL.*] +ignore_missing_imports = True + +[mypy-cv2.*] +ignore_missing_imports = True + +[mypy-depth_anything_3.*] +ignore_missing_imports = True + +[mypy-torch2trt.*] +ignore_missing_imports = True + +[mypy-rclpy.*] +ignore_missing_imports = True + +[mypy-sensor_msgs.*] +ignore_missing_imports = True + +[mypy-std_msgs.*] +ignore_missing_imports = True + +[mypy-cv_bridge.*] +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e7e57f3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.black] +line-length = 88 +target-version = ['py310'] + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203", "W503"] + +[tool.pycodestyle] +max-line-length = 88 +ignore = ["E203", "W503"] From ac44aa9d68e8f6297cfa591c98cd98454da9320d Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 21:32:57 -0600 Subject: [PATCH 06/10] fix(docker): quote pip install requirements for shell safety - Quote version specifiers in pip install commands to prevent shell interpretation issues with comparison operators --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9f0aaae..64c3681 100644 --- a/Dockerfile +++ b/Dockerfile @@ -96,12 +96,12 @@ RUN if [ "$BUILD_TYPE" = "cuda-base" ]; then \ # Install other Python dependencies RUN pip3 install --no-cache-dir \ - transformers>=4.35.0 \ - huggingface-hub>=0.19.0 \ - opencv-python>=4.8.0 \ - pillow>=10.0.0 \ - numpy>=1.24.0,<2.0 \ - timm>=0.9.0 + "transformers>=4.35.0" \ + "huggingface-hub>=0.19.0" \ + "opencv-python>=4.8.0" \ + "pillow>=10.0.0" \ + "numpy>=1.24.0,<2.0" \ + "timm>=0.9.0" # Install Depth Anything 3 RUN pip3 install --no-cache-dir \ From 8737498fddeb808dfbf82cacd4127bba82dd8cd0 Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 21:33:07 -0600 Subject: [PATCH 07/10] style: apply consistent code formatting to inference modules - Apply Black-compatible formatting (double quotes, trailing commas) - Improve code readability with consistent line breaks - No functional changes --- depth_anything_3_ros2/__init__.py | 2 +- depth_anything_3_ros2/da3_inference.py | 44 ++++----- .../da3_inference_optimized.py | 98 +++++++++---------- 3 files changed, 66 insertions(+), 78 deletions(-) diff --git a/depth_anything_3_ros2/__init__.py b/depth_anything_3_ros2/__init__.py index 2a2b1b8..1f38f5f 100644 --- a/depth_anything_3_ros2/__init__.py +++ b/depth_anything_3_ros2/__init__.py @@ -4,4 +4,4 @@ A camera-agnostic ROS2 wrapper for Depth Anything 3 monocular depth estimation. """ -__version__ = '1.0.0' +__version__ = "1.0.0" diff --git a/depth_anything_3_ros2/da3_inference.py b/depth_anything_3_ros2/da3_inference.py index 0f796f0..55e528e 100644 --- a/depth_anything_3_ros2/da3_inference.py +++ b/depth_anything_3_ros2/da3_inference.py @@ -27,7 +27,7 @@ def __init__( self, model_name: str = "depth-anything/DA3-BASE", device: str = "cuda", - cache_dir: Optional[str] = None + cache_dir: Optional[str] = None, ): """ Initialize the DA3 inference wrapper. @@ -67,26 +67,25 @@ def _setup_device(self, requested_device: str) -> str: Raises: ValueError: If requested device is invalid """ - if requested_device not in ['cuda', 'cpu']: + if requested_device not in ["cuda", "cpu"]: raise ValueError( - f"Invalid device '{requested_device}'. " - f"Must be 'cuda' or 'cpu'" + f"Invalid device '{requested_device}'. " f"Must be 'cuda' or 'cpu'" ) # Check CUDA availability - if requested_device == 'cuda': + if requested_device == "cuda": if not torch.cuda.is_available(): logger.warning( "CUDA requested but not available. Falling back to CPU. " "Performance may be degraded." ) - return 'cpu' + return "cpu" else: cuda_device = torch.cuda.get_device_name(0) logger.info(f"Using CUDA device: {cuda_device}") - return 'cuda' + return "cuda" - return 'cpu' + return "cpu" def _load_model(self) -> None: """ @@ -104,8 +103,7 @@ def _load_model(self) -> None: # Load model with optional cache directory if self.cache_dir: self._model = DepthAnything3.from_pretrained( - self.model_name, - cache_dir=self.cache_dir + self.model_name, cache_dir=self.cache_dir ) else: self._model = DepthAnything3.from_pretrained(self.model_name) @@ -130,7 +128,7 @@ def inference( self, image: np.ndarray, return_confidence: bool = True, - return_camera_params: bool = False + return_camera_params: bool = False, ) -> Dict[str, np.ndarray]: """ Run depth inference on an input image. @@ -161,9 +159,7 @@ def inference( ) if image.dtype != np.uint8: - logger.warning( - f"Expected uint8 image, got {image.dtype}. Converting..." - ) + logger.warning(f"Expected uint8 image, got {image.dtype}. Converting...") image = image.astype(np.uint8) try: @@ -175,22 +171,20 @@ def inference( prediction = self._model.inference([pil_image]) # Extract results - result = { - 'depth': prediction.depth[0].astype(np.float32) - } + result = {"depth": prediction.depth[0].astype(np.float32)} if return_confidence: - result['confidence'] = prediction.conf[0].astype(np.float32) + result["confidence"] = prediction.conf[0].astype(np.float32) if return_camera_params: - result['extrinsics'] = prediction.extrinsics[0].astype(np.float32) - result['intrinsics'] = prediction.intrinsics[0].astype(np.float32) + result["extrinsics"] = prediction.extrinsics[0].astype(np.float32) + result["intrinsics"] = prediction.intrinsics[0].astype(np.float32) return result except torch.cuda.OutOfMemoryError as e: # Clear CUDA cache on OOM - if self.device == 'cuda': + if self.device == "cuda": torch.cuda.empty_cache() raise RuntimeError( f"CUDA out of memory during inference. Try reducing image size or " @@ -207,16 +201,16 @@ def get_gpu_memory_usage(self) -> Optional[Dict[str, float]]: Dictionary with 'allocated_mb' and 'reserved_mb' if CUDA is available, None otherwise """ - if self.device == 'cuda' and torch.cuda.is_available(): + if self.device == "cuda" and torch.cuda.is_available(): return { - 'allocated_mb': torch.cuda.memory_allocated() / (1024 ** 2), - 'reserved_mb': torch.cuda.memory_reserved() / (1024 ** 2) + "allocated_mb": torch.cuda.memory_allocated() / (1024**2), + "reserved_mb": torch.cuda.memory_reserved() / (1024**2), } return None def clear_cache(self) -> None: """Clear CUDA cache to free up memory.""" - if self.device == 'cuda' and torch.cuda.is_available(): + if self.device == "cuda" and torch.cuda.is_available(): torch.cuda.empty_cache() logger.info("CUDA cache cleared") diff --git a/depth_anything_3_ros2/da3_inference_optimized.py b/depth_anything_3_ros2/da3_inference_optimized.py index d2a39ac..003933a 100644 --- a/depth_anything_3_ros2/da3_inference_optimized.py +++ b/depth_anything_3_ros2/da3_inference_optimized.py @@ -16,7 +16,7 @@ GPUDepthUpsampler, GPUImagePreprocessor, CUDAStreamManager, - GPUMemoryMonitor + GPUMemoryMonitor, ) logger = logging.getLogger(__name__) @@ -24,6 +24,7 @@ class InferenceBackend(Enum): """Available inference backends.""" + PYTORCH = "pytorch" TENSORRT_FP16 = "tensorrt_fp16" TENSORRT_INT8 = "tensorrt_int8" @@ -51,7 +52,7 @@ def __init__( enable_upsampling: bool = True, upsample_mode: str = "bilinear", use_cuda_streams: bool = False, - trt_model_path: Optional[str] = None + trt_model_path: Optional[str] = None, ): """ Initialize optimized DA3 inference wrapper. @@ -78,13 +79,12 @@ def __init__( # Initialize GPU utilities self.upsampler = GPUDepthUpsampler(mode=upsample_mode, device=self.device) self.preprocessor = GPUImagePreprocessor( - target_size=model_input_size, - device=self.device + target_size=model_input_size, device=self.device ) # Initialize CUDA streams self.stream_manager = None - if use_cuda_streams and self.device == 'cuda': + if use_cuda_streams and self.device == "cuda": self.stream_manager = CUDAStreamManager(num_streams=3) # Load model @@ -98,25 +98,28 @@ def __init__( def _setup_device(self, requested_device: str) -> str: """Setup and validate compute device.""" - if requested_device not in ['cuda', 'cpu']: + if requested_device not in ["cuda", "cpu"]: raise ValueError(f"Invalid device: {requested_device}") - if requested_device == 'cuda': + if requested_device == "cuda": if not torch.cuda.is_available(): logger.warning("CUDA requested but not available, falling back to CPU") - return 'cpu' + return "cpu" else: cuda_device = torch.cuda.get_device_name(0) logger.info(f"Using CUDA device: {cuda_device}") - return 'cuda' + return "cuda" - return 'cpu' + return "cpu" def _load_model(self) -> None: """Load model based on selected backend.""" if self.backend == InferenceBackend.PYTORCH: self._load_pytorch_model() - elif self.backend in [InferenceBackend.TENSORRT_FP16, InferenceBackend.TENSORRT_INT8]: + elif self.backend in [ + InferenceBackend.TENSORRT_FP16, + InferenceBackend.TENSORRT_INT8, + ]: self._load_tensorrt_model() else: raise ValueError(f"Unsupported backend: {self.backend}") @@ -130,8 +133,7 @@ def _load_pytorch_model(self) -> None: if self.cache_dir: self._model = DepthAnything3.from_pretrained( - self.model_name, - cache_dir=self.cache_dir + self.model_name, cache_dir=self.cache_dir ) else: self._model = DepthAnything3.from_pretrained(self.model_name) @@ -140,7 +142,7 @@ def _load_pytorch_model(self) -> None: self._model.eval() # Enable mixed precision for FP16 inference - if self.device == 'cuda': + if self.device == "cuda": self._model = self._model.half() # Convert to FP16 logger.info("Enabled FP16 mixed precision") @@ -174,8 +176,7 @@ def _load_tensorrt_model(self) -> None: from torch2trt import TRTModule except ImportError: raise ImportError( - "torch2trt not installed. Install with: " - "pip install torch2trt" + "torch2trt not installed. Install with: " "pip install torch2trt" ) logger.info(f"Loading TensorRT model: {trt_path}") @@ -184,10 +185,13 @@ def _load_tensorrt_model(self) -> None: self._model = TRTModule() # Use weights_only=True for security (PyTorch 1.13+) try: - self._model.load_state_dict(torch.load(trt_path, weights_only=True)) + state_dict = torch.load(trt_path, weights_only=True) + self._model.load_state_dict(state_dict) except TypeError: # Fallback for older PyTorch versions - logger.warning("PyTorch version does not support weights_only parameter") + logger.warning( + "PyTorch version does not support weights_only parameter" + ) self._model.load_state_dict(torch.load(trt_path)) logger.info(f"TensorRT model loaded successfully ({self.backend.value})") @@ -200,7 +204,7 @@ def inference( image: np.ndarray, return_confidence: bool = True, return_camera_params: bool = False, - output_size: Optional[Tuple[int, int]] = None + output_size: Optional[Tuple[int, int]] = None, ) -> Dict[str, np.ndarray]: """ Run optimized depth inference. @@ -251,15 +255,10 @@ def inference( # Run inference based on backend if self.backend == InferenceBackend.PYTORCH: result = self._inference_pytorch( - img_tensor, - return_confidence, - return_camera_params + img_tensor, return_confidence, return_camera_params ) else: - result = self._inference_tensorrt( - img_tensor, - return_confidence - ) + result = self._inference_tensorrt(img_tensor, return_confidence) # Upsample to output size if needed if self.enable_upsampling and output_size != self.model_input_size: @@ -268,7 +267,7 @@ def inference( return result except torch.cuda.OutOfMemoryError as e: - if self.device == 'cuda': + if self.device == "cuda": torch.cuda.empty_cache() raise RuntimeError( f"CUDA out of memory. Try reducing input size or " @@ -281,48 +280,45 @@ def _inference_pytorch( self, img_tensor: torch.Tensor, return_confidence: bool, - return_camera_params: bool + return_camera_params: bool, ) -> Dict[str, np.ndarray]: """Run PyTorch inference.""" from PIL import Image # Convert tensor back to PIL for DA3 API # TODO: Modify DA3 to accept tensors directly - img_numpy = (img_tensor.squeeze(0).permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8) + img_cpu = img_tensor.squeeze(0).permute(1, 2, 0).cpu().numpy() + img_numpy = (img_cpu * 255).astype(np.uint8) pil_image = Image.fromarray(img_numpy) # Run inference - with torch.cuda.amp.autocast(enabled=(self.device == 'cuda')): + with torch.cuda.amp.autocast(enabled=(self.device == "cuda")): prediction = self._model.inference([pil_image]) # Validate prediction if prediction is None: raise RuntimeError("Model returned None prediction") - if not hasattr(prediction, 'depth') or prediction.depth is None: + if not hasattr(prediction, "depth") or prediction.depth is None: raise RuntimeError("Model prediction missing depth output") if len(prediction.depth) == 0: raise RuntimeError("Model returned empty depth map") # Extract results - result = { - 'depth': prediction.depth[0].astype(np.float32) - } + result = {"depth": prediction.depth[0].astype(np.float32)} if return_confidence: - result['confidence'] = prediction.conf[0].astype(np.float32) + result["confidence"] = prediction.conf[0].astype(np.float32) if return_camera_params: - result['extrinsics'] = prediction.extrinsics[0].astype(np.float32) - result['intrinsics'] = prediction.intrinsics[0].astype(np.float32) + result["extrinsics"] = prediction.extrinsics[0].astype(np.float32) + result["intrinsics"] = prediction.intrinsics[0].astype(np.float32) return result def _inference_tensorrt( - self, - img_tensor: torch.Tensor, - return_confidence: bool + self, img_tensor: torch.Tensor, return_confidence: bool ) -> Dict[str, np.ndarray]: """Run TensorRT inference.""" # Run TensorRT inference @@ -332,7 +328,7 @@ def _inference_tensorrt( # Assuming output is depth map, modify based on actual TRT model output if isinstance(output, torch.Tensor): depth = output.squeeze().cpu().numpy().astype(np.float32) - result = {'depth': depth} + result = {"depth": depth} # TensorRT models typically only output depth if return_confidence: @@ -343,7 +339,7 @@ def _inference_tensorrt( "Returning uniform confidence map." ) confidence = np.ones_like(depth, dtype=np.float32) - result['confidence'] = confidence + result["confidence"] = confidence else: raise ValueError("Unexpected TensorRT output format") @@ -351,15 +347,13 @@ def _inference_tensorrt( return result def _upsample_results( - self, - result: Dict[str, np.ndarray], - target_size: Tuple[int, int] + self, result: Dict[str, np.ndarray], target_size: Tuple[int, int] ) -> Dict[str, np.ndarray]: """Upsample depth and confidence to target size on GPU.""" upsampled = {} for key, value in result.items(): - if key in ['depth', 'confidence']: + if key in ["depth", "confidence"]: # Upsample on GPU upsampled[key] = self.upsampler.upsample_numpy(value, target_size) else: @@ -370,13 +364,13 @@ def _upsample_results( def get_gpu_memory_usage(self) -> Optional[Dict[str, float]]: """Get GPU memory usage statistics.""" - if self.device == 'cuda': + if self.device == "cuda": return GPUMemoryMonitor.get_memory_stats() return None def clear_cache(self) -> None: """Clear CUDA cache.""" - if self.device == 'cuda': + if self.device == "cuda": GPUMemoryMonitor.clear_cache() def cleanup(self) -> None: @@ -393,14 +387,14 @@ def cleanup(self) -> None: self.clear_cache() # Clean up GPU utilities - if hasattr(self, 'upsampler'): + if hasattr(self, "upsampler"): del self.upsampler - if hasattr(self, 'preprocessor'): + if hasattr(self, "preprocessor"): del self.preprocessor - if hasattr(self, 'stream_manager') and self.stream_manager is not None: - if hasattr(self.stream_manager, 'cleanup'): + if hasattr(self, "stream_manager") and self.stream_manager is not None: + if hasattr(self.stream_manager, "cleanup"): self.stream_manager.cleanup() logger.info("DA3InferenceOptimized cleanup completed") From 7898612da8479bb657c104d4116b1d25669df397 Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 21:33:16 -0600 Subject: [PATCH 08/10] style: apply consistent code formatting to node implementations - Apply Black-compatible formatting (double quotes, trailing commas) - Improve code readability with consistent line breaks - No functional changes --- .../depth_anything_3_node.py | 144 +++++-------- .../depth_anything_3_node_optimized.py | 201 ++++++++---------- 2 files changed, 144 insertions(+), 201 deletions(-) diff --git a/depth_anything_3_ros2/depth_anything_3_node.py b/depth_anything_3_ros2/depth_anything_3_node.py index 5e45667..c82b40a 100644 --- a/depth_anything_3_ros2/depth_anything_3_node.py +++ b/depth_anything_3_ros2/depth_anything_3_node.py @@ -17,11 +17,7 @@ from cv_bridge import CvBridge, CvBridgeError from .da3_inference import DA3InferenceWrapper -from .utils import ( - normalize_depth, - colorize_depth, - PerformanceMetrics -) +from .utils import normalize_depth, colorize_depth, PerformanceMetrics class DepthAnything3Node(Node): @@ -34,7 +30,7 @@ class DepthAnything3Node(Node): def __init__(self): """Initialize the Depth Anything 3 ROS2 node.""" - super().__init__('depth_anything_3') + super().__init__("depth_anything_3") # Declare parameters self._declare_parameters() @@ -54,9 +50,7 @@ def __init__(self): ) try: self.model = DA3InferenceWrapper( - model_name=self.model_name, - device=self.device, - cache_dir=self.cache_dir + model_name=self.model_name, device=self.device, cache_dir=self.cache_dir ) self.get_logger().info("Model loaded successfully") except Exception as e: @@ -67,45 +61,29 @@ def __init__(self): qos = QoSProfile( reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, - depth=self.queue_size + depth=self.queue_size, ) # Create subscribers self.image_sub = self.create_subscription( - Image, - '~/image_raw', - self.image_callback, - qos + Image, "~/image_raw", self.image_callback, qos ) self.camera_info_sub = self.create_subscription( - CameraInfo, - '~/camera_info', - self.camera_info_callback, - qos + CameraInfo, "~/camera_info", self.camera_info_callback, qos ) # Create publishers - self.depth_pub = self.create_publisher(Image, '~/depth', 10) + self.depth_pub = self.create_publisher(Image, "~/depth", 10) if self.publish_colored: - self.depth_colored_pub = self.create_publisher( - Image, - '~/depth_colored', - 10 - ) + self.depth_colored_pub = self.create_publisher(Image, "~/depth_colored", 10) if self.publish_confidence: - self.confidence_pub = self.create_publisher( - Image, - '~/confidence', - 10 - ) + self.confidence_pub = self.create_publisher(Image, "~/confidence", 10) self.camera_info_pub = self.create_publisher( - CameraInfo, - '~/depth/camera_info', - 10 + CameraInfo, "~/depth/camera_info", 10 ) # Store latest camera info @@ -122,53 +100,53 @@ def __init__(self): def _declare_parameters(self) -> None: """Declare all ROS2 parameters with default values.""" # Model configuration - self.declare_parameter('model_name', 'depth-anything/DA3-BASE') - self.declare_parameter('device', 'cuda') - self.declare_parameter('cache_dir', '') + self.declare_parameter("model_name", "depth-anything/DA3-BASE") + self.declare_parameter("device", "cuda") + self.declare_parameter("cache_dir", "") # Image processing - self.declare_parameter('inference_height', 518) - self.declare_parameter('inference_width', 518) - self.declare_parameter('input_encoding', 'bgr8') + self.declare_parameter("inference_height", 518) + self.declare_parameter("inference_width", 518) + self.declare_parameter("input_encoding", "bgr8") # Output configuration - self.declare_parameter('normalize_depth', True) - self.declare_parameter('publish_colored', True) - self.declare_parameter('publish_confidence', True) - self.declare_parameter('colormap', 'turbo') + self.declare_parameter("normalize_depth", True) + self.declare_parameter("publish_colored", True) + self.declare_parameter("publish_confidence", True) + self.declare_parameter("colormap", "turbo") # Performance - self.declare_parameter('queue_size', 1) - self.declare_parameter('processing_threads', 1) + self.declare_parameter("queue_size", 1) + self.declare_parameter("processing_threads", 1) # Logging - self.declare_parameter('log_inference_time', False) + self.declare_parameter("log_inference_time", False) def _load_parameters(self) -> None: """Load parameters from ROS2 parameter server.""" # Model configuration - self.model_name = self.get_parameter('model_name').value - self.device = self.get_parameter('device').value - cache_dir_param = self.get_parameter('cache_dir').value + self.model_name = self.get_parameter("model_name").value + self.device = self.get_parameter("device").value + cache_dir_param = self.get_parameter("cache_dir").value self.cache_dir = cache_dir_param if cache_dir_param else None # Image processing - self.inference_height = self.get_parameter('inference_height').value - self.inference_width = self.get_parameter('inference_width').value - self.input_encoding = self.get_parameter('input_encoding').value + self.inference_height = self.get_parameter("inference_height").value + self.inference_width = self.get_parameter("inference_width").value + self.input_encoding = self.get_parameter("input_encoding").value # Output configuration - self.normalize_depth_output = self.get_parameter('normalize_depth').value - self.publish_colored = self.get_parameter('publish_colored').value - self.publish_confidence = self.get_parameter('publish_confidence').value - self.colormap = self.get_parameter('colormap').value + self.normalize_depth_output = self.get_parameter("normalize_depth").value + self.publish_colored = self.get_parameter("publish_colored").value + self.publish_confidence = self.get_parameter("publish_confidence").value + self.colormap = self.get_parameter("colormap").value # Performance - self.queue_size = self.get_parameter('queue_size').value - self.processing_threads = self.get_parameter('processing_threads').value + self.queue_size = self.get_parameter("queue_size").value + self.processing_threads = self.get_parameter("processing_threads").value # Logging - self.log_inference_time = self.get_parameter('log_inference_time').value + self.log_inference_time = self.get_parameter("log_inference_time").value def camera_info_callback(self, msg: CameraInfo) -> None: """ @@ -191,15 +169,15 @@ def image_callback(self, msg: Image) -> None: try: # Convert ROS Image to OpenCV format try: - cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding='rgb8') + cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="rgb8") except CvBridgeError as e: - self.get_logger().error(f'CV Bridge conversion failed: {e}') + self.get_logger().error(f"CV Bridge conversion failed: {e}") return # Ensure image is in correct format (RGB, uint8) if cv_image.dtype != np.uint8: self.get_logger().warn( - f'Image dtype is {cv_image.dtype}, converting to uint8' + f"Image dtype is {cv_image.dtype}, converting to uint8" ) cv_image = cv_image.astype(np.uint8) @@ -209,16 +187,16 @@ def image_callback(self, msg: Image) -> None: result = self.model.inference( cv_image, return_confidence=self.publish_confidence, - return_camera_params=False + return_camera_params=False, ) except Exception as e: - self.get_logger().error(f'Inference failed: {e}') + self.get_logger().error(f"Inference failed: {e}") return inference_time = time.time() - inference_start # Extract depth map - depth_map = result['depth'] + depth_map = result["depth"] # Normalize if requested if self.normalize_depth_output: @@ -232,8 +210,8 @@ def image_callback(self, msg: Image) -> None: self._publish_colored_depth(depth_map, msg.header) # Publish confidence map - if self.publish_confidence and 'confidence' in result: - self._publish_confidence(result['confidence'], msg.header) + if self.publish_confidence and "confidence" in result: + self._publish_confidence(result["confidence"], msg.header) # Publish camera info if self.latest_camera_info is not None: @@ -246,7 +224,7 @@ def image_callback(self, msg: Image) -> None: self.metrics.update(inference_time, total_time) except Exception as e: - self.get_logger().error(f'Unexpected error in image callback: {e}') + self.get_logger().error(f"Unexpected error in image callback: {e}") def _publish_depth(self, depth_map: np.ndarray, header: Header) -> None: """ @@ -257,17 +235,13 @@ def _publish_depth(self, depth_map: np.ndarray, header: Header) -> None: header: Original image header for timestamp and frame_id """ try: - depth_msg = self.bridge.cv2_to_imgmsg(depth_map, encoding='32FC1') + depth_msg = self.bridge.cv2_to_imgmsg(depth_map, encoding="32FC1") depth_msg.header = header self.depth_pub.publish(depth_msg) except CvBridgeError as e: - self.get_logger().error(f'Failed to publish depth map: {e}') + self.get_logger().error(f"Failed to publish depth map: {e}") - def _publish_colored_depth( - self, - depth_map: np.ndarray, - header: Header - ) -> None: + def _publish_colored_depth(self, depth_map: np.ndarray, header: Header) -> None: """ Publish colorized depth visualization. @@ -278,23 +252,17 @@ def _publish_colored_depth( try: # Colorize depth colored_depth = colorize_depth( - depth_map, - colormap=self.colormap, - normalize=True + depth_map, colormap=self.colormap, normalize=True ) # Convert to ROS message - colored_msg = self.bridge.cv2_to_imgmsg(colored_depth, encoding='bgr8') + colored_msg = self.bridge.cv2_to_imgmsg(colored_depth, encoding="bgr8") colored_msg.header = header self.depth_colored_pub.publish(colored_msg) except Exception as e: - self.get_logger().error(f'Failed to publish colored depth: {e}') + self.get_logger().error(f"Failed to publish colored depth: {e}") - def _publish_confidence( - self, - confidence_map: np.ndarray, - header: Header - ) -> None: + def _publish_confidence(self, confidence_map: np.ndarray, header: Header) -> None: """ Publish confidence map. @@ -303,11 +271,11 @@ def _publish_confidence( header: Original image header for timestamp and frame_id """ try: - conf_msg = self.bridge.cv2_to_imgmsg(confidence_map, encoding='32FC1') + conf_msg = self.bridge.cv2_to_imgmsg(confidence_map, encoding="32FC1") conf_msg.header = header self.confidence_pub.publish(conf_msg) except CvBridgeError as e: - self.get_logger().error(f'Failed to publish confidence map: {e}') + self.get_logger().error(f"Failed to publish confidence map: {e}") def _log_performance(self) -> None: """Log performance metrics periodically.""" @@ -332,7 +300,7 @@ def _log_performance(self) -> None: def destroy_node(self) -> None: """Clean up resources on node shutdown.""" self.get_logger().info("Shutting down Depth Anything 3 node") - if hasattr(self, 'model'): + if hasattr(self, "model"): del self.model super().destroy_node() @@ -358,5 +326,5 @@ def main(args=None): rclpy.shutdown() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/depth_anything_3_ros2/depth_anything_3_node_optimized.py b/depth_anything_3_ros2/depth_anything_3_node_optimized.py index ebde67d..22c0434 100644 --- a/depth_anything_3_ros2/depth_anything_3_node_optimized.py +++ b/depth_anything_3_ros2/depth_anything_3_node_optimized.py @@ -37,7 +37,7 @@ class DepthAnything3NodeOptimized(Node): def __init__(self): """Initialize the optimized Depth Anything 3 ROS2 node.""" - super().__init__('depth_anything_3_optimized') + super().__init__("depth_anything_3_optimized") # Declare parameters self._declare_parameters() @@ -67,7 +67,7 @@ def __init__(self): enable_upsampling=self.enable_upsampling, upsample_mode=self.upsample_mode, use_cuda_streams=self.use_cuda_streams, - trt_model_path=self.trt_model_path + trt_model_path=self.trt_model_path, ) self.get_logger().info("Optimized model loaded successfully") except Exception as e: @@ -78,45 +78,29 @@ def __init__(self): qos = QoSProfile( reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, - depth=self.queue_size + depth=self.queue_size, ) # Create subscribers self.image_sub = self.create_subscription( - Image, - '~/image_raw', - self.image_callback, - qos + Image, "~/image_raw", self.image_callback, qos ) self.camera_info_sub = self.create_subscription( - CameraInfo, - '~/camera_info', - self.camera_info_callback, - qos + CameraInfo, "~/camera_info", self.camera_info_callback, qos ) # Create publishers - self.depth_pub = self.create_publisher(Image, '~/depth', 10) + self.depth_pub = self.create_publisher(Image, "~/depth", 10) if self.publish_colored: - self.depth_colored_pub = self.create_publisher( - Image, - '~/depth_colored', - 10 - ) + self.depth_colored_pub = self.create_publisher(Image, "~/depth_colored", 10) if self.publish_confidence: - self.confidence_pub = self.create_publisher( - Image, - '~/confidence', - 10 - ) + self.confidence_pub = self.create_publisher(Image, "~/confidence", 10) self.camera_info_pub = self.create_publisher( - CameraInfo, - '~/depth/camera_info', - 10 + CameraInfo, "~/depth/camera_info", 10 ) # Store latest camera info @@ -146,81 +130,81 @@ def __init__(self): def _declare_parameters(self) -> None: """Declare all ROS2 parameters.""" # Model configuration - self.declare_parameter('model_name', 'depth-anything/DA3-SMALL') - self.declare_parameter('backend', 'pytorch') # pytorch, tensorrt_fp16, tensorrt_int8 - self.declare_parameter('device', 'cuda') - self.declare_parameter('cache_dir', '') - self.declare_parameter('trt_model_path', '') + self.declare_parameter("model_name", "depth-anything/DA3-SMALL") + # Backend options: pytorch, tensorrt_fp16, tensorrt_int8 + self.declare_parameter("backend", "pytorch") + self.declare_parameter("device", "cuda") + self.declare_parameter("cache_dir", "") + self.declare_parameter("trt_model_path", "") # Image processing - self.declare_parameter('model_input_height', 384) - self.declare_parameter('model_input_width', 384) - self.declare_parameter('output_height', 1080) - self.declare_parameter('output_width', 1920) - self.declare_parameter('input_encoding', 'bgr8') + self.declare_parameter("model_input_height", 384) + self.declare_parameter("model_input_width", 384) + self.declare_parameter("output_height", 1080) + self.declare_parameter("output_width", 1920) + self.declare_parameter("input_encoding", "bgr8") # GPU optimization - self.declare_parameter('enable_upsampling', True) - self.declare_parameter('upsample_mode', 'bilinear') - self.declare_parameter('use_cuda_streams', False) + self.declare_parameter("enable_upsampling", True) + self.declare_parameter("upsample_mode", "bilinear") + self.declare_parameter("use_cuda_streams", False) # Output configuration - self.declare_parameter('normalize_depth', True) - self.declare_parameter('publish_colored', True) - self.declare_parameter('publish_confidence', True) - self.declare_parameter('colormap', 'turbo') - self.declare_parameter('async_colorization', True) - self.declare_parameter('check_subscribers', True) + self.declare_parameter("normalize_depth", True) + self.declare_parameter("publish_colored", True) + self.declare_parameter("publish_confidence", True) + self.declare_parameter("colormap", "turbo") + self.declare_parameter("async_colorization", True) + self.declare_parameter("check_subscribers", True) # Performance - self.declare_parameter('queue_size', 1) - self.declare_parameter('log_inference_time', True) + self.declare_parameter("queue_size", 1) + self.declare_parameter("log_inference_time", True) def _load_parameters(self) -> None: """Load parameters from ROS2 parameter server.""" # Model configuration - self.model_name = self.get_parameter('model_name').value - self.backend = self.get_parameter('backend').value - self.device = self.get_parameter('device').value - cache_dir_param = self.get_parameter('cache_dir').value + self.model_name = self.get_parameter("model_name").value + self.backend = self.get_parameter("backend").value + self.device = self.get_parameter("device").value + cache_dir_param = self.get_parameter("cache_dir").value self.cache_dir = cache_dir_param if cache_dir_param else None - trt_path_param = self.get_parameter('trt_model_path').value + trt_path_param = self.get_parameter("trt_model_path").value self.trt_model_path = trt_path_param if trt_path_param else None # Image processing - input_h = self.get_parameter('model_input_height').value - input_w = self.get_parameter('model_input_width').value + input_h = self.get_parameter("model_input_height").value + input_w = self.get_parameter("model_input_width").value self.model_input_size = (input_h, input_w) - output_h = self.get_parameter('output_height').value - output_w = self.get_parameter('output_width').value + output_h = self.get_parameter("output_height").value + output_w = self.get_parameter("output_width").value self.output_resolution = (output_h, output_w) - self.input_encoding = self.get_parameter('input_encoding').value + self.input_encoding = self.get_parameter("input_encoding").value # GPU optimization - self.enable_upsampling = self.get_parameter('enable_upsampling').value - self.upsample_mode = self.get_parameter('upsample_mode').value - self.use_cuda_streams = self.get_parameter('use_cuda_streams').value + self.enable_upsampling = self.get_parameter("enable_upsampling").value + self.upsample_mode = self.get_parameter("upsample_mode").value + self.use_cuda_streams = self.get_parameter("use_cuda_streams").value # Output configuration - self.normalize_depth_output = self.get_parameter('normalize_depth').value - self.publish_colored = self.get_parameter('publish_colored').value - self.publish_confidence = self.get_parameter('publish_confidence').value - self.colormap = self.get_parameter('colormap').value - self.async_colorization = self.get_parameter('async_colorization').value - self.check_subscribers = self.get_parameter('check_subscribers').value + self.normalize_depth_output = self.get_parameter("normalize_depth").value + self.publish_colored = self.get_parameter("publish_colored").value + self.publish_confidence = self.get_parameter("publish_confidence").value + self.colormap = self.get_parameter("colormap").value + self.async_colorization = self.get_parameter("async_colorization").value + self.check_subscribers = self.get_parameter("check_subscribers").value # Performance - self.queue_size = self.get_parameter('queue_size').value - self.log_inference_time = self.get_parameter('log_inference_time').value + self.queue_size = self.get_parameter("queue_size").value + self.log_inference_time = self.get_parameter("log_inference_time").value def _setup_async_colorization(self) -> None: """Setup async colorization thread.""" self.colorization_queue = Queue(maxsize=2) self.colorization_thread = threading.Thread( - target=self._colorization_worker, - daemon=True + target=self._colorization_worker, daemon=True ) self.colorization_thread.start() self.get_logger().info("Async colorization thread started") @@ -239,27 +223,25 @@ def _colorization_worker(self) -> None: # Colorize depth colored_depth = colorize_depth( - depth_map, - colormap=self.colormap, - normalize=True + depth_map, colormap=self.colormap, normalize=True ) # Publish with thread safety try: with self._shutdown_lock: - if self._running and hasattr(self, 'depth_colored_pub'): + if self._running and hasattr(self, "depth_colored_pub"): colored_msg = self.bridge.cv2_to_imgmsg( - colored_depth, encoding='bgr8' + colored_depth, encoding="bgr8" ) colored_msg.header = header self.depth_colored_pub.publish(colored_msg) except CvBridgeError as e: - self.get_logger().error(f'Failed to publish colored depth: {e}') + self.get_logger().error(f"Failed to publish colored depth: {e}") except Empty: continue except Exception as e: - self.get_logger().error(f'Error in colorization worker: {e}') + self.get_logger().error(f"Error in colorization worker: {e}") self.get_logger().info("Colorization worker thread exiting") @@ -278,14 +260,14 @@ def image_callback(self, msg: Image) -> None: try: # Convert ROS Image to OpenCV format try: - cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding='rgb8') + cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="rgb8") except CvBridgeError as e: - self.get_logger().error(f'CV Bridge conversion failed: {e}') + self.get_logger().error(f"CV Bridge conversion failed: {e}") return # Validate converted image if cv_image is None or cv_image.size == 0: - self.get_logger().error('Received empty image after conversion') + self.get_logger().error("Received empty image after conversion") return # Ensure correct format @@ -299,16 +281,16 @@ def image_callback(self, msg: Image) -> None: cv_image, return_confidence=self.publish_confidence, return_camera_params=False, - output_size=self.output_resolution + output_size=self.output_resolution, ) except Exception as e: - self.get_logger().error(f'Inference failed: {e}') + self.get_logger().error(f"Inference failed: {e}") return inference_time = time.time() - inference_start # Extract depth map - depth_map = result['depth'] + depth_map = result["depth"] # Normalize if requested if self.normalize_depth_output: @@ -318,8 +300,8 @@ def image_callback(self, msg: Image) -> None: self._publish_depth(depth_map, msg.header) # Publish confidence map if requested - if self.publish_confidence and 'confidence' in result: - self._publish_confidence(result['confidence'], msg.header) + if self.publish_confidence and "confidence" in result: + self._publish_confidence(result["confidence"], msg.header) # Handle colored depth if self.publish_colored: @@ -327,10 +309,13 @@ def image_callback(self, msg: Image) -> None: if self.check_subscribers: if self.depth_colored_pub.get_subscription_count() == 0: pass # Skip colorization if no subscribers - elif self.async_colorization and self.colorization_queue is not None: + elif ( + self.async_colorization and self.colorization_queue is not None + ): # Async colorization (off critical path) try: - self.colorization_queue.put_nowait((depth_map.copy(), msg.header)) + item = (depth_map.copy(), msg.header) + self.colorization_queue.put_nowait(item) except Full: # Queue full, skip this frame (OK for real-time) pass @@ -340,7 +325,8 @@ def image_callback(self, msg: Image) -> None: elif self.async_colorization and self.colorization_queue is not None: # Always colorize async try: - self.colorization_queue.put_nowait((depth_map.copy(), msg.header)) + item = (depth_map.copy(), msg.header) + self.colorization_queue.put_nowait(item) except Full: # Queue full, skip this frame (OK for real-time) pass @@ -351,6 +337,7 @@ def image_callback(self, msg: Image) -> None: # Publish camera info (create a copy to avoid modifying original) if self.latest_camera_info is not None: from copy import deepcopy + camera_info_msg = deepcopy(self.latest_camera_info) camera_info_msg.header = msg.header self.camera_info_pub.publish(camera_info_msg) @@ -360,7 +347,7 @@ def image_callback(self, msg: Image) -> None: self.metrics.update(inference_time, total_time) except Exception as e: - self.get_logger().error(f'Unexpected error in image callback: {e}') + self.get_logger().error(f"Unexpected error in image callback: {e}") def _normalize_depth_fast(self, depth: np.ndarray) -> np.ndarray: """Fast depth normalization.""" @@ -375,43 +362,33 @@ def _normalize_depth_fast(self, depth: np.ndarray) -> np.ndarray: def _publish_depth(self, depth_map: np.ndarray, header: Header) -> None: """Publish depth map.""" try: - depth_msg = self.bridge.cv2_to_imgmsg(depth_map, encoding='32FC1') + depth_msg = self.bridge.cv2_to_imgmsg(depth_map, encoding="32FC1") depth_msg.header = header self.depth_pub.publish(depth_msg) except CvBridgeError as e: - self.get_logger().error(f'Failed to publish depth map: {e}') + self.get_logger().error(f"Failed to publish depth map: {e}") - def _publish_colored_depth( - self, - depth_map: np.ndarray, - header: Header - ) -> None: + def _publish_colored_depth(self, depth_map: np.ndarray, header: Header) -> None: """Publish colorized depth (synchronous).""" try: colored_depth = colorize_depth( - depth_map, - colormap=self.colormap, - normalize=True + depth_map, colormap=self.colormap, normalize=True ) - colored_msg = self.bridge.cv2_to_imgmsg(colored_depth, encoding='bgr8') + colored_msg = self.bridge.cv2_to_imgmsg(colored_depth, encoding="bgr8") colored_msg.header = header self.depth_colored_pub.publish(colored_msg) except Exception as e: - self.get_logger().error(f'Failed to publish colored depth: {e}') + self.get_logger().error(f"Failed to publish colored depth: {e}") - def _publish_confidence( - self, - confidence_map: np.ndarray, - header: Header - ) -> None: + def _publish_confidence(self, confidence_map: np.ndarray, header: Header) -> None: """Publish confidence map.""" try: - conf_msg = self.bridge.cv2_to_imgmsg(confidence_map, encoding='32FC1') + conf_msg = self.bridge.cv2_to_imgmsg(confidence_map, encoding="32FC1") conf_msg.header = header self.confidence_pub.publish(conf_msg) except CvBridgeError as e: - self.get_logger().error(f'Failed to publish confidence map: {e}') + self.get_logger().error(f"Failed to publish confidence map: {e}") def _log_performance(self) -> None: """Log performance metrics.""" @@ -447,9 +424,7 @@ def destroy_node(self) -> None: self.colorization_thread.join(timeout=5.0) if self.colorization_thread.is_alive(): - self.get_logger().warning( - "Colorization thread did not exit cleanly" - ) + self.get_logger().warning("Colorization thread did not exit cleanly") # Clean up queue if self.colorization_queue is not None: @@ -461,8 +436,8 @@ def destroy_node(self) -> None: break # Clean up model with explicit cleanup method - if hasattr(self, 'model'): - if hasattr(self.model, 'cleanup'): + if hasattr(self, "model"): + if hasattr(self.model, "cleanup"): try: self.model.cleanup() except Exception as e: @@ -488,5 +463,5 @@ def main(args=None): rclpy.shutdown() -if __name__ == '__main__': +if __name__ == "__main__": main() From fe5dbc0b90e69e7dc6d8d44a588579600fae9f50 Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 21:33:25 -0600 Subject: [PATCH 09/10] style: apply consistent code formatting to utility modules - Apply Black-compatible formatting (double quotes, trailing commas) - Improve code readability with consistent line breaks - No functional changes --- depth_anything_3_ros2/gpu_utils.py | 69 +++++++++++------------------- depth_anything_3_ros2/utils.py | 65 ++++++++++++++-------------- 2 files changed, 56 insertions(+), 78 deletions(-) diff --git a/depth_anything_3_ros2/gpu_utils.py b/depth_anything_3_ros2/gpu_utils.py index c9131ee..24c7b27 100644 --- a/depth_anything_3_ros2/gpu_utils.py +++ b/depth_anything_3_ros2/gpu_utils.py @@ -27,11 +27,7 @@ class GPUDepthUpsampler: All operations are performed on GPU to minimize CPU-GPU transfers. """ - def __init__( - self, - mode: str = 'bilinear', - device: str = 'cuda' - ): + def __init__(self, mode: str = "bilinear", device: str = "cuda"): """ Initialize GPU upsampler. @@ -42,23 +38,21 @@ def __init__( self.mode = mode self.device = device - if mode not in ['bilinear', 'bicubic', 'nearest']: + if mode not in ["bilinear", "bicubic", "nearest"]: raise ValueError( f"Invalid mode '{mode}'. " f"Must be 'bilinear', 'bicubic', or 'nearest'" ) # Check CUDA availability - if device == 'cuda' and not torch.cuda.is_available(): + if device == "cuda" and not torch.cuda.is_available(): logger.warning("CUDA not available, falling back to CPU") - self.device = 'cpu' + self.device = "cpu" logger.info(f"GPU upsampler initialized: mode={mode}, device={self.device}") def upsample_tensor( - self, - tensor: torch.Tensor, - target_size: Tuple[int, int] + self, tensor: torch.Tensor, target_size: Tuple[int, int] ) -> torch.Tensor: """ Upsample a tensor on GPU. @@ -93,15 +87,13 @@ def upsample_tensor( tensor, size=target_size, mode=self.mode, - align_corners=False if self.mode != 'nearest' else None + align_corners=False if self.mode != "nearest" else None, ) return upsampled def upsample_numpy( - self, - array: np.ndarray, - target_size: Tuple[int, int] + self, array: np.ndarray, target_size: Tuple[int, int] ) -> np.ndarray: """ Upsample a numpy array using GPU acceleration. @@ -145,7 +137,7 @@ def upsample_numpy( tensor, size=target_size, mode=self.mode, - align_corners=False if self.mode != 'nearest' else None + align_corners=False if self.mode != "nearest" else None, ) # Convert back to numpy @@ -165,11 +157,7 @@ class GPUImagePreprocessor: to minimize CPU-GPU transfer overhead. """ - def __init__( - self, - target_size: Tuple[int, int] = (384, 384), - device: str = 'cuda' - ): + def __init__(self, target_size: Tuple[int, int] = (384, 384), device: str = "cuda"): """ Initialize GPU preprocessor. @@ -180,14 +168,12 @@ def __init__( self.target_size = target_size self.device = device - if device == 'cuda' and not torch.cuda.is_available(): + if device == "cuda" and not torch.cuda.is_available(): logger.warning("CUDA not available, falling back to CPU") - self.device = 'cpu' + self.device = "cpu" def preprocess( - self, - image: np.ndarray, - return_tensor: bool = True + self, image: np.ndarray, return_tensor: bool = True ) -> Union[torch.Tensor, np.ndarray]: """ Preprocess image for model input on GPU. @@ -216,10 +202,7 @@ def preprocess( # Resize to target size if tensor.shape[2:] != self.target_size: tensor = F.interpolate( - tensor, - size=self.target_size, - mode='bilinear', - align_corners=False + tensor, size=self.target_size, mode="bilinear", align_corners=False ) if return_tensor: @@ -303,9 +286,7 @@ def tensor_to_numpy_gpu(tensor: torch.Tensor) -> np.ndarray: def numpy_to_tensor_gpu( - array: np.ndarray, - device: str = 'cuda', - dtype: torch.dtype = torch.float32 + array: np.ndarray, device: str = "cuda", dtype: torch.dtype = torch.float32 ) -> torch.Tensor: """ Convert numpy array to GPU tensor with minimal overhead. @@ -365,27 +346,27 @@ def get_memory_stats() -> dict: """ if not torch.cuda.is_available(): return { - 'allocated_mb': 0.0, - 'reserved_mb': 0.0, - 'free_mb': 0.0, - 'total_mb': 0.0 + "allocated_mb": 0.0, + "reserved_mb": 0.0, + "free_mb": 0.0, + "total_mb": 0.0, } # Use current device instead of hardcoded device 0 device_id = torch.cuda.current_device() - allocated = torch.cuda.memory_allocated(device_id) / (1024 ** 2) - reserved = torch.cuda.memory_reserved(device_id) / (1024 ** 2) + allocated = torch.cuda.memory_allocated(device_id) / (1024**2) + reserved = torch.cuda.memory_reserved(device_id) / (1024**2) # Get total memory for current device - total = torch.cuda.get_device_properties(device_id).total_memory / (1024 ** 2) + total = torch.cuda.get_device_properties(device_id).total_memory / (1024**2) free = total - allocated return { - 'allocated_mb': allocated, - 'reserved_mb': reserved, - 'free_mb': free, - 'total_mb': total + "allocated_mb": allocated, + "reserved_mb": reserved, + "free_mb": free, + "total_mb": total, } @staticmethod diff --git a/depth_anything_3_ros2/utils.py b/depth_anything_3_ros2/utils.py index 49c6337..6edf950 100644 --- a/depth_anything_3_ros2/utils.py +++ b/depth_anything_3_ros2/utils.py @@ -10,8 +10,9 @@ import cv2 -def normalize_depth(depth: np.ndarray, min_val: Optional[float] = None, - max_val: Optional[float] = None) -> np.ndarray: +def normalize_depth( + depth: np.ndarray, min_val: Optional[float] = None, max_val: Optional[float] = None +) -> np.ndarray: """ Normalize depth map to [0, 1] range. @@ -37,9 +38,7 @@ def normalize_depth(depth: np.ndarray, min_val: Optional[float] = None, def colorize_depth( - depth: np.ndarray, - colormap: str = 'turbo', - normalize: bool = True + depth: np.ndarray, colormap: str = "turbo", normalize: bool = True ) -> np.ndarray: """ Colorize depth map for visualization. @@ -58,21 +57,21 @@ def colorize_depth( """ # Map colormap names to OpenCV constants colormap_dict = { - 'turbo': cv2.COLORMAP_TURBO, - 'viridis': cv2.COLORMAP_VIRIDIS, - 'plasma': cv2.COLORMAP_PLASMA, - 'magma': cv2.COLORMAP_MAGMA, - 'jet': cv2.COLORMAP_JET, - 'hot': cv2.COLORMAP_HOT, - 'cool': cv2.COLORMAP_COOL, - 'spring': cv2.COLORMAP_SPRING, - 'summer': cv2.COLORMAP_SUMMER, - 'autumn': cv2.COLORMAP_AUTUMN, - 'winter': cv2.COLORMAP_WINTER, - 'bone': cv2.COLORMAP_BONE, - 'hsv': cv2.COLORMAP_HSV, - 'parula': cv2.COLORMAP_PARULA, - 'inferno': cv2.COLORMAP_INFERNO, + "turbo": cv2.COLORMAP_TURBO, + "viridis": cv2.COLORMAP_VIRIDIS, + "plasma": cv2.COLORMAP_PLASMA, + "magma": cv2.COLORMAP_MAGMA, + "jet": cv2.COLORMAP_JET, + "hot": cv2.COLORMAP_HOT, + "cool": cv2.COLORMAP_COOL, + "spring": cv2.COLORMAP_SPRING, + "summer": cv2.COLORMAP_SUMMER, + "autumn": cv2.COLORMAP_AUTUMN, + "winter": cv2.COLORMAP_WINTER, + "bone": cv2.COLORMAP_BONE, + "hsv": cv2.COLORMAP_HSV, + "parula": cv2.COLORMAP_PARULA, + "inferno": cv2.COLORMAP_INFERNO, } if colormap.lower() not in colormap_dict: @@ -100,7 +99,7 @@ def resize_image( image: np.ndarray, target_size: Tuple[int, int], keep_aspect_ratio: bool = True, - interpolation: int = cv2.INTER_LINEAR + interpolation: int = cv2.INTER_LINEAR, ) -> np.ndarray: """ Resize image to target size. @@ -135,7 +134,7 @@ def resize_image( # Center the resized image in padded image y_offset = (target_h - new_h) // 2 x_offset = (target_w - new_w) // 2 - padded[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized + padded[y_offset : y_offset + new_h, x_offset : x_offset + new_w] = resized return padded else: @@ -144,8 +143,7 @@ def resize_image( def depth_to_meters( - depth: np.ndarray, - depth_range: Tuple[float, float] = (0.1, 10.0) + depth: np.ndarray, depth_range: Tuple[float, float] = (0.1, 10.0) ) -> np.ndarray: """ Convert normalized depth [0, 1] to metric depth in meters. @@ -162,8 +160,7 @@ def depth_to_meters( def compute_confidence_mask( - confidence: np.ndarray, - threshold: float = 0.5 + confidence: np.ndarray, threshold: float = 0.5 ) -> np.ndarray: """ Compute binary confidence mask. @@ -223,10 +220,10 @@ def get_metrics(self) -> dict: """ if not self.inference_times: return { - 'avg_inference_ms': 0.0, - 'avg_total_ms': 0.0, - 'fps': 0.0, - 'frame_count': 0 + "avg_inference_ms": 0.0, + "avg_total_ms": 0.0, + "fps": 0.0, + "frame_count": 0, } avg_inference = np.mean(self.inference_times) * 1000 # Convert to ms @@ -234,10 +231,10 @@ def get_metrics(self) -> dict: fps = 1.0 / np.mean(self.total_times) if np.mean(self.total_times) > 0 else 0.0 return { - 'avg_inference_ms': avg_inference, - 'avg_total_ms': avg_total, - 'fps': fps, - 'frame_count': self.frame_count + "avg_inference_ms": avg_inference, + "avg_total_ms": avg_total, + "fps": fps, + "frame_count": self.frame_count, } def reset(self) -> None: From 7fa2321b1e41f727452aafb0e2f22651abfb4c9e Mon Sep 17 00:00:00 2001 From: gerdsenai-admin Date: Mon, 8 Dec 2025 21:33:35 -0600 Subject: [PATCH 10/10] fix(tests): apply code formatting and fix lint issues - Remove unused Mock imports (F401) - Fix line length issues (E501) - Fix inline comment spacing (E261) - Apply Black-compatible formatting (double quotes, trailing commas) --- test/test_generic_camera.py | 58 ++++++++++++++------------- test/test_inference.py | 78 +++++++++++++++++++------------------ test/test_node.py | 41 +++++++++---------- 3 files changed, 90 insertions(+), 87 deletions(-) diff --git a/test/test_generic_camera.py b/test/test_generic_camera.py index 6cd89f0..b6f1511 100644 --- a/test/test_generic_camera.py +++ b/test/test_generic_camera.py @@ -6,7 +6,7 @@ """ import unittest -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import patch, MagicMock import numpy as np import rclpy @@ -32,7 +32,7 @@ def _create_image_msg( height: int, width: int, encoding: str, - frame_id: str = 'camera_optical_frame' + frame_id: str = "camera_optical_frame", ) -> Image: """ Create a test Image message. @@ -55,10 +55,12 @@ def _create_image_msg( msg.is_bigendian = 0 # Create appropriate test data based on encoding - if encoding in ['bgr8', 'rgb8']: + if encoding in ["bgr8", "rgb8"]: channels = 3 - test_data = np.random.randint(0, 255, (height, width, channels), dtype=np.uint8) - elif encoding == 'mono8': + test_data = np.random.randint( + 0, 255, (height, width, channels), dtype=np.uint8 + ) + elif encoding == "mono8": channels = 1 test_data = np.random.randint(0, 255, (height, width), dtype=np.uint8) else: @@ -69,15 +71,15 @@ def _create_image_msg( return msg - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_bgr8_encoding(self, mock_wrapper): """Test processing BGR8 encoded images.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node mock_model = MagicMock() mock_result = { - 'depth': np.random.rand(480, 640).astype(np.float32), - 'confidence': np.random.rand(480, 640).astype(np.float32) + "depth": np.random.rand(480, 640).astype(np.float32), + "confidence": np.random.rand(480, 640).astype(np.float32), } mock_model.inference.return_value = mock_result mock_wrapper.return_value = mock_model @@ -85,7 +87,7 @@ def test_bgr8_encoding(self, mock_wrapper): node = DepthAnything3Node() # Create BGR8 test message - msg = self._create_image_msg(480, 640, 'bgr8') + msg = self._create_image_msg(480, 640, "bgr8") msg.header.stamp = node.get_clock().now().to_msg() # Process message @@ -96,15 +98,15 @@ def test_bgr8_encoding(self, mock_wrapper): node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_rgb8_encoding(self, mock_wrapper): """Test processing RGB8 encoded images.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node mock_model = MagicMock() mock_result = { - 'depth': np.random.rand(480, 640).astype(np.float32), - 'confidence': np.random.rand(480, 640).astype(np.float32) + "depth": np.random.rand(480, 640).astype(np.float32), + "confidence": np.random.rand(480, 640).astype(np.float32), } mock_model.inference.return_value = mock_result mock_wrapper.return_value = mock_model @@ -112,7 +114,7 @@ def test_rgb8_encoding(self, mock_wrapper): node = DepthAnything3Node() # Create RGB8 test message - msg = self._create_image_msg(480, 640, 'rgb8') + msg = self._create_image_msg(480, 640, "rgb8") msg.header.stamp = node.get_clock().now().to_msg() # Process message @@ -123,7 +125,7 @@ def test_rgb8_encoding(self, mock_wrapper): node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_different_image_sizes(self, mock_wrapper): """Test processing images of different sizes.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node @@ -135,19 +137,19 @@ def test_different_image_sizes(self, mock_wrapper): # Test various common camera resolutions resolutions = [ - (480, 640), # VGA + (480, 640), # VGA (720, 1280), # HD - (1080, 1920), # Full HD + (1080, 1920), # Full HD ] for height, width in resolutions: mock_result = { - 'depth': np.random.rand(height, width).astype(np.float32), - 'confidence': np.random.rand(height, width).astype(np.float32) + "depth": np.random.rand(height, width).astype(np.float32), + "confidence": np.random.rand(height, width).astype(np.float32), } mock_model.inference.return_value = mock_result - msg = self._create_image_msg(height, width, 'rgb8') + msg = self._create_image_msg(height, width, "rgb8") msg.header.stamp = node.get_clock().now().to_msg() # Should handle different sizes without errors @@ -155,15 +157,15 @@ def test_different_image_sizes(self, mock_wrapper): node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_different_frame_ids(self, mock_wrapper): """Test that node handles different frame IDs correctly.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node mock_model = MagicMock() mock_result = { - 'depth': np.random.rand(480, 640).astype(np.float32), - 'confidence': np.random.rand(480, 640).astype(np.float32) + "depth": np.random.rand(480, 640).astype(np.float32), + "confidence": np.random.rand(480, 640).astype(np.float32), } mock_model.inference.return_value = mock_result mock_wrapper.return_value = mock_model @@ -172,14 +174,14 @@ def test_different_frame_ids(self, mock_wrapper): # Test various frame IDs from different cameras frame_ids = [ - 'camera_optical_frame', - 'zed_left_camera_optical_frame', - 'camera_color_optical_frame', - 'usb_cam_optical_frame', + "camera_optical_frame", + "zed_left_camera_optical_frame", + "camera_color_optical_frame", + "usb_cam_optical_frame", ] for frame_id in frame_ids: - msg = self._create_image_msg(480, 640, 'rgb8', frame_id) + msg = self._create_image_msg(480, 640, "rgb8", frame_id) msg.header.stamp = node.get_clock().now().to_msg() # Should handle different frame IDs without errors @@ -188,5 +190,5 @@ def test_different_frame_ids(self, mock_wrapper): node.destroy_node() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_inference.py b/test/test_inference.py index a525eaf..95e59c8 100644 --- a/test/test_inference.py +++ b/test/test_inference.py @@ -7,7 +7,7 @@ import unittest import numpy as np -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import patch, MagicMock class TestDA3InferenceWrapper(unittest.TestCase): @@ -15,29 +15,27 @@ class TestDA3InferenceWrapper(unittest.TestCase): def setUp(self): """Set up test fixtures.""" - self.test_image = np.random.randint( - 0, 255, (480, 640, 3), dtype=np.uint8 - ) + self.test_image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) - @patch('depth_anything_3_ros2.da3_inference.torch') - @patch('depth_anything_3_ros2.da3_inference.DepthAnything3') + @patch("depth_anything_3_ros2.da3_inference.torch") + @patch("depth_anything_3.api.DepthAnything3") def test_device_selection_cuda_available(self, mock_da3, mock_torch): """Test device selection when CUDA is available.""" mock_torch.cuda.is_available.return_value = True - mock_torch.cuda.get_device_name.return_value = 'NVIDIA GPU' + mock_torch.cuda.get_device_name.return_value = "NVIDIA GPU" from depth_anything_3_ros2.da3_inference import DA3InferenceWrapper mock_model = MagicMock() mock_da3.from_pretrained.return_value = mock_model - wrapper = DA3InferenceWrapper(model_name='test-model', device='cuda') + wrapper = DA3InferenceWrapper(model_name="test-model", device="cuda") - self.assertEqual(wrapper.device, 'cuda') + self.assertEqual(wrapper.device, "cuda") mock_torch.cuda.is_available.assert_called_once() - @patch('depth_anything_3_ros2.da3_inference.torch') - @patch('depth_anything_3_ros2.da3_inference.DepthAnything3') + @patch("depth_anything_3_ros2.da3_inference.torch") + @patch("depth_anything_3.api.DepthAnything3") def test_device_selection_cuda_unavailable(self, mock_da3, mock_torch): """Test device fallback to CPU when CUDA is unavailable.""" mock_torch.cuda.is_available.return_value = False @@ -47,12 +45,12 @@ def test_device_selection_cuda_unavailable(self, mock_da3, mock_torch): mock_model = MagicMock() mock_da3.from_pretrained.return_value = mock_model - wrapper = DA3InferenceWrapper(model_name='test-model', device='cuda') + wrapper = DA3InferenceWrapper(model_name="test-model", device="cuda") - self.assertEqual(wrapper.device, 'cpu') + self.assertEqual(wrapper.device, "cpu") - @patch('depth_anything_3_ros2.da3_inference.torch') - @patch('depth_anything_3_ros2.da3_inference.DepthAnything3') + @patch("depth_anything_3_ros2.da3_inference.torch") + @patch("depth_anything_3.api.DepthAnything3") def test_inference_with_valid_image(self, mock_da3, mock_torch): """Test inference with valid input image.""" from depth_anything_3_ros2.da3_inference import DA3InferenceWrapper @@ -68,51 +66,57 @@ def test_inference_with_valid_image(self, mock_da3, mock_torch): mock_prediction.conf = [np.random.rand(480, 640).astype(np.float32)] mock_model.inference.return_value = mock_prediction - wrapper = DA3InferenceWrapper(model_name='test-model', device='cpu') + wrapper = DA3InferenceWrapper(model_name="test-model", device="cpu") # Test inference result = wrapper.inference(self.test_image, return_confidence=True) - self.assertIn('depth', result) - self.assertIn('confidence', result) - self.assertEqual(result['depth'].shape, (480, 640)) - self.assertEqual(result['confidence'].shape, (480, 640)) + self.assertIn("depth", result) + self.assertIn("confidence", result) + self.assertEqual(result["depth"].shape, (480, 640)) + self.assertEqual(result["confidence"].shape, (480, 640)) def test_inference_invalid_input_type(self): """Test inference with invalid input type.""" - with patch('depth_anything_3_ros2.da3_inference.torch'): - with patch('depth_anything_3_ros2.da3_inference.DepthAnything3'): + with patch("depth_anything_3_ros2.da3_inference.torch") as mock_torch: + with patch("depth_anything_3.api.DepthAnything3") as mock_da3: from depth_anything_3_ros2.da3_inference import DA3InferenceWrapper - # This would normally fail during model loading, but we'll test - # the validation logic separately if we can instantiate - pass + mock_torch.cuda.is_available.return_value = False + mock_model = MagicMock() + mock_da3.from_pretrained.return_value = mock_model - @patch('depth_anything_3_ros2.da3_inference.torch') - @patch('depth_anything_3_ros2.da3_inference.DepthAnything3') + wrapper = DA3InferenceWrapper(model_name="test-model") + + # Test that invalid input types raise ValueError + with self.assertRaises(ValueError): + wrapper.inference("not an array") + + @patch("depth_anything_3_ros2.da3_inference.torch") + @patch("depth_anything_3.api.DepthAnything3") def test_gpu_memory_usage_cuda(self, mock_da3, mock_torch): """Test GPU memory usage reporting with CUDA.""" from depth_anything_3_ros2.da3_inference import DA3InferenceWrapper mock_torch.cuda.is_available.return_value = True - mock_torch.cuda.get_device_name.return_value = 'NVIDIA GPU' + mock_torch.cuda.get_device_name.return_value = "NVIDIA GPU" mock_torch.cuda.memory_allocated.return_value = 1024 * 1024 * 100 mock_torch.cuda.memory_reserved.return_value = 1024 * 1024 * 150 mock_model = MagicMock() mock_da3.from_pretrained.return_value = mock_model - wrapper = DA3InferenceWrapper(model_name='test-model', device='cuda') + wrapper = DA3InferenceWrapper(model_name="test-model", device="cuda") mem_usage = wrapper.get_gpu_memory_usage() self.assertIsNotNone(mem_usage) - self.assertIn('allocated_mb', mem_usage) - self.assertIn('reserved_mb', mem_usage) - self.assertAlmostEqual(mem_usage['allocated_mb'], 100.0, places=1) - self.assertAlmostEqual(mem_usage['reserved_mb'], 150.0, places=1) + self.assertIn("allocated_mb", mem_usage) + self.assertIn("reserved_mb", mem_usage) + self.assertAlmostEqual(mem_usage["allocated_mb"], 100.0, places=1) + self.assertAlmostEqual(mem_usage["reserved_mb"], 150.0, places=1) - @patch('depth_anything_3_ros2.da3_inference.torch') - @patch('depth_anything_3_ros2.da3_inference.DepthAnything3') + @patch("depth_anything_3_ros2.da3_inference.torch") + @patch("depth_anything_3.api.DepthAnything3") def test_gpu_memory_usage_cpu(self, mock_da3, mock_torch): """Test GPU memory usage reporting with CPU.""" from depth_anything_3_ros2.da3_inference import DA3InferenceWrapper @@ -122,11 +126,11 @@ def test_gpu_memory_usage_cpu(self, mock_da3, mock_torch): mock_model = MagicMock() mock_da3.from_pretrained.return_value = mock_model - wrapper = DA3InferenceWrapper(model_name='test-model', device='cpu') + wrapper = DA3InferenceWrapper(model_name="test-model", device="cpu") mem_usage = wrapper.get_gpu_memory_usage() self.assertIsNone(mem_usage) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_node.py b/test/test_node.py index c3cacdf..74483ee 100644 --- a/test/test_node.py +++ b/test/test_node.py @@ -5,7 +5,7 @@ """ import unittest -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import patch, MagicMock import numpy as np import rclpy @@ -28,11 +28,9 @@ def tearDownClass(cls): def setUp(self): """Set up test fixtures.""" - self.test_image = np.random.randint( - 0, 255, (480, 640, 3), dtype=np.uint8 - ) + self.test_image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_node_initialization(self, mock_wrapper): """Test node initializes with default parameters.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node @@ -44,16 +42,16 @@ def test_node_initialization(self, mock_wrapper): node = DepthAnything3Node() # Check node name - self.assertEqual(node.get_name(), 'depth_anything_3') + self.assertEqual(node.get_name(), "depth_anything_3") # Check parameters exist - self.assertTrue(node.has_parameter('model_name')) - self.assertTrue(node.has_parameter('device')) - self.assertTrue(node.has_parameter('normalize_depth')) + self.assertTrue(node.has_parameter("model_name")) + self.assertTrue(node.has_parameter("device")) + self.assertTrue(node.has_parameter("normalize_depth")) node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_parameter_loading(self, mock_wrapper): """Test parameter loading from ROS2 parameter server.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node @@ -65,15 +63,14 @@ def test_parameter_loading(self, mock_wrapper): # Check default parameter values self.assertEqual( - node.get_parameter('model_name').value, - 'depth-anything/DA3-BASE' + node.get_parameter("model_name").value, "depth-anything/DA3-BASE" ) - self.assertEqual(node.get_parameter('device').value, 'cuda') - self.assertTrue(node.get_parameter('normalize_depth').value) + self.assertEqual(node.get_parameter("device").value, "cuda") + self.assertTrue(node.get_parameter("normalize_depth").value) node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_publishers_created(self, mock_wrapper): """Test that all publishers are created correctly.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node @@ -91,7 +88,7 @@ def test_publishers_created(self, mock_wrapper): node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_subscribers_created(self, mock_wrapper): """Test that all subscribers are created correctly.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node @@ -107,7 +104,7 @@ def test_subscribers_created(self, mock_wrapper): node.destroy_node() - @patch('depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper') + @patch("depth_anything_3_ros2.depth_anything_3_node.DA3InferenceWrapper") def test_image_callback_processing(self, mock_wrapper): """Test image callback processes messages correctly.""" from depth_anything_3_ros2.depth_anything_3_node import DepthAnything3Node @@ -115,8 +112,8 @@ def test_image_callback_processing(self, mock_wrapper): # Mock the model inference mock_model = MagicMock() mock_result = { - 'depth': np.random.rand(480, 640).astype(np.float32), - 'confidence': np.random.rand(480, 640).astype(np.float32) + "depth": np.random.rand(480, 640).astype(np.float32), + "confidence": np.random.rand(480, 640).astype(np.float32), } mock_model.inference.return_value = mock_result mock_wrapper.return_value = mock_model @@ -127,10 +124,10 @@ def test_image_callback_processing(self, mock_wrapper): msg = Image() msg.header = Header() msg.header.stamp = node.get_clock().now().to_msg() - msg.header.frame_id = 'camera_optical_frame' + msg.header.frame_id = "camera_optical_frame" msg.height = 480 msg.width = 640 - msg.encoding = 'rgb8' + msg.encoding = "rgb8" msg.is_bigendian = 0 msg.step = 640 * 3 msg.data = self.test_image.tobytes() @@ -144,5 +141,5 @@ def test_image_callback_processing(self, mock_wrapper): node.destroy_node() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()