import torch class DebugVisualizer: """ Debug visualization tools for light estimation analysis. """ @staticmethod def generate_debug_mask(mask): """ Generate debug visualization of the binary mask. """ # Ensure mask is 3D (B, H, W) if mask.dim() == 4 and mask.shape[-1] == 1: mask = mask.squeeze(-1) # Convert to RGB visualization: white for lit areas, black for unlit debug_mask = mask.float().unsqueeze(-1).repeat(1, 1, 1, 3) return debug_mask @staticmethod def generate_lit_normals_visualization(normals, mask): """ Generate visualization showing only the lit normal vectors. """ normal_rgb = (normals + 1.0) / 2.0 lit_mask = mask.unsqueeze(-1) lit_normals_viz = normal_rgb * lit_mask return lit_normals_viz @staticmethod def generate_directional_visualization(normals, results): """ Generate color-coded visualization of directional analysis. """ batch_size, height, width, _ = normals.shape viz = torch.zeros(batch_size, height, width, 3, device=normals.device) x_category = results['x_category'] y_category = results['y_category'] hard_soft_index = results['hard_soft_index'] overall_confidence = results['confidence']['overall_confidence'] # Base colors if x_category == "left": base_color = torch.tensor([255, 100, 100], device=normals.device) elif x_category == "right": base_color = torch.tensor([200, 80, 80], device=normals.device) else: base_color = torch.tensor([150, 150, 150], device=normals.device) # Adjust for Y direction if y_category == "top": base_color += torch.tensor([0, 100, 50], device=normals.device) elif y_category == "bottom": base_color += torch.tensor([50, -50, 0], device=normals.device) # Apply softness and confidence softness_factor = 1.0 - hard_soft_index final_color = base_color * (0.7 + 0.3 * softness_factor) * overall_confidence final_color = torch.clamp(final_color, 0, 255) viz[:, :, :] = final_color.byte() return viz.float() / 255.0 @staticmethod def generate_colormap_preview(height=256, width=256): """ Generate a clear reference guide for normal map color encoding. This shows how 3D surface orientations are encoded as RGB colors in normal maps, making it easy to understand what each color means. """ # Create reference visualization viz = torch.zeros(height, width, 3) # Color legend for normal map encoding normal_colors = { 'right': torch.tensor([1.0, 0.5, 0.5]), # Red - Right facing 'left': torch.tensor([0.5, 1.0, 1.0]), # Cyan - Left facing 'up': torch.tensor([0.5, 1.0, 0.5]), # Green - Up facing 'down': torch.tensor([1.0, 0.5, 1.0]), # Purple - Down facing 'out': torch.tensor([0.5, 0.5, 1.0]), # Blue - Outward facing 'flat': torch.tensor([0.5, 0.5, 0.5]), # Gray - Flat/neutral } # Create visual layout section_height = height // 3 section_width = width // 3 sections = [ ('X+ (Right)', normal_colors['right'], 0, 0), ('X- (Left)', normal_colors['left'], 0, 1), ('Y+ (Up)', normal_colors['up'], 1, 0), ('Y- (Down)', normal_colors['down'], 1, 1), ('Z+ (Out)', normal_colors['out'], 2, 0), ('Z=0 (Flat)', normal_colors['flat'], 2, 1), ] # Draw color sections with labels for label, color, row, col in sections: y_start, x_start = row * section_height, col * section_width # Fill section with color viz[y_start:y_start+section_height, x_start:x_start+section_width] = color # Add simple label area (could be enhanced with text rendering) label_y = y_start + section_height // 2 label_x = x_start + 10 # Add title and explanation title_area_height = height // 6 viz[:title_area_height, :] = 0.8 # Light gray background for title return viz.unsqueeze(0) @staticmethod def generate_color_histogram(normals, title="Normal Distribution"): """ Generate a clear, informative histogram of normal map color distribution. Args: normals: Normal vectors tensor (B, H, W, 3) in [-1, 1] range title: Title for the histogram Returns: histogram_viz: Clear visualization with labels and reference (1, 512, 512, 3) """ return DebugVisualizer._create_enhanced_histogram(normals, title, show_reference=True) @staticmethod def generate_histogram_before(normals, title="Before Masking"): """ Generate histogram of full normal map distribution. Args: normals: Normal vectors tensor (B, H, W, 3) for entire image title: Title for the histogram Returns: histogram_viz: RGB histogram of full distribution (1, 256, 256, 3) """ return DebugVisualizer._create_color_histogram(normals, title) @staticmethod def generate_histogram_after(lit_normals, title="After Masking (Lit Areas)"): """ Generate histogram of lit areas normal distribution. Args: lit_normals: Normal vectors tensor (N, 3) for lit areas only title: Title for the histogram Returns: histogram_viz: RGB histogram of lit areas distribution (1, 256, 256, 3) """ # Add batch dimension if needed for consistent processing if lit_normals.dim() == 2: lit_normals = lit_normals.unsqueeze(0) return DebugVisualizer._create_color_histogram(lit_normals, title) @staticmethod def _create_color_histogram(normals, title="Color Distribution"): """ Internal method to create color histogram from normal vectors. Args: normals: Normal vectors tensor (B, H, W, 3) or (N, 3) title: Title for the histogram Returns: histogram_viz: RGB visualization (1, 256, 256, 3) """ # Flatten normals to analyze distribution flat_normals = normals.view(-1, 3) if flat_normals.shape[0] == 0: return torch.zeros(1, 256, 256, 3) # Scale normal vectors from [-1, 1] to [0, 255] for histogram bins scaled_normals = (flat_normals + 1.0) * 127.5 scaled_normals = torch.clamp(scaled_normals, 0, 255) # Create 2D histogram using Red and Green channels as coordinates hist_size = 256 histogram = torch.zeros(hist_size, hist_size) # Sample for performance sample_size = min(flat_normals.shape[0], 100000) indices = torch.randperm(flat_normals.shape[0])[:sample_size] for i in indices: normal = scaled_normals[i] r, g, b = normal[0].item(), normal[1].item(), normal[2].item() # Use red and green as histogram coordinates r_bin = int(r) g_bin = int(g) if 0 <= r_bin < hist_size and 0 <= g_bin < hist_size: histogram[r_bin, g_bin] += 1.0 # Normalize for visualization if histogram.max() > 0: histogram = histogram / histogram.max() # Create RGB visualization rgb_histogram = torch.zeros(hist_size, hist_size, 3) for i in range(hist_size): for j in range(hist_size): density = histogram[i, j].item() if density > 0: # Color by position and density # X direction (red) + Y direction (green) + density (blue) x_intensity = i / 255.0 # Red channel shows X position y_intensity = j / 255.0 # Green channel shows Y position z_intensity = density # Blue channel shows density rgb_histogram[i, j] = torch.tensor([ x_intensity, # X direction indicator y_intensity, # Y direction indicator z_intensity # Density indicator ]) return rgb_histogram.unsqueeze(0) @staticmethod def _create_enhanced_histogram(normals, title="Normal Distribution", show_reference=True): """ Create an enhanced histogram with labels, legends, and clear visual design. Args: normals: Normal vectors tensor (B, H, W, 3) or (N, 3) title: Title for the histogram show_reference: Whether to show reference information Returns: enhanced_viz: Complete visualization with labels (1, 512, 512, 3) """ # Get base histogram (256x256) base_histogram = DebugVisualizer._create_color_histogram_internal(normals) if base_histogram is None: return torch.zeros(1, 512, 512, 3) # Create enhanced visualization with labels and reference viz_size = 512 hist_size = 256 enhanced = torch.zeros(viz_size, viz_size, 3) # Place histogram in top-left area (256x256) enhanced[:hist_size, :hist_size] = base_histogram[0] # Add reference information (right side) if show_reference: enhanced = DebugVisualizer._add_histogram_labels(enhanced, title) return enhanced.unsqueeze(0) @staticmethod def _create_color_histogram_internal(normals): """Internal histogram creation without labels""" flat_normals = normals.view(-1, 3) if flat_normals.shape[0] == 0: return None # Scale to [0, 255] for histogram bins scaled_normals = (flat_normals + 1.0) * 127.5 scaled_normals = torch.clamp(scaled_normals, 0, 255) # Create histogram hist_size = 256 histogram = torch.zeros(hist_size, hist_size) sample_size = min(flat_normals.shape[0], 50000) indices = torch.randperm(flat_normals.shape[0])[:sample_size] for i in indices: normal = scaled_normals[i] r, g, b = normal[0].item(), normal[1].item(), normal[2].item() r_bin, g_bin = int(r), int(g) if 0 <= r_bin < hist_size and 0 <= g_bin < hist_size: histogram[r_bin, g_bin] += 1.0 # Normalize if histogram.max() > 0: histogram = histogram / histogram.max() # Create visualization with clear color mapping rgb_histogram = torch.zeros(hist_size, hist_size, 3) # Add coordinate grid lines grid_color = torch.tensor([0.3, 0.3, 0.3]) for i in range(0, hist_size, 32): rgb_histogram[i, :, :] = grid_color rgb_histogram[:, i, :] = grid_color # Add density visualization for i in range(hist_size): for j in range(hist_size): density = histogram[i, j].item() if density > 0.01: # Threshold to reduce noise # Heatmap: blue (low) to red (high) if density < 0.33: intensity = density * 3 rgb_histogram[i, j] = torch.tensor([0.0, 0.0, intensity]) elif density < 0.66: intensity = (density - 0.33) * 3 rgb_histogram[i, j] = torch.tensor([intensity, 0.0, 0.0]) else: intensity = (density - 0.66) * 3 rgb_histogram[i, j] = torch.tensor([1.0, intensity, 0.0]) return rgb_histogram.unsqueeze(0) @staticmethod def _add_histogram_labels(enhanced_viz, title): """Add labels and reference information to histogram""" viz_size = enhanced_viz.shape[0] # Add title (top area) title_y = 20 # Simple title for now - could be enhanced with text rendering # Add axis labels # X-axis: "Left ← X Direction → Right" # Y-axis: "Bottom ← Y Direction → Top" # Add color legend (right side) legend_x = int(viz_size * 0.8) legend_width = viz_size - legend_x # Color legend: show what colors mean legend_colors = [ torch.tensor([0.0, 0.0, 1.0]), # Blue = low density torch.tensor([0.0, 1.0, 0.0]), # Green = medium density torch.tensor([1.0, 0.0, 0.0]), # Red = high density ] for i, color in enumerate(legend_colors): y = int(viz_size * 0.3) + i * 30 enhanced_viz[y:y+20, legend_x:legend_x+20] = color return enhanced_viz