| """ |
| Visualization module for EL defect analysis. |
| |
| Creates overlay images with color-coded defect masks: |
| - Crack → Blue (visible against bright cell regions) |
| - Dark → Red (contrast with bright areas) |
| - Cross → Cyan (distinguishable from regular cracks) |
| - Busbar → Green (feature, not defect) |
| |
| All overlays use alpha blending so original image detail remains visible. |
| Handles resize alignment to prevent mask/image size mismatches. |
| """ |
|
|
| import cv2 |
| import numpy as np |
| from typing import Dict, List, Tuple, Optional |
|
|
|
|
| |
| DEFECT_COLORS_BGR = { |
| "background": (0, 0, 0), |
| "dark": (0, 0, 255), |
| "crack": (255, 0, 0), |
| "cross": (255, 255, 0), |
| "busbar": (0, 255, 0), |
| } |
|
|
| |
| DEFECT_COLORS_RGB = { |
| "background": (0, 0, 0), |
| "dark": (255, 0, 0), |
| "crack": (0, 0, 255), |
| "cross": (0, 255, 255), |
| "busbar": (0, 255, 0), |
| } |
|
|
| CLASS_NAMES = ["background", "dark", "crack", "cross", "busbar"] |
|
|
|
|
| def create_overlay( |
| image: np.ndarray, |
| mask: np.ndarray, |
| alpha: float = 0.4, |
| show_background: bool = False, |
| ) -> np.ndarray: |
| """ |
| Create colored overlay of segmentation mask on image. |
| |
| Args: |
| image: Grayscale or BGR image (any size) |
| mask: Class index mask (any size, will be resized to match image) |
| alpha: Overlay transparency (0 = fully transparent, 1 = fully opaque) |
| show_background: If True, also color background class |
| |
| Returns: |
| BGR image with colored overlay |
| """ |
| |
| if image.ndim == 2: |
| vis = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) |
| else: |
| vis = image.copy() |
| |
| |
| if vis.dtype != np.uint8: |
| if vis.max() <= 1.0: |
| vis = (vis * 255).astype(np.uint8) |
| else: |
| vis = vis.astype(np.uint8) |
| |
| h, w = vis.shape[:2] |
| |
| |
| if mask.shape[:2] != (h, w): |
| mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST) |
| |
| |
| overlay = vis.copy() |
| |
| for class_idx, class_name in enumerate(CLASS_NAMES): |
| if class_idx == 0 and not show_background: |
| continue |
| |
| color = DEFECT_COLORS_BGR[class_name] |
| class_mask = mask == class_idx |
| |
| if class_mask.any(): |
| overlay[class_mask] = color |
| |
| |
| result = cv2.addWeighted(vis, 1 - alpha, overlay, alpha, 0) |
| |
| return result |
|
|
|
|
| def create_class_overlay( |
| image: np.ndarray, |
| mask: np.ndarray, |
| class_name: str, |
| alpha: float = 0.5, |
| color: Optional[Tuple[int, int, int]] = None, |
| ) -> np.ndarray: |
| """ |
| Create overlay for a single class. |
| |
| Args: |
| image: Grayscale or BGR image |
| mask: Binary mask for one class |
| class_name: For color lookup |
| alpha: Overlay transparency |
| color: Override color (BGR) |
| """ |
| if image.ndim == 2: |
| vis = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) |
| else: |
| vis = image.copy() |
| |
| if vis.dtype != np.uint8: |
| vis = (vis * 255).astype(np.uint8) if vis.max() <= 1 else vis.astype(np.uint8) |
| |
| h, w = vis.shape[:2] |
| if mask.shape[:2] != (h, w): |
| mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST) |
| |
| if color is None: |
| color = DEFECT_COLORS_BGR.get(class_name, (255, 255, 255)) |
| |
| overlay = vis.copy() |
| overlay[mask > 0] = color |
| |
| return cv2.addWeighted(vis, 1 - alpha, overlay, alpha, 0) |
|
|
|
|
| def create_color_mask( |
| mask: np.ndarray, |
| include_background: bool = False, |
| ) -> np.ndarray: |
| """ |
| Convert class index mask to RGB color visualization. |
| |
| Returns: |
| (H, W, 3) uint8 RGB image |
| """ |
| h, w = mask.shape[:2] |
| color_img = np.zeros((h, w, 3), dtype=np.uint8) |
| |
| for class_idx, class_name in enumerate(CLASS_NAMES): |
| if class_idx == 0 and not include_background: |
| continue |
| |
| color = DEFECT_COLORS_RGB[class_name] |
| color_img[mask == class_idx] = color |
| |
| return color_img |
|
|
|
|
| def draw_cell_results( |
| image: np.ndarray, |
| cell_results: List[dict], |
| cells: list, |
| ) -> np.ndarray: |
| """ |
| Draw cell analysis results on module image. |
| |
| Shows per-cell: |
| - Bounding box (green = PASS, red = FAIL) |
| - Cell ID |
| - Defect score |
| """ |
| if image.ndim == 2: |
| vis = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) |
| else: |
| vis = image.copy() |
| |
| if vis.dtype != np.uint8: |
| vis = (vis * 255).astype(np.uint8) if vis.max() <= 1 else vis.astype(np.uint8) |
| |
| for cell_info, result in zip(cells, cell_results): |
| y1, x1, y2, x2 = cell_info.bbox |
| score = result.get("defect_score", 0) |
| |
| |
| if score < 25: |
| color = (0, 255, 0) |
| elif score < 50: |
| color = (0, 255, 255) |
| else: |
| color = (0, 0, 255) |
| |
| cv2.rectangle(vis, (x1, y1), (x2, y2), color, 2) |
| |
| |
| label = f"C{cell_info.cell_id}: {score:.0f}" |
| cv2.putText( |
| vis, label, (x1 + 2, y1 + 15), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1 |
| ) |
| |
| return vis |
|
|
|
|
| def create_summary_image( |
| original: np.ndarray, |
| overlay: np.ndarray, |
| mask_color: np.ndarray, |
| decision: str, |
| score: float, |
| ) -> np.ndarray: |
| """ |
| Create a summary image with original, overlay, and color mask side by side. |
| |
| Returns: |
| (H, W*3, 3) BGR image with all three panels |
| """ |
| |
| panels = [] |
| for img in [original, overlay, mask_color]: |
| if img.ndim == 2: |
| img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) |
| if img.dtype != np.uint8: |
| img = (img * 255).astype(np.uint8) if img.max() <= 1 else img.astype(np.uint8) |
| panels.append(img) |
| |
| |
| target_h = 400 |
| resized = [] |
| for p in panels: |
| scale = target_h / p.shape[0] |
| new_w = int(p.shape[1] * scale) |
| resized.append(cv2.resize(p, (new_w, target_h))) |
| |
| |
| summary = np.hstack(resized) |
| |
| |
| color = (0, 255, 0) if decision == "PASS" else (0, 0, 255) |
| text = f"{decision} (Score: {score:.1f})" |
| cv2.putText( |
| summary, text, (10, 30), |
| cv2.FONT_HERSHEY_SIMPLEX, 1.0, color, 2 |
| ) |
| |
| return summary |
|
|
|
|
| def create_legend(height: int = 400, width: int = 200) -> np.ndarray: |
| """Create a color legend for defect classes.""" |
| legend = np.ones((height, width, 3), dtype=np.uint8) * 255 |
| |
| y_offset = 30 |
| for class_name in CLASS_NAMES[1:]: |
| color = DEFECT_COLORS_BGR[class_name] |
| |
| |
| cv2.rectangle( |
| legend, (10, y_offset), (40, y_offset + 20), color, -1 |
| ) |
| cv2.rectangle( |
| legend, (10, y_offset), (40, y_offset + 20), (0, 0, 0), 1 |
| ) |
| |
| |
| cv2.putText( |
| legend, class_name.capitalize(), (50, y_offset + 15), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1 |
| ) |
| |
| y_offset += 35 |
| |
| return legend |
|
|