File size: 10,913 Bytes
acf4902
 
 
 
 
9d91ec3
acf4902
f7aa858
 
acf4902
 
 
 
f7aa858
acf4902
f7aa858
 
 
acf4902
 
 
 
 
 
 
 
 
 
f7aa858
acf4902
f7aa858
acf4902
f7aa858
acf4902
f7aa858
acf4902
 
b91e4f3
 
 
f7aa858
9d91ec3
b90770d
 
9d91ec3
 
b90770d
b4b440a
b90770d
b4b440a
 
 
 
 
 
 
 
 
acf4902
f7aa858
5969781
 
acf4902
f7aa858
 
 
 
acf4902
f7aa858
 
 
 
acf4902
f7aa858
 
 
 
acf4902
f7aa858
 
 
acf4902
f7aa858
acf4902
 
 
f7aa858
 
acf4902
9653c99
 
f7aa858
acf4902
 
f7aa858
acf4902
 
 
 
 
 
 
 
 
9653c99
 
 
acf4902
 
f7aa858
 
 
 
 
 
 
acf4902
 
 
f7aa858
 
 
 
acf4902
9653c99
 
f7aa858
 
 
 
acf4902
 
 
f7aa858
 
 
 
 
 
 
 
acf4902
f7aa858
 
 
 
 
 
 
acf4902
f7aa858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acf4902
f7aa858
 
 
 
 
 
 
acf4902
f7aa858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acf4902
f7aa858
 
acf4902
f7aa858
 
 
 
 
 
 
 
 
 
 
 
acf4902
 
 
 
9653c99
f7aa858
 
 
 
 
 
 
 
9653c99
f7aa858
 
 
 
9653c99
f7aa858
 
 
 
 
9653c99
f7aa858
 
 
9653c99
f7aa858
 
 
acf4902
f7aa858
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
256
257
258
259
260
261
262
263
import numpy as np
import cv2
from PIL import Image
import random

def generate_mask(image, thresh_val=None, min_size=9, crop_top=0, crop_bottom=0, crop_left=0, crop_right=0, max_size=52, typical_unit_size=40):
    """
    Generate a mask from the input image using optimized OpenCV functions.
    Replaces skimage dependency for better performance in Cloud Run.
    """
    if image is None:
        return None, None, None
    
    # 1. Conversión eficiente a NumPy
    if isinstance(image, Image.Image):
        if image.mode != 'L':
            image = image.convert('L')
        img = np.asarray(image)
    elif isinstance(image, np.ndarray):
        if len(image.shape) == 3:
            img = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
        else:
            img = image
    else:
        return None, None, None
    
    h, w = img.shape
    
    # 2. Recorte (Slicing)
    y1 = max(0, crop_top)
    y2 = min(h, h - crop_bottom)
    x1 = max(0, crop_left)
    x2 = min(w, w - crop_right)
    
    if y2 <= y1 or x2 <= x1:
        return None, None, None
    
    # Usamos slicing directo. OpenCV maneja la memoria eficientemente.
    img_crop = img[y1:y2, x1:x2]
    
    # 3. Binarización
    # Suavizar para mejorar el cálculo del umbral, sin perder la imagen original
    img_blur = cv2.GaussianBlur(img_crop, (5, 5), 0)

    if thresh_val is None or thresh_val == "" or str(thresh_val).lower() in ["none", "auto"]:
        # img_blur para Otsu
        otsu_thresh, bw = cv2.threshold(img_blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        print(f"Umbral calculado automáticamente (Otsu): {otsu_thresh}")
    else:
        try:
            thresh_int = int(thresh_val)
        except ValueError:
            print(f"Advertencia: '{thresh_val}' no es un número válido. Usando Otsu por defecto.")
            otsu_thresh, bw = cv2.threshold(img_blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
            thresh_int = otsu_thresh # Solo para el print
        else:
            _, bw = cv2.threshold(img_crop, thresh_int, 255, cv2.THRESH_BINARY)
            print(f"Umbral manual aplicado: {thresh_int}")
    
    # Fallback automático si no hay blancos (check rápido con np.any)
    # if not np.any(bw):
    #     _, bw = cv2.threshold(img_crop, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    # 4. Etiquetado optimizado (Connected Components)
    # Reemplaza a skimage.measure.label y regionprops
    # stats es una matriz donde cada fila es [x, y, w, h, area]
    num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(bw, connectivity=8)
    
    # 5. Generación de Máscara Vectorizada (Sin bucles lentos)
    # stats[:, 4] es la columna de áreas. El índice 0 es el fondo.
    valid_indices = np.where(stats[:, 4] >= min_size)[0]
    valid_indices = valid_indices[valid_indices != 0] # Quitar fondo
    
    # Crear Look-Up Table (LUT) para generar la máscara final rápido
    lut = np.zeros(num_labels, dtype=np.uint8)
    lut[valid_indices] = 255
    mask = lut[labels] # Aplicación vectorial de la LUT
    
    # Crear versión color para visualización
    mask_color = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
    mask_color[mask > 0] = [0, 255, 0] # Verde (formato BGR para OpenCV interno, luego PIL lo interpreta)
    
    # Empaquetamos datos (compatible con la estructura anterior pero con extras de OpenCV)
    processed_data = {
        "mask": mask,
        "labels": labels,
        "stats": stats,          # Nuevo: Stats de OpenCV (x,y,w,h,area)
        "centroids": centroids,  # Nuevo: Centroides de OpenCV
        "min_size": min_size,
        "max_size": max_size,
        "typical_unit_size": typical_unit_size,
        "img_crop": img_crop
    }
    
    # Convertir a PIL para el return final
    mask_img = Image.fromarray(mask_color)
    img_crop_pil = Image.fromarray(img_crop)
    
    return mask_img, processed_data, img_crop_pil


def visualize_structures(
    input_data, 
    min_size=1, 
    visualization_mode="contours",
    max_size=1000,
    typical_unit_size=40
):
    """
    Analyze and visualize structures using optimized OpenCV calls.
    """
    # 1. Extracción de datos
    stats = None
    centroids = None
    
    if isinstance(input_data, dict):
        if input_data is None:
            return None, "Debes procesar una imagen primero"
        mask = input_data["mask"]
        img_crop = input_data.get("img_crop", mask.copy())
        # Intentamos recuperar stats ya calculados para ahorrar tiempo
        stats = input_data.get("stats")
        centroids = input_data.get("centroids")
        min_size = input_data.get("min_size", min_size)
        max_size = input_data.get("max_size", max_size)
        typical_unit_size = input_data.get("typical_unit_size", typical_unit_size)
    elif isinstance(input_data, Image.Image):
        mask = np.array(input_data.convert("L"))
        mask = (mask > 0).astype(np.uint8) * 255
        img_crop = mask
    else:
        return None, "Input data type not supported"
    
    # Imagen base a color
    if len(img_crop.shape) == 2:
        img_color = cv2.cvtColor(img_crop, cv2.COLOR_GRAY2RGB)
    else:
        img_color = img_crop.copy()

    # 2. Si no tenemos stats (vino de una imagen directa), calcularlos
    if stats is None:
        num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8)

    # 3. Filtrado vectorial de estructuras válidas
    # stats[:, 4] es el área.
    areas = stats[:, 4]
    valid_mask = (areas >= min_size)
    valid_mask[0] = False # Ignorar el fondo (label 0)
    valid_indices = np.where(valid_mask)[0]
    
    count = len(valid_indices)
    
    if count == 0:
        return Image.fromarray(img_color), "No se detectaron estructuras."

    total_units = 0
    structure_counter = 0

    # Lógica específica por modo de visualización
    
    # -- MODO 1: Bounding Boxes (Muy rápido, usa stats directos) --
    if visualization_mode == "bounding_boxes":
        for idx in valid_indices:
            structure_counter += 1
            x, y, w, h = stats[idx, 0:4]
            area = areas[idx]
            
            # Calcular unidades
            units = 1 if area <= max_size else max(1, round(area / typical_unit_size))
            total_units += units
            
            label_text = str(structure_counter) if units == 1 else f"{structure_counter}({units}u)"
            
            cv2.rectangle(img_color, (x, y), (x+w, y+h), (0, 255, 0), 2)
            cv2.putText(img_color, label_text, (x, y-5), 
                       cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)

    # -- MODO 2: Connected Components (Coloreado por labels) --
    elif visualization_mode == "connected_components":
        # Generar colores aleatorios para la LUT
        # Necesitamos colores para todos los labels (incluso los inválidos, para simplificar indexing)
        colors = np.random.randint(0, 255, size=(len(stats), 3), dtype=np.uint8)
        colors[0] = [0, 0, 0] # Fondo negro
        
        # Crear imagen de etiquetas coloreada
        # Recuperamos labels si no los tenemos (raro, pero posible si input fue Image)
        if 'labels' not in locals():
             _, labels, _, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
             
        # Pintar
        colored_labels = colors[labels]
        
        # Iterar solo para poner texto y contar
        for idx in valid_indices:
            structure_counter += 1
            area = areas[idx]
            cx, cy = int(centroids[idx][0]), int(centroids[idx][1])
            
            units = 1 if area <= max_size else max(1, round(area / typical_unit_size))
            total_units += units
            
            label_text = str(structure_counter) if units == 1 else f"{structure_counter}({units}u)"
            cv2.putText(colored_labels, label_text, (cx-10, cy), 
                       cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)
            
        # Mezclar con original
        img_color = cv2.addWeighted(img_color, 0.3, colored_labels, 0.7, 0)

    # -- MODO 3: Contornos / Relleno / Elipses / Círculos --
    else:
        # Encontrar contornos es rápido en la máscara binaria ya limpia
        # Usamos la máscara en lugar de 'labels' porque findContours espera imagen de 8-bits
        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        # Filtramos contornos manualmente por si acaso (aunque la máscara ya debería estar limpia)
        valid_contours = [c for c in contours if cv2.contourArea(c) >= min_size]
        
        # Recalculamos count basado en contornos encontrados
        count = len(valid_contours)
        structure_counter = 0
        
        for i, cnt in enumerate(valid_contours):
            structure_counter += 1
            area = cv2.contourArea(cnt)
            
            units = 1 if area <= max_size else max(1, round(area / typical_unit_size))
            total_units += units
            
            # Centroide para texto
            M = cv2.moments(cnt)
            if M["m00"] != 0:
                cx = int(M["m10"] / M["m00"])
                cy = int(M["m01"] / M["m00"])
            else:
                x,y,w,h = cv2.boundingRect(cnt)
                cx, cy = x + w//2, y + h//2

            label_text = f"{structure_counter}" if units == 1 else f"{structure_counter}({units}u)"

            if visualization_mode == "contours":
                cv2.drawContours(img_color, [cnt], -1, (0, 255, 0), 2)
                cv2.putText(img_color, label_text, (cx-10, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)
                
            elif visualization_mode == "filled":
                color = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))
                cv2.drawContours(img_color, [cnt], -1, color, -1)
                cv2.putText(img_color, label_text, (cx-10, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)
                
            elif visualization_mode == "ellipses":
                if len(cnt) >= 5:
                    ellipse = cv2.fitEllipse(cnt)
                    cv2.ellipse(img_color, ellipse, (0, 255, 0), 2)
                    cv2.putText(img_color, label_text, (cx-10, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)
                else:
                    # Fallback a rectangulo si no alcanza para elipse
                    x,y,w,h = cv2.boundingRect(cnt)
                    cv2.rectangle(img_color, (x, y), (x+w, y+h), (0, 255, 0), 2)
                    
            else: # Default: Circles / Points
                cv2.circle(img_color, (cx, cy), 8, (0, 0, 255), 2)
                cv2.putText(img_color, label_text, (cx-10, cy-15), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)

    return Image.fromarray(img_color), f"Estructuras: {count} | Unidades estimadas: {total_units}"