nithishbasireddy commited on
Commit
b73f4b0
·
verified ·
1 Parent(s): ec6198e

Upload src/pipeline/preprocessing.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pipeline/preprocessing.py +208 -0
src/pipeline/preprocessing.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Preprocessing pipeline for EL images.
3
+
4
+ This is CRITICAL for production robustness. Real-world EL images vary wildly:
5
+ - Factory cameras with different exposure settings
6
+ - Degraded modules with overall lower luminescence
7
+ - Overexposed images from new, high-efficiency cells
8
+ - Noisy images from long-exposure captures
9
+
10
+ The preprocessing pipeline normalizes ALL inputs to look similar,
11
+ making the model's job much easier.
12
+
13
+ Pipeline:
14
+ 1. Convert to grayscale (if not already)
15
+ 2. CLAHE: Contrast Limited Adaptive Histogram Equalization
16
+ - Enhances local contrast without amplifying noise
17
+ - tileGridSize=(8,8): processes image in 8x8 blocks
18
+ - clipLimit=2.0: prevents over-enhancement
19
+ 3. Intensity normalization: scale to [0, 1]
20
+ 4. Resize to consistent input size
21
+ """
22
+
23
+ import cv2
24
+ import numpy as np
25
+ from typing import Tuple, Optional
26
+
27
+
28
+ class ELPreprocessor:
29
+ """
30
+ Production preprocessor for EL images.
31
+
32
+ Handles: dark images, bright images, varying sizes, noise.
33
+ Produces: consistent normalized grayscale output.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ target_size: Tuple[int, int] = (512, 512),
39
+ clahe_clip_limit: float = 2.0,
40
+ clahe_tile_grid: Tuple[int, int] = (8, 8),
41
+ denoise: bool = True,
42
+ denoise_strength: int = 7,
43
+ ):
44
+ """
45
+ Args:
46
+ target_size: (H, W) output size
47
+ clahe_clip_limit: CLAHE contrast limit. Higher = more enhancement.
48
+ 2.0 is standard; use 3.0-4.0 for very dark images.
49
+ clahe_tile_grid: CLAHE tile size. (8,8) is standard.
50
+ Smaller tiles = more local contrast enhancement.
51
+ denoise: Apply non-local means denoising
52
+ denoise_strength: Denoising filter strength (higher = more smoothing)
53
+ """
54
+ self.target_size = target_size
55
+ self.clahe = cv2.createCLAHE(
56
+ clipLimit=clahe_clip_limit,
57
+ tileGridSize=clahe_tile_grid
58
+ )
59
+ self.denoise = denoise
60
+ self.denoise_strength = denoise_strength
61
+
62
+ def process(self, image: np.ndarray) -> np.ndarray:
63
+ """
64
+ Full preprocessing pipeline.
65
+
66
+ Args:
67
+ image: Input image (any format: RGB, grayscale, any size, any bit depth)
68
+
69
+ Returns:
70
+ Preprocessed grayscale image, shape (H, W), dtype float32, range [0, 1]
71
+ """
72
+ # Step 1: Convert to grayscale
73
+ gray = self._to_grayscale(image)
74
+
75
+ # Step 2: Denoise (before CLAHE to prevent noise amplification)
76
+ if self.denoise:
77
+ gray = self._denoise(gray)
78
+
79
+ # Step 3: CLAHE — adaptive contrast enhancement
80
+ # This is the most important step: makes dark images visible,
81
+ # prevents bright images from being washed out
82
+ enhanced = self.clahe.apply(gray)
83
+
84
+ # Step 4: Intensity normalization to [0, 1]
85
+ normalized = self._normalize_intensity(enhanced)
86
+
87
+ # Step 5: Resize to target size
88
+ resized = cv2.resize(
89
+ normalized,
90
+ (self.target_size[1], self.target_size[0]), # cv2 uses (W, H)
91
+ interpolation=cv2.INTER_LINEAR
92
+ )
93
+
94
+ return resized.astype(np.float32)
95
+
96
+ def process_for_model(self, image: np.ndarray) -> np.ndarray:
97
+ """
98
+ Process image and prepare for model input.
99
+
100
+ Returns:
101
+ Shape (1, H, W) float32, normalized with mean=0.5, std=0.5
102
+ """
103
+ processed = self.process(image)
104
+ # Normalize to match training: (x - 0.5) / 0.5
105
+ model_input = (processed - 0.5) / 0.5
106
+ return model_input[np.newaxis, ...] # Add channel dim: (1, H, W)
107
+
108
+ def _to_grayscale(self, image: np.ndarray) -> np.ndarray:
109
+ """Convert any format to 8-bit grayscale."""
110
+ if image is None or image.size == 0:
111
+ raise ValueError("Empty or None image received")
112
+
113
+ if image.ndim == 3:
114
+ if image.shape[2] == 4: # RGBA
115
+ image = cv2.cvtColor(image, cv2.COLOR_RGBA2GRAY)
116
+ elif image.shape[2] == 3: # RGB/BGR
117
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
118
+ else:
119
+ image = image[:, :, 0] # Take first channel
120
+
121
+ # Handle 16-bit images (some industrial cameras)
122
+ if image.dtype == np.uint16:
123
+ image = (image / 256).astype(np.uint8)
124
+ elif image.dtype == np.float32 or image.dtype == np.float64:
125
+ if image.max() <= 1.0:
126
+ image = (image * 255).astype(np.uint8)
127
+ else:
128
+ image = np.clip(image, 0, 255).astype(np.uint8)
129
+ elif image.dtype != np.uint8:
130
+ image = image.astype(np.uint8)
131
+
132
+ return image
133
+
134
+ def _denoise(self, gray: np.ndarray) -> np.ndarray:
135
+ """
136
+ Non-local means denoising.
137
+
138
+ Trade-off: removes sensor noise but can blur thin cracks.
139
+ strength=7 is conservative; increase for very noisy images.
140
+ """
141
+ return cv2.fastNlMeansDenoising(
142
+ gray,
143
+ h=self.denoise_strength,
144
+ templateWindowSize=7,
145
+ searchWindowSize=21
146
+ )
147
+
148
+ def _normalize_intensity(self, image: np.ndarray) -> np.ndarray:
149
+ """
150
+ Percentile-based intensity normalization.
151
+
152
+ Why percentile instead of min-max?
153
+ - Hot/dead pixels don't skew the range
154
+ - More robust for real-world images
155
+ - 1st and 99th percentile clips extreme outliers
156
+ """
157
+ p_low = np.percentile(image, 1)
158
+ p_high = np.percentile(image, 99)
159
+
160
+ if p_high - p_low < 10: # Very low contrast image
161
+ # Fallback to full-range normalization
162
+ p_low = image.min()
163
+ p_high = image.max()
164
+
165
+ if p_high == p_low:
166
+ return np.zeros_like(image, dtype=np.float32)
167
+
168
+ normalized = (image.astype(np.float32) - p_low) / (p_high - p_low)
169
+ return np.clip(normalized, 0, 1)
170
+
171
+ def get_image_stats(self, image: np.ndarray) -> dict:
172
+ """
173
+ Compute diagnostic statistics for an EL image.
174
+ Useful for quality assessment and adaptive parameter tuning.
175
+ """
176
+ gray = self._to_grayscale(image)
177
+ return {
178
+ "mean_intensity": float(gray.mean()),
179
+ "std_intensity": float(gray.std()),
180
+ "min_intensity": int(gray.min()),
181
+ "max_intensity": int(gray.max()),
182
+ "dynamic_range": int(gray.max()) - int(gray.min()),
183
+ "is_dark": gray.mean() < 50,
184
+ "is_bright": gray.mean() > 200,
185
+ "is_low_contrast": gray.std() < 20,
186
+ "shape": gray.shape,
187
+ }
188
+
189
+
190
+ def batch_preprocess(
191
+ images: list,
192
+ preprocessor: Optional[ELPreprocessor] = None,
193
+ ) -> np.ndarray:
194
+ """
195
+ Preprocess a batch of images for model input.
196
+
197
+ Returns:
198
+ (N, 1, H, W) float32 array ready for torch.from_numpy()
199
+ """
200
+ if preprocessor is None:
201
+ preprocessor = ELPreprocessor()
202
+
203
+ batch = []
204
+ for img in images:
205
+ processed = preprocessor.process_for_model(img)
206
+ batch.append(processed)
207
+
208
+ return np.stack(batch, axis=0)