File size: 6,087 Bytes
3f5e07e
0aabd3f
f3cde7d
8602d84
 
 
0aabd3f
8602d84
3f5e07e
f3cde7d
 
3f5e07e
7b69340
 
f3cde7d
3f5e07e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f3cde7d
3f5e07e
 
 
 
 
 
f3cde7d
3f5e07e
f3cde7d
3f5e07e
 
f3cde7d
3f5e07e
f3cde7d
3f5e07e
 
f3cde7d
 
3f5e07e
 
7b69340
 
3f5e07e
 
0aabd3f
3f5e07e
 
f3cde7d
3f5e07e
7b69340
 
3f5e07e
7b69340
3f5e07e
 
 
 
 
 
 
 
 
 
0aabd3f
8602d84
3f5e07e
 
 
 
 
 
 
 
f3cde7d
3f5e07e
 
 
4f75d88
3f5e07e
 
f3cde7d
0aabd3f
f3cde7d
3f5e07e
f3cde7d
0aabd3f
 
3f5e07e
0aabd3f
3f5e07e
 
 
 
7b69340
f3cde7d
3f5e07e
 
 
0aabd3f
f3cde7d
0aabd3f
f3cde7d
 
3f5e07e
0aabd3f
3f5e07e
 
 
f3cde7d
 
 
3f5e07e
 
 
 
 
8602d84
0aabd3f
 
 
f3cde7d
 
 
3f5e07e
 
f3cde7d
3f5e07e
 
f3cde7d
3f5e07e
 
f3cde7d
3f5e07e
 
 
 
0aabd3f
 
f3cde7d
3f5e07e
0aabd3f
8602d84
 
0aabd3f
 
7b69340
3f5e07e
 
 
 
 
 
 
0aabd3f
3f5e07e
 
0aabd3f
f3cde7d
0aabd3f
8602d84
 
0aabd3f
8602d84
f3cde7d
3f5e07e
8602d84
3f5e07e
0aabd3f
8602d84
0aabd3f
 
7b69340
 
3f5e07e
 
7b69340
0aabd3f
 
 
 
 
 
 
 
3f5e07e
 
 
 
 
0aabd3f
8602d84
 
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
# Contador de larvas – versión alineada a 2047x1148
# Autor: Cesarria & ChatGPT

import gradio as gr
import cv2
import numpy as np
import statistics

# --- CONFIG ---
IMG_W = 2047
IMG_H = 1148
BORDER = 6   # recorte de marco
global_count = 0
median_single_area = None


# =============== helpers ===============
def ellipse_ratio(cnt):
    """Relación eje menor / eje mayor de la elipse (0..1)."""
    if len(cnt) < 5:
        return None
    (_, _), (MA, ma), _ = cv2.fitEllipse(cnt)
    return min(MA, ma) / max(MA, ma)


def contour_solidity(cnt):
    """Solidez = área / área del hull. Sirve para descartar harina 'desflecada'."""
    area = cv2.contourArea(cnt)
    if area <= 0:
        return 0.0
    hull = cv2.convexHull(cnt)
    hull_area = cv2.contourArea(hull)
    if hull_area == 0:
        return 0.0
    return float(area) / float(hull_area)


def preprocess(image_bgr):
    """
    - redimensiona a 2047x1148
    - recorta bordes
    - pasa a gris
    - CLAHE
    - resta de fondo
    - normaliza
    """
    img = cv2.resize(image_bgr, (IMG_W, IMG_H), interpolation=cv2.INTER_LINEAR)

    # recorte
    roi = img[BORDER:IMG_H - BORDER, BORDER:IMG_W - BORDER]

    gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)

    # realzar los puntitos blancos
    clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
    gray = clahe.apply(gray)

    # quitar gradiente
    bg = cv2.GaussianBlur(gray, (25, 25), 0)
    sub = cv2.subtract(gray, bg)

    # normalizar
    sub = cv2.normalize(sub, None, 0, 255, cv2.NORM_MINMAX)

    # pequeño blur para bajar granitos de harina
    sub = cv2.medianBlur(sub, 3)

    return sub, img  # devolvemos también la imagen redimensionada completa


def detect_larvas(image_bgr,
                  thresh_value=10,
                  min_area=6,
                  max_area_single=40,
                  shape_min=0.55,
                  shape_max=0.95,
                  min_solidity=0.7):
    """
    Detecta y cuenta larvas.
    - trabaja SIEMPRE en 2047x1148
    - filtra por área, forma y solidez
    """
    global median_single_area

    gray_proc, base_img = preprocess(image_bgr)  # base_img ya es 2047x1148
    h_proc, w_proc = gray_proc.shape[:1][0], gray_proc.shape[:1][0]

    # umbral
    if thresh_value == 0:
        _, th = cv2.threshold(gray_proc, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    else:
        _, th = cv2.threshold(gray_proc, int(thresh_value), 255, cv2.THRESH_BINARY)

    # limpiar un poco
    kernel = np.ones((3, 3), np.uint8)
    th = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel, iterations=1)

    # contornos en la imagen recortada
    contours, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    good = []
    areas_all = []
    areas_single = []

    for c in contours:
        area = cv2.contourArea(c)
        if area < min_area or area > 5000:
            continue

        # forma (descartar harina despareja)
        ratio = ellipse_ratio(c)
        if ratio is None or not (shape_min <= ratio <= shape_max):
            continue

        # solidez (descarta cosas con bordes poco definidos)
        sol = contour_solidity(c)
        if sol < min_solidity:
            continue

        good.append(c)
        areas_all.append(area)
        if area <= max_area_single:
            areas_single.append(area)

    # estimar área típica
    if areas_single:
        median_single_area = statistics.median_low(areas_single)
    elif areas_all:
        median_single_area = statistics.median_low(areas_all)
    else:
        # no detectamos nada
        out = base_img.copy()
        cv2.putText(out, "LARVAS: 0", (40, 80),
                    cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3)
        return out, 0

    total = 0
    for c in good:
        a = cv2.contourArea(c)
        if a <= max_area_single:
            total += 1
        else:
            est = int(round(a / median_single_area))
            total += max(1, est)

    # dibujar sobre la imagen COMPLETA (ya 2047x1148)
    out = base_img.copy()
    for c in good:
        # como trabajamos sobre la imagen recortada, hay que desplazar
        c_shifted = c + np.array([[BORDER, BORDER]])
        cv2.drawContours(out, [c_shifted], -1, (0, 255, 0), 1)

    cv2.putText(out, f"LARVAS: {total}", (40, 80),
                cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3)

    return out, total


# =============== gradio wrappers ===============
def process(image, thresh, min_a, max_a):
    global global_count
    if image is None:
        return None, "No subiste imagen", f"Conteo total: {global_count}"

    img_bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)

    out_img_bgr, n = detect_larvas(
        img_bgr,
        thresh_value=int(thresh),
        min_area=int(min_a),
        max_area_single=int(max_a)
    )
    global_count += n
    out_img_rgb = cv2.cvtColor(out_img_bgr, cv2.COLOR_BGR2RGB)
    return out_img_rgb, f"Larvas en la imagen: {n}", f"Conteo total: {global_count}"


def reset():
    global global_count
    global_count = 0
    return f"Conteo total: {global_count}"


# =============== interfaz ===============
with gr.Blocks() as demo:
    gr.Markdown("## Contador de larvas – v6 (rangos amplios y alineado)")

    with gr.Row():
        with gr.Column(scale=1):
            inp = gr.Image(label="Subí la foto")

            thresh = gr.Slider(0, 300, 10, 1, label="Umbral (0=Otsu auto)")
            min_area = gr.Slider(0, 300, 6, 1, label="Min área px²")
            max_area_single = gr.Slider(0, 300, 40, 1, label="Máx área 1 larva px²")

            btn = gr.Button("Procesar")
            btn_reset = gr.Button("Reset contador")

        with gr.Column(scale=1):
            out_img = gr.Image(label="Resultado")
            out_txt = gr.Textbox(label="Resultado individual")
            out_total = gr.Textbox(label="Resultado acumulado")

    btn.click(
        process,
        inputs=[inp, thresh, min_area, max_area_single],
        outputs=[out_img, out_txt, out_total]
    )
    btn_reset.click(reset, [], [out_total])

demo.launch(debug=True)