Upload src/pipeline/full_pipeline.py with huggingface_hub
Browse files- src/pipeline/full_pipeline.py +358 -0
src/pipeline/full_pipeline.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Full Pipeline Orchestrator.
|
| 3 |
+
|
| 4 |
+
Connects all pipeline stages into a single coherent system:
|
| 5 |
+
|
| 6 |
+
input image
|
| 7 |
+
β
|
| 8 |
+
preprocessing (normalize brightness)
|
| 9 |
+
β
|
| 10 |
+
module segmentation (grid detection)
|
| 11 |
+
β
|
| 12 |
+
cell extraction
|
| 13 |
+
β
|
| 14 |
+
deep learning inference (U-Net)
|
| 15 |
+
β
|
| 16 |
+
post-processing (clean masks)
|
| 17 |
+
β
|
| 18 |
+
defect analysis
|
| 19 |
+
β
|
| 20 |
+
decision engine
|
| 21 |
+
β
|
| 22 |
+
visualization + results
|
| 23 |
+
|
| 24 |
+
This is the main entry point for the entire system.
|
| 25 |
+
All error handling and fallback logic is centralized here.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
import cv2
|
| 29 |
+
import numpy as np
|
| 30 |
+
import time
|
| 31 |
+
from typing import Dict, List, Optional, Tuple
|
| 32 |
+
from dataclasses import dataclass, field
|
| 33 |
+
|
| 34 |
+
from .preprocessing import ELPreprocessor
|
| 35 |
+
from .module_segmentation import ModuleSegmenter, CellInfo, estimate_pixel_to_mm
|
| 36 |
+
from .inference import DefectInferenceEngine, MockInferenceEngine
|
| 37 |
+
from .mask_cleaning import MaskCleaner
|
| 38 |
+
from .crack_analysis import DefectAnalyzer, CellAnalysisResult
|
| 39 |
+
from .decision_engine import DecisionEngine, ModuleDecision
|
| 40 |
+
from .visualization import (
|
| 41 |
+
create_overlay, create_color_mask, draw_cell_results,
|
| 42 |
+
create_summary_image, DEFECT_COLORS_RGB, CLASS_NAMES
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@dataclass
|
| 47 |
+
class PipelineResult:
|
| 48 |
+
"""Complete pipeline output."""
|
| 49 |
+
# Input
|
| 50 |
+
original_image: np.ndarray
|
| 51 |
+
preprocessed_image: np.ndarray
|
| 52 |
+
|
| 53 |
+
# Segmentation
|
| 54 |
+
cells: List[CellInfo]
|
| 55 |
+
num_cells: int
|
| 56 |
+
grid_image: Optional[np.ndarray] = None
|
| 57 |
+
|
| 58 |
+
# Per-cell results
|
| 59 |
+
cell_masks: List[np.ndarray] = field(default_factory=list)
|
| 60 |
+
cell_overlays: List[np.ndarray] = field(default_factory=list)
|
| 61 |
+
cell_analyses: List[CellAnalysisResult] = field(default_factory=list)
|
| 62 |
+
|
| 63 |
+
# Module-level
|
| 64 |
+
module_mask: Optional[np.ndarray] = None
|
| 65 |
+
module_overlay: Optional[np.ndarray] = None
|
| 66 |
+
decision: Optional[ModuleDecision] = None
|
| 67 |
+
|
| 68 |
+
# Timing
|
| 69 |
+
timings: Dict[str, float] = field(default_factory=dict)
|
| 70 |
+
|
| 71 |
+
# Errors/warnings
|
| 72 |
+
warnings: List[str] = field(default_factory=list)
|
| 73 |
+
|
| 74 |
+
def to_dict(self) -> dict:
|
| 75 |
+
return {
|
| 76 |
+
"num_cells": self.num_cells,
|
| 77 |
+
"decision": self.decision.to_dict() if self.decision else None,
|
| 78 |
+
"cell_analyses": [c.to_dict() for c in self.cell_analyses],
|
| 79 |
+
"timings": {k: round(v, 3) for k, v in self.timings.items()},
|
| 80 |
+
"warnings": self.warnings,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class ELInspectionPipeline:
|
| 85 |
+
"""
|
| 86 |
+
Production-grade EL defect inspection pipeline.
|
| 87 |
+
|
| 88 |
+
Usage:
|
| 89 |
+
pipeline = ELInspectionPipeline(model_path="model.pth")
|
| 90 |
+
result = pipeline.run(image)
|
| 91 |
+
|
| 92 |
+
# Access results
|
| 93 |
+
print(result.decision.decision) # "PASS" or "FAIL"
|
| 94 |
+
print(result.decision.overall_score) # 0-100
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
def __init__(
|
| 98 |
+
self,
|
| 99 |
+
model_path: Optional[str] = None,
|
| 100 |
+
encoder_name: str = "resnet34",
|
| 101 |
+
input_size: int = 512,
|
| 102 |
+
device: Optional[str] = None,
|
| 103 |
+
cell_type: str = "standard",
|
| 104 |
+
quality_standard: str = "default",
|
| 105 |
+
use_mock: bool = False,
|
| 106 |
+
):
|
| 107 |
+
"""
|
| 108 |
+
Args:
|
| 109 |
+
model_path: Path to trained model weights
|
| 110 |
+
encoder_name: Model encoder name
|
| 111 |
+
input_size: Model input size
|
| 112 |
+
device: 'cuda', 'cpu', or None for auto
|
| 113 |
+
cell_type: Solar cell type for mm conversion
|
| 114 |
+
quality_standard: 'default', 'strict', or 'lenient'
|
| 115 |
+
use_mock: Use mock inference (for testing without model)
|
| 116 |
+
"""
|
| 117 |
+
self.input_size = input_size
|
| 118 |
+
self.cell_type = cell_type
|
| 119 |
+
|
| 120 |
+
# Initialize components
|
| 121 |
+
self.preprocessor = ELPreprocessor(
|
| 122 |
+
target_size=(input_size, input_size)
|
| 123 |
+
)
|
| 124 |
+
self.segmenter = ModuleSegmenter()
|
| 125 |
+
|
| 126 |
+
if use_mock or model_path is None:
|
| 127 |
+
self.engine = MockInferenceEngine(input_size=input_size)
|
| 128 |
+
self._using_mock = True
|
| 129 |
+
else:
|
| 130 |
+
try:
|
| 131 |
+
self.engine = DefectInferenceEngine(
|
| 132 |
+
model_path=model_path,
|
| 133 |
+
encoder_name=encoder_name,
|
| 134 |
+
device=device,
|
| 135 |
+
input_size=input_size,
|
| 136 |
+
)
|
| 137 |
+
self._using_mock = False
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"WARNING: Failed to load model: {e}. Using mock inference.")
|
| 140 |
+
self.engine = MockInferenceEngine(input_size=input_size)
|
| 141 |
+
self._using_mock = True
|
| 142 |
+
|
| 143 |
+
self.mask_cleaner = MaskCleaner()
|
| 144 |
+
self.decision_engine = DecisionEngine(standard=quality_standard)
|
| 145 |
+
|
| 146 |
+
def run(
|
| 147 |
+
self,
|
| 148 |
+
image: np.ndarray,
|
| 149 |
+
custom_thresholds: Optional[Dict] = None,
|
| 150 |
+
) -> PipelineResult:
|
| 151 |
+
"""
|
| 152 |
+
Run the full inspection pipeline.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
image: Input EL image (any format/size)
|
| 156 |
+
custom_thresholds: Override decision thresholds
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
PipelineResult with all outputs
|
| 160 |
+
"""
|
| 161 |
+
timings = {}
|
| 162 |
+
warnings = []
|
| 163 |
+
|
| 164 |
+
# ββ Step 1: Preprocessing βββββββββββββββββββββββββββββ
|
| 165 |
+
t0 = time.time()
|
| 166 |
+
try:
|
| 167 |
+
preprocessed = self.preprocessor.process(image)
|
| 168 |
+
stats = self.preprocessor.get_image_stats(image)
|
| 169 |
+
|
| 170 |
+
if stats["is_dark"]:
|
| 171 |
+
warnings.append("Input image is very dark β results may be less reliable")
|
| 172 |
+
if stats["is_low_contrast"]:
|
| 173 |
+
warnings.append("Input image has low contrast β enhanced with CLAHE")
|
| 174 |
+
except Exception as e:
|
| 175 |
+
warnings.append(f"Preprocessing error: {e}. Using raw image.")
|
| 176 |
+
preprocessed = self._safe_grayscale(image)
|
| 177 |
+
|
| 178 |
+
timings["preprocessing"] = time.time() - t0
|
| 179 |
+
|
| 180 |
+
# ββ Step 2: Module Segmentation βββββββββββββββββββββββ
|
| 181 |
+
t0 = time.time()
|
| 182 |
+
try:
|
| 183 |
+
# Use preprocessed image for grid detection (better contrast)
|
| 184 |
+
gray_uint8 = (preprocessed * 255).astype(np.uint8)
|
| 185 |
+
cells = self.segmenter.segment(gray_uint8)
|
| 186 |
+
grid_image = self.segmenter.get_grid_visualization(gray_uint8, cells)
|
| 187 |
+
except Exception as e:
|
| 188 |
+
warnings.append(f"Grid detection failed: {e}. Treating as single cell.")
|
| 189 |
+
h, w = preprocessed.shape[:2]
|
| 190 |
+
cells = [CellInfo(
|
| 191 |
+
cell_id=1, row=0, col=0,
|
| 192 |
+
image=gray_uint8 if 'gray_uint8' in dir() else (preprocessed * 255).astype(np.uint8),
|
| 193 |
+
bbox=(0, 0, h, w), area_pixels=h * w
|
| 194 |
+
)]
|
| 195 |
+
grid_image = None
|
| 196 |
+
|
| 197 |
+
timings["segmentation"] = time.time() - t0
|
| 198 |
+
|
| 199 |
+
# ββ Step 3: Estimate pixel-to-mm scale ββββββββββββββββ
|
| 200 |
+
if len(cells) > 1:
|
| 201 |
+
# Use median cell size for calibration
|
| 202 |
+
cell_widths = [c.bbox[3] - c.bbox[1] for c in cells]
|
| 203 |
+
cell_heights = [c.bbox[2] - c.bbox[0] for c in cells]
|
| 204 |
+
median_w = int(np.median(cell_widths))
|
| 205 |
+
median_h = int(np.median(cell_heights))
|
| 206 |
+
px_per_mm_factor = estimate_pixel_to_mm(median_w, median_h, self.cell_type)
|
| 207 |
+
px_per_mm = 1.0 / px_per_mm_factor if px_per_mm_factor > 0 else 5.0
|
| 208 |
+
else:
|
| 209 |
+
# Single cell: estimate from image size
|
| 210 |
+
h, w = preprocessed.shape[:2]
|
| 211 |
+
px_per_mm_factor = estimate_pixel_to_mm(w, h, self.cell_type)
|
| 212 |
+
px_per_mm = 1.0 / px_per_mm_factor if px_per_mm_factor > 0 else 5.0
|
| 213 |
+
|
| 214 |
+
# ββ Step 4: Per-cell inference + analysis βββββββββββββ
|
| 215 |
+
t0 = time.time()
|
| 216 |
+
cell_masks = []
|
| 217 |
+
cell_overlays = []
|
| 218 |
+
cell_analyses = []
|
| 219 |
+
|
| 220 |
+
defect_analyzer = DefectAnalyzer(px_per_mm=px_per_mm)
|
| 221 |
+
|
| 222 |
+
for cell in cells:
|
| 223 |
+
try:
|
| 224 |
+
# Run inference on cell image
|
| 225 |
+
cell_img = cell.image
|
| 226 |
+
|
| 227 |
+
# Get mask
|
| 228 |
+
mask = self.engine.predict(cell_img)
|
| 229 |
+
|
| 230 |
+
# Clean mask
|
| 231 |
+
cleaned_mask = self.mask_cleaner.clean(mask)
|
| 232 |
+
cell_masks.append(cleaned_mask)
|
| 233 |
+
|
| 234 |
+
# Create overlay
|
| 235 |
+
overlay = create_overlay(cell_img, cleaned_mask, alpha=0.4)
|
| 236 |
+
cell_overlays.append(overlay)
|
| 237 |
+
|
| 238 |
+
# Analyze defects
|
| 239 |
+
# Resize cell image to match mask size for analysis
|
| 240 |
+
if cell_img.shape[:2] != cleaned_mask.shape[:2]:
|
| 241 |
+
cell_img_resized = cv2.resize(
|
| 242 |
+
cell_img,
|
| 243 |
+
(cleaned_mask.shape[1], cleaned_mask.shape[0]),
|
| 244 |
+
interpolation=cv2.INTER_LINEAR
|
| 245 |
+
)
|
| 246 |
+
else:
|
| 247 |
+
cell_img_resized = cell_img
|
| 248 |
+
|
| 249 |
+
analysis = defect_analyzer.analyze_cell(
|
| 250 |
+
cell_img_resized, cleaned_mask, cell.cell_id
|
| 251 |
+
)
|
| 252 |
+
cell_analyses.append(analysis)
|
| 253 |
+
|
| 254 |
+
except Exception as e:
|
| 255 |
+
warnings.append(f"Cell {cell.cell_id} analysis failed: {e}")
|
| 256 |
+
# Create empty result
|
| 257 |
+
from .crack_analysis import DarkResult
|
| 258 |
+
cell_masks.append(np.zeros_like(mask if 'mask' in dir() else np.zeros((self.input_size, self.input_size), dtype=np.uint8)))
|
| 259 |
+
cell_overlays.append(cell.image)
|
| 260 |
+
cell_analyses.append(CellAnalysisResult(
|
| 261 |
+
cell_id=cell.cell_id, cracks=[], dark=DarkResult(0,0,0,0,0,"none"),
|
| 262 |
+
cross_cracks=[], total_crack_length_mm=0, num_cracks=0,
|
| 263 |
+
num_cross_cracks=0, max_crack_severity="none", defect_score=0
|
| 264 |
+
))
|
| 265 |
+
|
| 266 |
+
timings["inference_analysis"] = time.time() - t0
|
| 267 |
+
|
| 268 |
+
# ββ Step 5: Compose module-level mask βββββββββββββββββ
|
| 269 |
+
t0 = time.time()
|
| 270 |
+
try:
|
| 271 |
+
module_mask = self._compose_module_mask(
|
| 272 |
+
preprocessed.shape, cells, cell_masks
|
| 273 |
+
)
|
| 274 |
+
module_overlay = create_overlay(
|
| 275 |
+
(preprocessed * 255).astype(np.uint8),
|
| 276 |
+
module_mask, alpha=0.4
|
| 277 |
+
)
|
| 278 |
+
except Exception as e:
|
| 279 |
+
warnings.append(f"Module mask composition failed: {e}")
|
| 280 |
+
module_mask = None
|
| 281 |
+
module_overlay = None
|
| 282 |
+
|
| 283 |
+
timings["visualization"] = time.time() - t0
|
| 284 |
+
|
| 285 |
+
# ββ Step 6: Module-level decision βββββββββββββββββββββ
|
| 286 |
+
t0 = time.time()
|
| 287 |
+
|
| 288 |
+
if custom_thresholds:
|
| 289 |
+
self.decision_engine.update_thresholds(**custom_thresholds)
|
| 290 |
+
|
| 291 |
+
decision = self.decision_engine.decide(cell_analyses)
|
| 292 |
+
timings["decision"] = time.time() - t0
|
| 293 |
+
|
| 294 |
+
if self._using_mock:
|
| 295 |
+
warnings.append("Using mock inference β results are approximate. Load a trained model for production use.")
|
| 296 |
+
|
| 297 |
+
timings["total"] = sum(timings.values())
|
| 298 |
+
|
| 299 |
+
return PipelineResult(
|
| 300 |
+
original_image=image,
|
| 301 |
+
preprocessed_image=preprocessed,
|
| 302 |
+
cells=cells,
|
| 303 |
+
num_cells=len(cells),
|
| 304 |
+
grid_image=grid_image,
|
| 305 |
+
cell_masks=cell_masks,
|
| 306 |
+
cell_overlays=cell_overlays,
|
| 307 |
+
cell_analyses=cell_analyses,
|
| 308 |
+
module_mask=module_mask,
|
| 309 |
+
module_overlay=module_overlay,
|
| 310 |
+
decision=decision,
|
| 311 |
+
timings=timings,
|
| 312 |
+
warnings=warnings,
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
def _compose_module_mask(
|
| 316 |
+
self,
|
| 317 |
+
shape: Tuple,
|
| 318 |
+
cells: List[CellInfo],
|
| 319 |
+
cell_masks: List[np.ndarray]
|
| 320 |
+
) -> np.ndarray:
|
| 321 |
+
"""
|
| 322 |
+
Compose per-cell masks back into a module-level mask.
|
| 323 |
+
|
| 324 |
+
Handles size mismatches between cell extraction and model output.
|
| 325 |
+
"""
|
| 326 |
+
h, w = shape[:2]
|
| 327 |
+
module_mask = np.zeros((h, w), dtype=np.uint8)
|
| 328 |
+
|
| 329 |
+
for cell, mask in zip(cells, cell_masks):
|
| 330 |
+
y1, x1, y2, x2 = cell.bbox
|
| 331 |
+
cell_h, cell_w = y2 - y1, x2 - x1
|
| 332 |
+
|
| 333 |
+
# Resize mask to cell bbox size
|
| 334 |
+
if mask.shape[:2] != (cell_h, cell_w):
|
| 335 |
+
resized = cv2.resize(
|
| 336 |
+
mask, (cell_w, cell_h),
|
| 337 |
+
interpolation=cv2.INTER_NEAREST
|
| 338 |
+
)
|
| 339 |
+
else:
|
| 340 |
+
resized = mask
|
| 341 |
+
|
| 342 |
+
# Place in module mask
|
| 343 |
+
module_mask[y1:y2, x1:x2] = resized
|
| 344 |
+
|
| 345 |
+
return module_mask
|
| 346 |
+
|
| 347 |
+
def _safe_grayscale(self, image: np.ndarray) -> np.ndarray:
|
| 348 |
+
"""Safely convert image to grayscale float32 [0, 1]."""
|
| 349 |
+
if image.ndim == 3:
|
| 350 |
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 351 |
+
else:
|
| 352 |
+
gray = image
|
| 353 |
+
|
| 354 |
+
if gray.dtype == np.uint8:
|
| 355 |
+
return gray.astype(np.float32) / 255.0
|
| 356 |
+
elif gray.max() > 1.0:
|
| 357 |
+
return gray.astype(np.float32) / 255.0
|
| 358 |
+
return gray.astype(np.float32)
|