File size: 8,622 Bytes
0917e8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import cv2
import torch
import matplotlib
import tkinter as tk
import numpy as np
import matplotlib.pyplot as plt

from tkinter import filedialog, messagebox
from matplotlib.colors import LinearSegmentedColormap
from matplotlib.widgets import RectangleSelector

crop_coords = None
pixel_size_um = None


def calculate_pixel_size(image, image_size_um=2.0):
    """Calculate the pixel size in micrometers based on the image dimensions."""
    image_width_pixels = image.shape[1]
    pixel_size_um = image_size_um / image_width_pixels
    return pixel_size_um


def process_image(data, height, width, pixel_size_um: float = 2.0) -> dict:
    """
    Process a 2D material with shape: [H, W]

    Returns
    ---
    - Dictionary of material properties
    """

    # Calculate pixel size
    pixel_size_um = calculate_pixel_size(data)

    # Normalize data
    normalized_data = cv2.normalize(data, None, 0, 255, cv2.NORM_MINMAX).astype(
        np.uint8
    )

    # Ensure the image is in float format for processing
    img = data.astype(np.float32)
    h, w = data.shape
    # Flattening: Fit a 1st-order polynomial (linear) to each row and subtract
    flattened_img = np.zeros_like(img, dtype=np.float32)

    for i in range(h):
        row = img[i, :]
        x = np.arange(w)

        # Fit a linear polynomial (1st order)
        p = np.polyfit(x, row, 1)  # Fit line y = ax + b

        # Compute background
        background = np.polyval(p, x)

        # Subtract the background from the row
        flattened_img[i, :] = row - background

        # flattened_img[i, :] -= np.min(flattened_img[i, :])  # Shift to keep variations

    # Normalize the image for display
    flattened_img = cv2.normalize(flattened_img, None, 0, 255, cv2.NORM_MINMAX)
    flattened_img = np.uint8(flattened_img)

    # Calculate the average current
    average_current = np.mean(data) / 1e-9

    # Define the coverage threshold
    threshold = 120 * 1e-12

    # Create a binary mask for coverage (1 = covered, 0 = uncovered)
    coverage_mask = (data > threshold).astype(int)

    # Calculate coverage percentage
    coverage_percentage = (np.sum(coverage_mask) / coverage_mask.size) * 100

    # Create a color map: Light blue for coverage, light grey for uncovered
    colors = ["black", "mistyrose"]
    cmap = LinearSegmentedColormap.from_list("coverage_map", colors, N=2)

    # Plot the coverage map
    x = np.linspace(0, 2, data.shape[1])  # X-axis (microns)
    y = np.linspace(0, 2, data.shape[0])  # Y-axis (microns)
    X, Y = np.meshgrid(x, y)

    # Apply Gamma Correction
    gamma = 0.7
    gamma_corrected_img = np.power(flattened_img / 255.0, gamma) * 255.0
    gamma_corrected_img = gamma_corrected_img.astype(np.uint8)

    # Apply Bilateral Filter
    bilateral_filtered = cv2.bilateralFilter(gamma_corrected_img, 9, 30, 75)

    # Apply Gaussian Blur
    blurred = cv2.GaussianBlur(bilateral_filtered, (3, 3), 3)

    # Adaptive Thresholding
    thresholded = cv2.adaptiveThreshold(
        blurred, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 7, 1
    )

    # Contour Detection
    contours, _ = cv2.findContours(
        thresholded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
    )
    output_image = cv2.cvtColor(blurred, cv2.COLOR_GRAY2BGR)

    curves_length = 0
    circular_shapes_area = 0
    extended_shapes_area = 0
    circular_shapes = []
    curved_lines = []
    extended_shapes = []

    for contour in contours:
        if cv2.contourArea(contour) > 0:
            perimeter = cv2.arcLength(contour, closed=True)
            area = cv2.contourArea(contour)
            circularity = 4 * np.pi * area / (perimeter**2) if perimeter != 0 else 0

            # Create a mask for the contour
            mask = np.zeros_like(thresholded, dtype=np.uint8)
            cv2.drawContours(mask, [contour], -1, 255, -1)
            # Check if the contour is inside a covered area (valid contour)
            if not np.any(coverage_mask[mask == 255]):
                continue  # Skip this contour if it falls in an uncovered region

            # Calculate the total number of pixels inside the contour
            total_pixels = cv2.countNonZero(mask)

            # Calculate the number of white pixels inside the contour
            white_pixels = cv2.countNonZero(cv2.bitwise_and(mask, thresholded))
            # Calculate the number of dark pixels inside the contour (where the thresholded image is 0)
            dark_pixels = total_pixels - cv2.countNonZero(
                cv2.bitwise_and(mask, thresholded)
            )

            x, y, w, h = cv2.boundingRect(contour)
            aspect_ratio = float(w) / h

            if circularity > 0.2 and area <= 1000:
                # Check for white pixel coverage only for circular shapes
                # if dark_pixels / total_pixels >= 0.8:
                # continue  # Skip this contour if 80% or more of the pixels are white

                # Process circular shapes
                circular_shapes.append(contour)
                circular_shapes_area += area
                cv2.drawContours(output_image, [contour], -1, (0, 255, 255), 1)
            elif aspect_ratio > 2 and area <= 1000:
                # Check for white pixel coverage only for extended shapes
                # if total_pixels > 0 and (white_pixels / total_pixels) >= 0.8:
                # continue  # Skip this contour if 80% or more of the pixels are white

                # Process extended shapes
                extended_shapes.append(contour)
                extended_shapes_area += area
                cv2.drawContours(output_image, [contour], -1, (0, 255, 255), 1)

            else:
                # First, calculate the area of the contour
                contour_area = cv2.contourArea(contour)

                if contour_area <= 250:  # Threshold for small areas
                    extended_shapes.append(contour)
                    extended_shapes_area += contour_area
                    cv2.drawContours(output_image, [contour], -1, (0, 255, 255), 1)
                else:
                    # Process curved lines without the white pixel check
                    curved_lines.append(contour)
                    curve_length = cv2.arcLength(contour, closed=True)
                    curves_length += curve_length
                    cv2.drawContours(output_image, [contour], -1, (0, 255, 0), 1)

    # Convert areas and lengths to micrometers
    circular_shapes_area_um2 = circular_shapes_area * (pixel_size_um**2)
    extended_shapes_area_um2 = extended_shapes_area * (pixel_size_um**2)
    curves_length_um = curves_length * pixel_size_um

    Total_Defect_Area = circular_shapes_area_um2 + extended_shapes_area_um2
    Total_Defect_Percentage = 100 * Total_Defect_Area / (height * width)

    results = {
        "average_current": average_current,
        "coverage_percentage": coverage_percentage,
        "circular_shapes_area": circular_shapes_area_um2,
        "extended_shapes_area": extended_shapes_area_um2,
        "curves_length": curves_length_um,
        "total_defect_area": Total_Defect_Area,
        "total_defect_percentage": Total_Defect_Percentage,
        "number_ext_shapes": len(extended_shapes),
    }

    return results


def calculate_roughness(height_data: np.ndarray) -> dict:

    if isinstance(height_data, torch.Tensor):
        height_data = height_data.cpu().numpy()

    # Zero-centering the data
    height_data -= np.mean(height_data)

    # RMS roughness in nm
    Sq = np.sqrt(np.mean(height_data**2)) * 1e9

    # Mean roughness in nm
    Sa = np.mean(np.abs(height_data)) * 1e9

    return {"RMS roughness (Sq)": Sq, "Mean roughness (Sa)": Sa}


def pcnt_abs_diff_surface_roughness(x1: torch.Tensor, x2: torch.Tensor) -> dict:

    x1 = x1.cpu().numpy()
    x2 = x2.cpu().numpy()

    char_1 = calculate_roughness(x1)
    char_2 = calculate_roughness(x2)

    diffs = {}
    for k in char_1:
        v1, v2 = char_1[k], char_2[k]
        err = abs(v1 - v2)
        diffs[k] = err

    return diffs


def calculate_abs_diff_between_samples(
    x1: torch.Tensor, x2: torch.Tensor, image_size_um: float
) -> dict:

    x1 = x1.cpu().numpy()
    x2 = x2.cpu().numpy()
    char_1 = process_image(x1, x1.shape[0], x1.shape[1], image_size_um)
    char_2 = process_image(x2, x2.shape[1], x2.shape[1], image_size_um)

    diffs = {}
    for k in char_1:
        v1, v2 = char_1[k], char_2[k]
        err = abs(v1 - v2)
        diffs[k] = err

    return diffs


if __name__ == "__main__":

    dummy = np.random.random((128, 128))
    res = process_image(dummy, 128, 128)

    breakpoint()