Spaces:
Sleeping
Sleeping
| """ | |
| Visualization utilities for detection validation. | |
| This module provides functions to visualize detection results, | |
| allowing visual inspection of walls, rooms, doors, and windows. | |
| """ | |
| from typing import Dict, List, Optional, Tuple, Union | |
| import numpy as np | |
| import cv2 | |
| def visualize_detections( | |
| image: np.ndarray, | |
| detections: Dict[str, List[np.ndarray]], | |
| output_path: Optional[str] = None, | |
| show_labels: bool = True, | |
| room_alpha: float = 0.3 | |
| ) -> np.ndarray: | |
| """ | |
| Visualize detection results with color-coded overlays. | |
| This function draws walls, rooms, doors, and windows on the input image | |
| with different colors and styles for easy visual inspection. | |
| Parameters | |
| ---------- | |
| image : np.ndarray | |
| Original or preprocessed image (grayscale or BGR) | |
| detections : Dict[str, List[np.ndarray]] | |
| Dictionary containing detection results with keys: | |
| - "walls": List of wall polygons (each Nx2 numpy array) | |
| - "rooms": List of room polygons (each Nx2 numpy array) | |
| - "doors": List of door polygons (each Nx2 numpy array) | |
| - "windows": List of window polygons (each Nx2 numpy array) | |
| output_path : str, optional | |
| If provided, save the annotated image to this path | |
| show_labels : bool, default=True | |
| Whether to add text labels for doors and windows | |
| room_alpha : float, default=0.3 | |
| Transparency for room fill (0.0 = transparent, 1.0 = opaque) | |
| Returns | |
| ------- | |
| np.ndarray | |
| Annotated image with detection overlays | |
| Examples | |
| -------- | |
| >>> detections = { | |
| ... "walls": [wall_polygon1, wall_polygon2], | |
| ... "rooms": [room_polygon1, room_polygon2], | |
| ... "doors": [door_polygon1], | |
| ... "windows": [window_polygon1, window_polygon2] | |
| ... } | |
| >>> annotated = visualize_detections(image, detections, "output.png") | |
| """ | |
| # Convert grayscale to BGR if needed | |
| if len(image.shape) == 2: | |
| viz_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) | |
| else: | |
| viz_image = image.copy() | |
| # Ensure image is uint8 | |
| if viz_image.dtype != np.uint8: | |
| viz_image = (viz_image * 255).astype(np.uint8) if viz_image.max() <= 1.0 else viz_image.astype(np.uint8) | |
| # Define colors (BGR format) | |
| colors = { | |
| "walls": (0, 255, 0), # Green | |
| "rooms": (144, 238, 144), # Light green | |
| "doors": (255, 0, 0), # Blue | |
| "windows": (0, 0, 255) # Red | |
| } | |
| # Define thicknesses | |
| thicknesses = { | |
| "walls": 2, | |
| "rooms": -1, # Filled | |
| "doors": 2, | |
| "windows": 2 | |
| } | |
| # Draw rooms first (with transparency) | |
| if "rooms" in detections and detections["rooms"]: | |
| rooms_overlay = viz_image.copy() | |
| for room_polygon in detections["rooms"]: | |
| if room_polygon is None or len(room_polygon) == 0: | |
| continue | |
| # Convert to integer coordinates | |
| points = _ensure_int_array(room_polygon) | |
| # Draw filled polygon | |
| cv2.fillPoly(rooms_overlay, [points], colors["rooms"]) | |
| # Blend with original image for transparency | |
| cv2.addWeighted(rooms_overlay, room_alpha, viz_image, 1 - room_alpha, 0, viz_image) | |
| # Draw walls | |
| if "walls" in detections and detections["walls"]: | |
| for wall_polygon in detections["walls"]: | |
| if wall_polygon is None or len(wall_polygon) == 0: | |
| continue | |
| points = _ensure_int_array(wall_polygon) | |
| cv2.polylines(viz_image, [points], isClosed=True, | |
| color=colors["walls"], thickness=thicknesses["walls"]) | |
| # Draw doors with labels | |
| if "doors" in detections and detections["doors"]: | |
| for i, door_polygon in enumerate(detections["doors"]): | |
| if door_polygon is None or len(door_polygon) == 0: | |
| continue | |
| points = _ensure_int_array(door_polygon) | |
| cv2.polylines(viz_image, [points], isClosed=True, | |
| color=colors["doors"], thickness=thicknesses["doors"]) | |
| # Add label | |
| if show_labels: | |
| centroid = _compute_centroid(points) | |
| _draw_label(viz_image, f"Door {i+1}", centroid, colors["doors"]) | |
| # Draw windows with labels | |
| if "windows" in detections and detections["windows"]: | |
| for i, window_polygon in enumerate(detections["windows"]): | |
| if window_polygon is None or len(window_polygon) == 0: | |
| continue | |
| points = _ensure_int_array(window_polygon) | |
| cv2.polylines(viz_image, [points], isClosed=True, | |
| color=colors["windows"], thickness=thicknesses["windows"]) | |
| # Add label | |
| if show_labels: | |
| centroid = _compute_centroid(points) | |
| _draw_label(viz_image, f"Win {i+1}", centroid, colors["windows"]) | |
| # Add legend | |
| viz_image = _add_legend(viz_image, colors) | |
| # Save if output path provided | |
| if output_path: | |
| cv2.imwrite(output_path, viz_image) | |
| print(f"Visualization saved to: {output_path}") | |
| return viz_image | |
| def visualize_comparison( | |
| image: np.ndarray, | |
| detections_before: Dict[str, List[np.ndarray]], | |
| detections_after: Dict[str, List[np.ndarray]], | |
| output_path: Optional[str] = None | |
| ) -> np.ndarray: | |
| """ | |
| Create side-by-side comparison of detections before and after refinement. | |
| Parameters | |
| ---------- | |
| image : np.ndarray | |
| Original image | |
| detections_before : Dict[str, List[np.ndarray]] | |
| Detections before refinement (YOLO) | |
| detections_after : Dict[str, List[np.ndarray]] | |
| Detections after refinement | |
| output_path : str, optional | |
| If provided, save the comparison image | |
| Returns | |
| ------- | |
| np.ndarray | |
| Side-by-side comparison image | |
| """ | |
| # Create visualizations | |
| viz_before = visualize_detections(image, detections_before, show_labels=False) | |
| viz_after = visualize_detections(image, detections_after, show_labels=True) | |
| # Add titles | |
| viz_before = _add_title(viz_before, "Before Refinement (YOLO)") | |
| viz_after = _add_title(viz_after, "After Refinement (Geometry)") | |
| # Concatenate horizontally | |
| comparison = np.hstack([viz_before, viz_after]) | |
| # Save if output path provided | |
| if output_path: | |
| cv2.imwrite(output_path, comparison) | |
| print(f"Comparison saved to: {output_path}") | |
| return comparison | |
| def visualize_vectorization_result( | |
| image: np.ndarray, | |
| vectorization_result, | |
| output_path: Optional[str] = None, | |
| show_labels: bool = True | |
| ) -> np.ndarray: | |
| """ | |
| Visualize a VectorizationResult object. | |
| Parameters | |
| ---------- | |
| image : np.ndarray | |
| Original image | |
| vectorization_result : VectorizationResult | |
| VectorizationResult object from wall_vectorizer | |
| output_path : str, optional | |
| If provided, save the visualization | |
| show_labels : bool, default=True | |
| Whether to show labels | |
| Returns | |
| ------- | |
| np.ndarray | |
| Annotated image | |
| """ | |
| # Convert VectorizationResult to detections dict | |
| detections = { | |
| "walls": [np.array(w.points) for w in vectorization_result.walls], | |
| "rooms": [np.array(r.points) for r in vectorization_result.rooms], | |
| "doors": [np.array(d.points) for d in vectorization_result.doors], | |
| "windows": [np.array(w.points) for w in vectorization_result.windows] | |
| } | |
| return visualize_detections(image, detections, output_path, show_labels) | |
| # ── Helper Functions ────────────────────────────────────────────────────────── | |
| def _ensure_int_array(polygon: Union[np.ndarray, List]) -> np.ndarray: | |
| """Convert polygon to integer numpy array.""" | |
| if isinstance(polygon, list): | |
| polygon = np.array(polygon) | |
| # Ensure 2D array | |
| if len(polygon.shape) == 1: | |
| polygon = polygon.reshape(-1, 2) | |
| return polygon.astype(np.int32) | |
| def _compute_centroid(points: np.ndarray) -> Tuple[int, int]: | |
| """Compute centroid of a polygon.""" | |
| centroid = np.mean(points, axis=0) | |
| return (int(centroid[0]), int(centroid[1])) | |
| def _draw_label( | |
| image: np.ndarray, | |
| text: str, | |
| position: Tuple[int, int], | |
| color: Tuple[int, int, int], | |
| font_scale: float = 0.4, | |
| thickness: int = 1 | |
| ): | |
| """Draw text label with background.""" | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| # Get text size | |
| (text_width, text_height), baseline = cv2.getTextSize( | |
| text, font, font_scale, thickness | |
| ) | |
| # Draw background rectangle | |
| x, y = position | |
| padding = 2 | |
| cv2.rectangle( | |
| image, | |
| (x - padding, y - text_height - padding), | |
| (x + text_width + padding, y + baseline + padding), | |
| (255, 255, 255), | |
| -1 | |
| ) | |
| # Draw text | |
| cv2.putText( | |
| image, | |
| text, | |
| (x, y), | |
| font, | |
| font_scale, | |
| color, | |
| thickness, | |
| cv2.LINE_AA | |
| ) | |
| def _add_legend( | |
| image: np.ndarray, | |
| colors: Dict[str, Tuple[int, int, int]], | |
| position: str = "top-right" | |
| ) -> np.ndarray: | |
| """Add color legend to image.""" | |
| legend_items = [ | |
| ("Walls", colors["walls"]), | |
| ("Rooms", colors["rooms"]), | |
| ("Doors", colors["doors"]), | |
| ("Windows", colors["windows"]) | |
| ] | |
| # Legend dimensions | |
| item_height = 25 | |
| item_width = 120 | |
| padding = 10 | |
| legend_height = len(legend_items) * item_height + 2 * padding | |
| legend_width = item_width + 2 * padding | |
| # Determine position | |
| h, w = image.shape[:2] | |
| if position == "top-right": | |
| x_start = w - legend_width - 10 | |
| y_start = 10 | |
| elif position == "top-left": | |
| x_start = 10 | |
| y_start = 10 | |
| elif position == "bottom-right": | |
| x_start = w - legend_width - 10 | |
| y_start = h - legend_height - 10 | |
| else: # bottom-left | |
| x_start = 10 | |
| y_start = h - legend_height - 10 | |
| # Draw legend background | |
| cv2.rectangle( | |
| image, | |
| (x_start, y_start), | |
| (x_start + legend_width, y_start + legend_height), | |
| (255, 255, 255), | |
| -1 | |
| ) | |
| cv2.rectangle( | |
| image, | |
| (x_start, y_start), | |
| (x_start + legend_width, y_start + legend_height), | |
| (0, 0, 0), | |
| 1 | |
| ) | |
| # Draw legend items | |
| for i, (label, color) in enumerate(legend_items): | |
| y = y_start + padding + i * item_height + item_height // 2 | |
| # Draw color box | |
| box_size = 15 | |
| cv2.rectangle( | |
| image, | |
| (x_start + padding, y - box_size // 2), | |
| (x_start + padding + box_size, y + box_size // 2), | |
| color, | |
| -1 | |
| ) | |
| cv2.rectangle( | |
| image, | |
| (x_start + padding, y - box_size // 2), | |
| (x_start + padding + box_size, y + box_size // 2), | |
| (0, 0, 0), | |
| 1 | |
| ) | |
| # Draw label text | |
| cv2.putText( | |
| image, | |
| label, | |
| (x_start + padding + box_size + 10, y + 5), | |
| cv2.FONT_HERSHEY_SIMPLEX, | |
| 0.4, | |
| (0, 0, 0), | |
| 1, | |
| cv2.LINE_AA | |
| ) | |
| return image | |
| def _add_title( | |
| image: np.ndarray, | |
| title: str, | |
| font_scale: float = 0.7, | |
| thickness: int = 2 | |
| ) -> np.ndarray: | |
| """Add title to top of image.""" | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| # Get text size | |
| (text_width, text_height), baseline = cv2.getTextSize( | |
| title, font, font_scale, thickness | |
| ) | |
| # Create space for title | |
| title_height = text_height + baseline + 20 | |
| titled_image = np.ones((image.shape[0] + title_height, image.shape[1], 3), dtype=np.uint8) * 255 | |
| titled_image[title_height:, :] = image | |
| # Draw title | |
| x = (image.shape[1] - text_width) // 2 | |
| y = text_height + 10 | |
| cv2.putText( | |
| titled_image, | |
| title, | |
| (x, y), | |
| font, | |
| font_scale, | |
| (0, 0, 0), | |
| thickness, | |
| cv2.LINE_AA | |
| ) | |
| return titled_image | |
| def create_detection_report( | |
| image: np.ndarray, | |
| detections: Dict[str, List[np.ndarray]], | |
| output_path: str, | |
| title: str = "Detection Report" | |
| ): | |
| """ | |
| Create a comprehensive detection report with statistics. | |
| Parameters | |
| ---------- | |
| image : np.ndarray | |
| Original image | |
| detections : Dict[str, List[np.ndarray]] | |
| Detection results | |
| output_path : str | |
| Path to save the report image | |
| title : str, default="Detection Report" | |
| Report title | |
| """ | |
| # Create visualization | |
| viz = visualize_detections(image, detections, show_labels=True) | |
| # Add title | |
| viz = _add_title(viz, title) | |
| # Add statistics panel | |
| stats_text = [ | |
| f"Walls: {len(detections.get('walls', []))}", | |
| f"Rooms: {len(detections.get('rooms', []))}", | |
| f"Doors: {len(detections.get('doors', []))}", | |
| f"Windows: {len(detections.get('windows', []))}" | |
| ] | |
| # Draw statistics | |
| y_offset = 50 | |
| for i, text in enumerate(stats_text): | |
| cv2.putText( | |
| viz, | |
| text, | |
| (10, y_offset + i * 25), | |
| cv2.FONT_HERSHEY_SIMPLEX, | |
| 0.6, | |
| (0, 0, 0), | |
| 2, | |
| cv2.LINE_AA | |
| ) | |
| # Save report | |
| cv2.imwrite(output_path, viz) | |
| print(f"Detection report saved to: {output_path}") | |