From 91049ff153e22ef63c102908094cfed68f99e348 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 15:40:34 +0000 Subject: [PATCH] Add depth-based FOV and tilt estimation node Implemented new DepthFOVEstimatorNode that uses depth maps for more robust geometric analysis. This is the recommended method for accurate FOV and tilt estimation. New Features: - Depth map input support (compatible with MiDaS, Depth Anything, etc.) - Depth discontinuity detection using Sobel gradients - Bilateral filtering for edge-preserving smoothing - Depth-based vanishing point detection - Depth gradient analysis for fallback tilt estimation - Handles various depth map formats (1/3 channel, normalized/unnormalized) Architecture: - Modular design following ComfyUI patterns - Accepts depth map + original image inputs - Same output interface as RGB node for easy switching - Separate node registration: "FOV & Tilt Estimator (Depth)" Advantages over RGB method: - Depth discontinuities represent real 3D geometry - Independent of texture, lighting, and shadows - More robust to image artifacts and reflections - Better fallback behavior for scenes without clear lines - Cleaner edge detection from structural boundaries Both nodes now available: 1. FOV & Tilt Estimator (RGB) - Direct RGB analysis 2. FOV & Tilt Estimator (Depth) - Depth-based (recommended) Updated README with: - Complete depth-based workflow documentation - Comparison between RGB and depth methods - Tips for both estimation approaches - Recommended depth estimation nodes - Detailed technical implementation notes --- README.md | 192 +++++++++++++----- fov_estimator_node.py | 459 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 602 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 60aa3cc..5988e63 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # ComfyUI FOV Estimator -A ComfyUI custom node that estimates the **Field of View (FOV)** and **tilt angle (horizon angle)** of images using computer vision techniques. +A ComfyUI custom node collection that estimates the **Field of View (FOV)** and **tilt angle (horizon angle)** of images using computer vision techniques. ## Features +- **Two Analysis Methods**: + - **RGB-based**: Analyzes images directly using edge detection and line detection + - **Depth-based**: Uses depth maps for more robust geometric analysis (recommended) - **FOV Estimation**: Estimates the camera's field of view in degrees by detecting vanishing points - **Tilt Detection**: Detects the horizon angle and tilt of the camera in degrees - **Visual Debugging**: Optionally overlays detected lines, vanishing points, and measurements on the output image -- **Configurable Parameters**: Adjust edge detection and line detection thresholds for different image types +- **Configurable Parameters**: Adjust thresholds for different image types and depth estimation models ## Installation @@ -31,89 +34,184 @@ A ComfyUI custom node that estimates the **Field of View (FOV)** and **tilt angl ## Usage -The node will appear in ComfyUI under the **image/analysis** category as **"FOV & Tilt Estimator"**. +Two nodes are available under the **image/analysis** category: -### Inputs +### 1. FOV & Tilt Estimator (Depth) - **Recommended** +Uses depth maps for more accurate and robust estimation. + +**Workflow:** +``` +Load Image → Depth Estimation (MiDaS/Depth Anything) → FOV & Tilt Estimator (Depth) + ↓ + Original Image (for visualization) +``` + +**Inputs:** +- **depth_map** (required): Depth map from a depth estimation node (MiDaS, Depth Anything, etc.) +- **image** (required): Original RGB image for visualization overlay +- **depth_edge_threshold** (optional, default: 0.1): Threshold for depth discontinuities (0.01-1.0) +- **line_threshold** (optional, default: 50): Threshold for Hough line detection (10-300) +- **visualize** (optional, default: True): Whether to draw detected features + +**Outputs:** +- **annotated_image**: RGB image with visualization overlays +- **fov_degrees**: Estimated horizontal field of view (float) +- **tilt_degrees**: Estimated tilt angle (float) +- **info**: Formatted text with results + +**Example Workflow:** +1. Load your image +2. Pass it through a depth estimation node (MiDaS, Depth Anything, etc.) +3. Connect both the depth map and original image to the Depth FOV Estimator +4. The node will analyze depth discontinuities (3D edges) for more accurate results + +### 2. FOV & Tilt Estimator (RGB) + +Analyzes RGB images directly without requiring depth estimation. + +**Inputs:** - **image** (required): The input image to analyze - **edge_threshold_low** (optional, default: 50): Lower threshold for Canny edge detection (0-255) - **edge_threshold_high** (optional, default: 150): Upper threshold for Canny edge detection (0-255) - **line_threshold** (optional, default: 100): Threshold for Hough line detection (10-500) -- **visualize** (optional, default: True): Whether to draw detected features on the output image +- **visualize** (optional, default: True): Whether to draw detected features -### Outputs - -- **annotated_image**: The input image with visualization overlays (if enabled) +**Outputs:** +- **annotated_image**: The input image with visualization overlays - **fov_degrees**: Estimated field of view in degrees (float) - **tilt_degrees**: Estimated tilt/horizon angle in degrees (float) - **info**: Text string with formatted results -### Example Workflow - +**Example Workflow:** 1. Load an image -2. Connect it to the FOV & Tilt Estimator node -3. View the results: - - The annotated image shows detected horizon lines (blue) and vanishing points (green) - - FOV and tilt values are displayed as text overlay and available as outputs +2. Connect it directly to the FOV & Tilt Estimator (RGB) node +3. View the results with detected features ## How It Works -### FOV Estimation +### Depth-Based Method (Recommended) + +**FOV Estimation from Depth:** +1. Detects **depth discontinuities** (3D edges) using Sobel gradients on the depth map +2. Applies bilateral filtering to preserve edges while smoothing +3. Detects lines from depth discontinuities using Hough transform +4. Filters for non-horizontal lines (converging building edges, roads, etc.) +5. Finds dominant vanishing point using RANSAC-like clustering +6. Calculates focal length from vanishing point position +7. Converts to horizontal FOV: `hfov = 2 × arctan(width / (2 × focal_length))` + +**Tilt Estimation from Depth:** +1. Detects depth discontinuities (horizon appears as depth transition) +2. Finds horizontal lines in depth edges +3. Calculates horizon position relative to frame center +4. Converts to tilt angle using vertical FOV +5. Falls back to depth gradient analysis if no clear horizon found + +**Advantages:** +- Depth discontinuities represent **real 3D geometry**, independent of texture/lighting +- More robust to shadows, reflections, and complex textures +- Cleaner edge detection from structural boundaries +- Can analyze depth gradients when lines aren't clear + +### RGB-Based Method + +**FOV Estimation:** Uses the pinhole camera model and vanishing point analysis: -1. Detects edge lines in the image using Canny edge detection and Hough transform -2. Filters for non-horizontal lines (converging lines from buildings, roads, etc.) -3. Finds intersections between line pairs to identify potential vanishing points -4. Uses RANSAC-like clustering to find the dominant vanishing point (most supported by line intersections) -5. Calculates focal length from the distance between the image center and vanishing point -6. Converts focal length to horizontal FOV using: `hfov = 2 × arctan(width / (2 × focal_length))` -7. Falls back to aspect ratio-based estimation if no good vanishing point is found +1. Applies Canny edge detection to find edges in RGB image +2. Uses Hough transform to detect lines +3. Filters for non-horizontal converging lines +4. Finds dominant vanishing point through intersection clustering +5. Calculates focal length from vanishing point distance to image center +6. Converts to FOV: `hfov = 2 × arctan(width / (2 × focal_length))` -**Key Principle:** The distance from the image center (principal point) to a vanishing point created by receding parallel lines approximates the camera's focal length in pixels. This relationship allows accurate FOV estimation from image geometry. +**Tilt Estimation:** +1. Detects edges using Canny +2. Finds horizontal lines (potential horizons) +3. Calculates horizon position relative to frame center +4. Converts to tilt angle using vertical FOV +5. Tilt = 0° when horizon is centered -### Tilt Detection -1. Converts image to grayscale -2. Applies Gaussian blur to reduce noise -3. Uses Canny edge detection to find edges -4. Applies Hough line transform to detect horizon lines -5. Filters for near-horizontal lines -6. Calculates where the horizon intersects the center of the frame -7. Converts horizontal FOV to vertical FOV using the image aspect ratio -8. Measures tilt based on horizon position relative to frame center using the estimated vertical FOV: - - **Tilt = 0°** when horizon is at the center of the frame - - **Tilt < 0°** when horizon is above center (camera looking up) - - **Tilt > 0°** when horizon is below center (camera looking down) +**Key Principle:** The distance from the image center to a vanishing point approximates the camera's focal length in pixels, enabling FOV calculation from pure geometry. ## Tips +### For Depth-Based Estimation (Recommended) +- **Use high-quality depth maps**: MiDaS v3.1 or Depth Anything v2 work best +- **Adjust depth_edge_threshold**: + - Lower (0.05-0.08) for subtle depth changes + - Higher (0.15-0.25) for noisy depth maps +- **For outdoor scenes**: Works well even without strong architectural features +- **For indoor scenes**: Excellent with depth - detects walls, furniture edges clearly + +### For RGB-Based Estimation - **For architecture/buildings**: Default settings work well - **For outdoor landscapes**: Reduce line_threshold to 50-70 - **For noisy images**: Increase edge_threshold_low to 70-100 - **For subtle edges**: Decrease edge_threshold_high to 100-120 +### General Recommendations +- **Use depth-based method whenever possible** - it's significantly more robust +- Images with clear geometric features (buildings, roads) work best for both methods +- For organic scenes (forests, clouds), depth-based method has better fallback behavior + ## Limitations -- **FOV estimation** works best on images with clear converging lines (buildings, roads, railroad tracks) -- **Tilt estimation** works best on images with visible horizons -- May be less accurate on images with: - - Extreme wide-angle or fisheye distortion (non-linear distortion) +### Depth-Based Method +- Requires depth estimation preprocessing (adds computational cost) +- Accuracy depends on depth map quality +- Very noisy depth maps (from poor lighting) may produce unreliable results +- Still assumes pinhole camera model (no fisheye correction) + +### RGB-Based Method +- **FOV estimation** requires clear converging lines (buildings, roads, railroad tracks) +- **Tilt estimation** requires visible horizons +- Sensitive to texture, lighting, and shadows +- May struggle with: - Very cluttered scenes with few clear lines - - No clear horizon or parallel lines - - Mostly organic/natural scenes without geometric features + - Organic/natural scenes without geometric features + - Low contrast images + +### Both Methods +- Assume linear perspective (pinhole camera model) +- Not designed for extreme wide-angle or fisheye distortion +- Accuracy degrades with severe lens distortion ## Technical Details - **Computer Vision**: Uses OpenCV for all image processing operations -- **Edge Detection**: Canny edge detection with configurable thresholds -- **Line Detection**: Hough line transform for finding straight lines +- **Depth Processing** (Depth-based node): + - Sobel gradient computation for depth discontinuities + - Bilateral filtering for edge-preserving smoothing + - Depth gradient analysis for fallback tilt estimation + - Handles various depth map formats (1-channel, 3-channel, normalized/unnormalized) +- **Edge Detection**: + - RGB: Canny edge detection with configurable thresholds + - Depth: Gradient-based depth discontinuity detection +- **Line Detection**: Hough line transform (both standard and probabilistic) - **FOV Estimation**: - RANSAC-like clustering for robust vanishing point detection - - Pinhole camera model for focal length estimation - - Handles images without clear vanishing points via aspect ratio fallback + - Pinhole camera model: `focal_length ≈ distance(principal_point, vanishing_point)` + - FOV calculation: `hfov = 2 × arctan(width / (2 × f))` + - Intelligent fallbacks based on aspect ratio - **Tilt Estimation**: - - Horizon line detection and position analysis - - Converts horizontal to vertical FOV using aspect ratio + - Horizon line detection from horizontal edges + - Position analysis relative to frame center + - Vertical FOV conversion: `vfov = 2 × arctan(tan(hfov/2) / aspect_ratio)` - Median-based robust estimation resistant to outliers + - Depth gradient analysis for scenes without clear horizons + +## Recommended Depth Estimation Nodes + +For best results with the depth-based estimator, use these ComfyUI depth estimation nodes: + +- **MiDaS** (v3.0, v3.1) - Good general-purpose depth estimation +- **Depth Anything** (v1, v2) - State-of-the-art depth estimation, recommended +- **ZoeDepth** - Metric depth estimation +- **LeReS** - High-quality depth for indoor scenes + +Install these through ComfyUI Manager or manually from their respective repositories. ## License diff --git a/fov_estimator_node.py b/fov_estimator_node.py index ee839c1..a785c12 100644 --- a/fov_estimator_node.py +++ b/fov_estimator_node.py @@ -465,11 +465,466 @@ class FOVEstimatorNode: return vis_img +class DepthFOVEstimatorNode: + """ + A ComfyUI node that estimates FOV and tilt from depth maps. + + Uses depth information for more robust geometric analysis: + - Depth discontinuities for edge detection + - 3D structure analysis for vanishing points + - Depth-aware horizon detection + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + return { + "required": { + "depth_map": ("IMAGE",), # Accepts depth map from depth estimation nodes + "image": ("IMAGE",), # Original image for visualization + }, + "optional": { + "depth_edge_threshold": ("FLOAT", { + "default": 0.1, + "min": 0.01, + "max": 1.0, + "step": 0.01, + "display": "number" + }), + "line_threshold": ("INT", { + "default": 50, + "min": 10, + "max": 300, + "step": 5, + "display": "number" + }), + "visualize": ("BOOLEAN", { + "default": True, + "display": "checkbox" + }), + } + } + + RETURN_TYPES = ("IMAGE", "FLOAT", "FLOAT", "STRING") + RETURN_NAMES = ("annotated_image", "fov_degrees", "tilt_degrees", "info") + FUNCTION = "estimate_fov_tilt_from_depth" + CATEGORY = "image/analysis" + + def estimate_fov_tilt_from_depth( + self, + depth_map: torch.Tensor, + image: torch.Tensor, + depth_edge_threshold: float = 0.1, + line_threshold: int = 50, + visualize: bool = True + ) -> Tuple[torch.Tensor, float, float, str]: + """ + Estimate FOV and tilt from depth map. + + Args: + depth_map: Depth map tensor [B, H, W, C] + image: Original RGB image for visualization [B, H, W, C] + depth_edge_threshold: Threshold for depth discontinuities + line_threshold: Threshold for Hough line detection + visualize: Whether to draw detected features + + Returns: + Tuple of (annotated_image, fov_degrees, tilt_degrees, info_string) + """ + batch_size = depth_map.shape[0] + results_images = [] + results_fov = [] + results_tilt = [] + results_info = [] + + for i in range(batch_size): + # Process depth map + depth_np = depth_map[i].cpu().numpy() + img_np = image[i].cpu().numpy() + + # Convert image from [0, 1] float to [0, 255] uint8 + img_np = (img_np * 255).astype(np.uint8) + + # Handle depth map format (could be 1 or 3 channels) + if depth_np.shape[-1] == 3: + # If 3 channels, take first channel (should all be same for grayscale depth) + depth_np = depth_np[:, :, 0] + elif depth_np.shape[-1] == 1: + depth_np = depth_np[:, :, 0] + + # Normalize depth to [0, 1] if not already + if depth_np.max() > 1.0: + depth_np = depth_np / 255.0 + + # Estimate FOV from depth map + fov_angle, vanishing_points = self._estimate_fov_from_depth( + depth_np, depth_edge_threshold, line_threshold + ) + + # Estimate tilt from depth map + tilt_angle, horizon_lines = self._estimate_tilt_from_depth( + depth_np, depth_edge_threshold, fov_angle + ) + + # Create visualization if requested + if visualize: + vis_img = self._visualize_depth_results( + img_np.copy(), depth_np, horizon_lines, vanishing_points, tilt_angle, fov_angle + ) + else: + vis_img = img_np + + # Convert back to ComfyUI tensor format + vis_tensor = torch.from_numpy(vis_img.astype(np.float32) / 255.0) + results_images.append(vis_tensor) + results_fov.append(fov_angle) + results_tilt.append(tilt_angle) + + # Create info string + info = f"FOV: {fov_angle:.2f}°, Tilt: {tilt_angle:.2f}° (depth-based)" + results_info.append(info) + + # Stack batch results + output_image = torch.stack(results_images, dim=0) + avg_fov = float(np.mean(results_fov)) + avg_tilt = float(np.mean(results_tilt)) + combined_info = "\n".join(results_info) + + return (output_image, avg_fov, avg_tilt, combined_info) + + def _estimate_fov_from_depth( + self, + depth: np.ndarray, + edge_threshold: float, + line_threshold: int + ) -> Tuple[float, list]: + """ + Estimate FOV using depth discontinuities and vanishing points. + + Depth discontinuities represent real 3D edges in the scene, + providing cleaner geometric information than RGB edges. + """ + height, width = depth.shape + + # Find depth discontinuities (edges in 3D structure) + depth_edges = self._detect_depth_discontinuities(depth, edge_threshold) + + # Detect lines from depth edges + lines = cv2.HoughLinesP( + depth_edges, + rho=1, + theta=np.pi / 180, + threshold=line_threshold, + minLineLength=min(width, height) // 10, + maxLineGap=min(width, height) // 20 + ) + + vanishing_points = [] + fov = None + + if lines is not None and len(lines) > 3: + # Find dominant vanishing point from depth-based lines + vanishing_point, supporting_lines = self._find_dominant_vanishing_point_from_lines( + lines, (height, width) + ) + + if vanishing_point is not None: + vanishing_points.append(vanishing_point) + + # Calculate FOV from vanishing point + principal_point = np.array([width / 2.0, height / 2.0]) + vp_array = np.array(vanishing_point) + + # Use horizontal distance for horizontal FOV + focal_length_estimate = abs(vp_array[0] - principal_point[0]) + + # If VP is near center, use Euclidean distance + if focal_length_estimate < width * 0.1: + focal_length_estimate = np.linalg.norm(vp_array - principal_point) + + # Ensure reasonable focal length + focal_length_estimate = max(focal_length_estimate, width * 0.3) + + # Calculate horizontal FOV + fov = 2 * np.degrees(np.arctan(width / (2 * focal_length_estimate))) + fov = np.clip(fov, 15, 150) + + # Fallback if no vanishing point found + if fov is None: + aspect_ratio = width / height + if aspect_ratio > 2.0: + fov = 100.0 + elif aspect_ratio > 1.7: + fov = 70.0 + elif aspect_ratio > 1.2: + fov = 55.0 + else: + fov = 50.0 + + return float(fov), vanishing_points + + def _estimate_tilt_from_depth( + self, + depth: np.ndarray, + edge_threshold: float, + fov_horizontal: float + ) -> Tuple[float, list]: + """ + Estimate tilt using depth-based horizon detection. + + The horizon in depth maps appears as a transition from near to far depths, + often more clearly visible than in RGB images. + """ + height, width = depth.shape + frame_center_y = height / 2.0 + + # Calculate vertical FOV + aspect_ratio = width / height + fov_vertical = 2 * np.degrees(np.arctan(np.tan(np.radians(fov_horizontal / 2)) / aspect_ratio)) + + # Find depth discontinuities + depth_edges = self._detect_depth_discontinuities(depth, edge_threshold) + + # Detect horizontal lines (potential horizons) + lines = cv2.HoughLines(depth_edges, 1, np.pi / 180, int(width * 0.3)) + + horizon_lines = [] + horizon_y_positions = [] + + if lines is not None: + for line in lines: + rho, theta = line[0] + angle_deg = np.degrees(theta) + + # Filter for near-horizontal lines + if 75 <= angle_deg <= 105: + cos_theta = np.cos(theta) + sin_theta = np.sin(theta) + + if abs(sin_theta) > 0.1: + x_center = width / 2.0 + y_at_center = (rho - x_center * cos_theta) / sin_theta + + if -height * 0.5 <= y_at_center <= height * 1.5: + horizon_lines.append((rho, theta)) + horizon_y_positions.append(y_at_center) + + # Calculate tilt from horizon position + if horizon_y_positions: + median_horizon_y = float(np.median(horizon_y_positions)) + y_offset = median_horizon_y - frame_center_y + degrees_per_pixel = fov_vertical / height + tilt_angle = y_offset * degrees_per_pixel + else: + # Alternative: analyze depth gradient + # Lower half of image should generally have nearer depths (ground) + # Upper half should have farther depths (sky/background) + tilt_angle = self._estimate_tilt_from_depth_gradient(depth, fov_vertical) + + return tilt_angle, horizon_lines + + def _detect_depth_discontinuities( + self, + depth: np.ndarray, + threshold: float + ) -> np.ndarray: + """ + Detect depth discontinuities (3D edges) in the depth map. + + Depth discontinuities represent real boundaries between objects, + independent of texture or lighting. + """ + # Normalize depth to [0, 1] + depth_normalized = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8) + + # Convert to uint8 for edge detection + depth_uint8 = (depth_normalized * 255).astype(np.uint8) + + # Apply bilateral filter to preserve edges while smoothing + depth_filtered = cv2.bilateralFilter(depth_uint8, 5, 50, 50) + + # Calculate depth gradients + grad_x = cv2.Sobel(depth_filtered, cv2.CV_64F, 1, 0, ksize=3) + grad_y = cv2.Sobel(depth_filtered, cv2.CV_64F, 0, 1, ksize=3) + gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2) + + # Normalize gradient + gradient_magnitude = (gradient_magnitude / gradient_magnitude.max() * 255).astype(np.uint8) + + # Threshold to get binary edge map + threshold_uint8 = int(threshold * 255) + _, edges = cv2.threshold(gradient_magnitude, threshold_uint8, 255, cv2.THRESH_BINARY) + + # Morphological operations to clean up edges + kernel = np.ones((3, 3), np.uint8) + edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) + + return edges + + def _estimate_tilt_from_depth_gradient( + self, + depth: np.ndarray, + fov_vertical: float + ) -> float: + """ + Estimate tilt from overall depth gradient when no clear horizon is found. + """ + height, width = depth.shape + + # Divide image into top and bottom halves + top_half = depth[:height//2, :] + bottom_half = depth[height//2:, :] + + # Calculate median depths + top_median = np.median(top_half) + bottom_median = np.median(bottom_half) + + # If bottom is significantly nearer than top, camera is level or tilted down + # If top is nearer, camera is tilted up significantly + depth_ratio = (top_median - bottom_median) / (top_median + bottom_median + 1e-8) + + # Estimate tilt angle (heuristic) + # Positive depth_ratio means top is farther (normal/level view) + # Negative means top is nearer (looking up at something) + tilt_angle = -depth_ratio * 30.0 # Scale factor is heuristic + + return np.clip(tilt_angle, -45.0, 45.0) + + def _find_dominant_vanishing_point_from_lines( + self, + lines: np.ndarray, + img_shape: Tuple[int, int] + ) -> Tuple[Tuple[float, float], int]: + """ + Find dominant vanishing point from depth-based lines. + Similar to RGB version but optimized for depth discontinuities. + """ + height, width = img_shape + + # Filter for non-horizontal lines + line_list = [] + for line in lines: + x1, y1, x2, y2 = line[0] + dx = x2 - x1 + dy = y2 - y1 + angle = abs(np.degrees(np.arctan2(dy, dx))) + + # Skip near-horizontal lines + if not (10 <= angle <= 170): + continue + + line_list.append(((float(x1), float(y1)), (float(x2), float(y2)))) + + if len(line_list) < 3: + return None, 0 + + # Find intersections + intersections = [] + for i in range(len(line_list)): + for j in range(i + 1, min(i + 50, len(line_list))): + pt = self._line_intersection(line_list[i], line_list[j]) + if pt is not None: + if (-width <= pt[0] <= 2*width) and (-height <= pt[1] <= 2*height): + intersections.append(pt) + + if len(intersections) < 3: + return None, 0 + + intersections = np.array(intersections) + + # Find best vanishing point through clustering + best_vp = None + best_support = 0 + + sample_indices = np.linspace(0, len(intersections)-1, min(20, len(intersections)), dtype=int) + + for idx in sample_indices: + candidate = intersections[idx] + distances = np.linalg.norm(intersections - candidate, axis=1) + threshold = max(width, height) * 0.15 + support = np.sum(distances < threshold) + + if support > best_support: + best_support = support + supporting_points = intersections[distances < threshold] + best_vp = tuple(np.median(supporting_points, axis=0)) + + if best_support >= 3: + return best_vp, best_support + + return None, 0 + + def _line_intersection( + self, + line1: Tuple[Tuple[float, float], Tuple[float, float]], + line2: Tuple[Tuple[float, float], Tuple[float, float]] + ) -> Tuple[float, float]: + """Find intersection point of two lines.""" + (x1, y1), (x2, y2) = line1 + (x3, y3), (x4, y4) = line2 + + denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + + if abs(denom) < 1e-6: + return None + + px = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom + py = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom + + return (px, py) + + def _visualize_depth_results( + self, + img: np.ndarray, + depth: np.ndarray, + horizon_lines: list, + vanishing_points: list, + tilt_angle: float, + fov_angle: float + ) -> np.ndarray: + """ + Visualize depth-based analysis results on the original image. + """ + vis_img = img.copy() + height, width = img.shape[:2] + + # Draw horizon lines in blue + for rho, theta in horizon_lines[:5]: + a = np.cos(theta) + b = np.sin(theta) + x0 = a * rho + y0 = b * rho + x1 = int(x0 + 2000 * (-b)) + y1 = int(y0 + 2000 * (a)) + x2 = int(x0 - 2000 * (-b)) + y2 = int(y0 - 2000 * (a)) + cv2.line(vis_img, (x1, y1), (x2, y2), (0, 0, 255), 2) + + # Draw vanishing points in green + for vp in vanishing_points: + x, y = int(vp[0]), int(vp[1]) + if 0 <= x < width and 0 <= y < height: + cv2.circle(vis_img, (x, y), 10, (0, 255, 0), -1) + cv2.circle(vis_img, (x, y), 15, (0, 255, 0), 2) + + # Add text overlay + font = cv2.FONT_HERSHEY_SIMPLEX + text_fov = f"FOV: {fov_angle:.1f} deg (depth)" + text_tilt = f"Tilt: {tilt_angle:.1f} deg" + + cv2.rectangle(vis_img, (10, 10), (340, 80), (0, 0, 0), -1) + cv2.putText(vis_img, text_fov, (20, 40), font, 0.7, (255, 255, 255), 2) + cv2.putText(vis_img, text_tilt, (20, 70), font, 0.7, (255, 255, 255), 2) + + return vis_img + + # ComfyUI node registration NODE_CLASS_MAPPINGS = { - "FOVEstimator": FOVEstimatorNode + "FOVEstimator": FOVEstimatorNode, + "DepthFOVEstimator": DepthFOVEstimatorNode } NODE_DISPLAY_NAME_MAPPINGS = { - "FOVEstimator": "FOV & Tilt Estimator" + "FOVEstimator": "FOV & Tilt Estimator (RGB)", + "DepthFOVEstimator": "FOV & Tilt Estimator (Depth)" }