nithishbasireddy commited on
Commit
219fb5f
Β·
verified Β·
1 Parent(s): 63fcffc

Upload src/pipeline/full_pipeline.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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)