cesar commited on
Commit
3f5e07e
·
verified ·
1 Parent(s): 4f75d88

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -94
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # Contador de larvas – versión sensible (v5)
2
  # Autor: Cesarria & ChatGPT
3
 
4
  import gradio as gr
@@ -6,119 +6,133 @@ import cv2
6
  import numpy as np
7
  import statistics
8
 
9
- # --- CONFIG GLOBAL ---
10
  IMG_W = 2047
11
  IMG_H = 1148
12
- BORDER_CROP = 6
13
  global_count = 0
14
  median_single_area = None
15
 
16
- # --- PREPROCESAMIENTO ---
17
- def preprocess_gray_from_bgr(img_bgr):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  """
19
- Prepara la imagen:
20
- - Asegura tamaño 2047x1148
21
- - Recorta bordes
22
- - Pasa a gris, aplica CLAHE fuerte
23
- - Quita gradiente leve (blur 15)
24
- - Filtro mediano final
25
  """
26
- h, w = img_bgr.shape[:2]
27
- if (w, h) != (IMG_W, IMG_H):
28
- img_bgr = cv2.resize(img_bgr, (IMG_W, IMG_H), interpolation=cv2.INTER_LINEAR)
29
- h, w = IMG_H, IMG_W
30
 
31
- # Recorte de marco
32
- y1, y2 = BORDER_CROP, h - BORDER_CROP
33
- x1, x2 = BORDER_CROP, w - BORDER_CROP
34
- img_crop = img_bgr[y1:y2, x1:x2]
35
 
36
- gray = cv2.cvtColor(img_crop, cv2.COLOR_BGR2GRAY)
37
 
38
- # Aumentar contraste local
39
- clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
40
  gray = clahe.apply(gray)
41
 
42
- # Quitar gradiente (blur leve)
43
- bg = cv2.GaussianBlur(gray, (15, 15), 0)
44
  sub = cv2.subtract(gray, bg)
45
 
46
- # Filtro mediano para reducir harina
47
- sub = cv2.medianBlur(sub, 3)
48
- return sub, (x1, y1)
49
-
50
-
51
- def binarize(img, thresh_value):
52
- """Threshold fijo o Otsu (si thresh_value=0)."""
53
- if thresh_value == 0:
54
- _, th = cv2.threshold(img, 0, 255,
55
- cv2.THRESH_BINARY + cv2.THRESH_OTSU)
56
- else:
57
- _, th = cv2.threshold(img, thresh_value, 255, cv2.THRESH_BINARY)
58
- return th
59
 
 
 
60
 
61
- def clean_mask(mask):
62
- """Elimina ruido y une fragmentos."""
63
- kernel = np.ones((3, 3), np.uint8)
64
- cleaned = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
65
- cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel, iterations=1)
66
- return cleaned
67
 
68
 
69
- # --- DETECCIÓN ---
70
- def detect_larvas(image,
71
  thresh_value=10,
72
- min_area=4,
73
- max_area_single=38,
74
- shape_min=0.3,
75
- shape_max=1.0):
76
- """Cuenta larvas usando preprocesamiento mejorado."""
 
 
 
 
 
77
  global median_single_area
78
 
79
- gray_proc, (x_off, y_off) = preprocess_gray_from_bgr(image)
80
- th = binarize(gray_proc, int(thresh_value))
81
- mask = clean_mask(th)
 
 
 
 
 
82
 
83
- contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
 
84
 
85
- if len(contours) == 0 and thresh_value > 0:
86
- # si no detectó nada, baja umbral automáticamente
87
- _, th2 = cv2.threshold(gray_proc, int(thresh_value * 0.7), 255, cv2.THRESH_BINARY)
88
- mask = clean_mask(th2)
89
- contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
90
 
91
  good = []
92
  areas_all = []
93
- areas_small = []
94
 
95
  for c in contours:
96
  area = cv2.contourArea(c)
97
- if area < min_area:
98
  continue
99
- if area > 5000:
 
 
 
100
  continue
101
 
102
- # Chequeo de forma
103
- if len(c) >= 5:
104
- (_, _), (MA, ma), _ = cv2.fitEllipse(c)
105
- ratio = min(MA, ma) / max(MA, ma)
106
- else:
107
- ratio = 1
108
- if not (shape_min <= ratio <= shape_max):
109
  continue
110
 
111
  good.append(c)
112
  areas_all.append(area)
113
  if area <= max_area_single:
114
- areas_small.append(area)
115
 
116
- if areas_small:
117
- median_single_area = statistics.median_low(areas_small)
 
118
  elif areas_all:
119
  median_single_area = statistics.median_low(areas_all)
120
  else:
121
- return image, 0
 
 
 
 
122
 
123
  total = 0
124
  for c in good:
@@ -126,33 +140,39 @@ def detect_larvas(image,
126
  if a <= max_area_single:
127
  total += 1
128
  else:
129
- estimated = int(round(a / median_single_area))
130
- total += max(1, estimated)
131
 
132
- out = image.copy()
 
133
  for c in good:
134
- c_shifted = c + np.array([[x_off, y_off]])
 
135
  cv2.drawContours(out, [c_shifted], -1, (0, 255, 0), 1)
136
- cv2.putText(out, f"LARVAS: {total}",
137
- (40, 80), cv2.FONT_HERSHEY_SIMPLEX, 2,
138
- (0, 0, 255), 3)
 
139
  return out, total
140
 
141
 
142
- # --- GRADIO ---
143
  def process(image, thresh, min_a, max_a):
144
  global global_count
145
  if image is None:
146
  return None, "No subiste imagen", f"Conteo total: {global_count}"
147
 
148
  img_bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
149
- proc, n = detect_larvas(img_bgr,
150
- thresh_value=int(thresh),
151
- min_area=int(min_a),
152
- max_area_single=int(max_a))
 
 
 
153
  global_count += n
154
- img_rgb = cv2.cvtColor(proc, cv2.COLOR_BGR2RGB)
155
- return img_rgb, f"Larvas en la imagen: {n}", f"Conteo total: {global_count}"
156
 
157
 
158
  def reset():
@@ -161,17 +181,17 @@ def reset():
161
  return f"Conteo total: {global_count}"
162
 
163
 
164
- # --- INTERFAZ ---
165
  with gr.Blocks() as demo:
166
- gr.Markdown("## 🧫 Contador de larvas – v5 (ajustado y sensible)")
167
 
168
  with gr.Row():
169
  with gr.Column(scale=1):
170
  inp = gr.Image(label="Subí la foto")
171
 
172
  thresh = gr.Slider(0, 300, 10, 1, label="Umbral (0=Otsu auto)")
173
- min_area = gr.Slider(0, 300, 4, 1, label="Min área px²")
174
- max_area_single = gr.Slider(0, 300, 38, 1, label="Máx área 1 larva px²")
175
 
176
  btn = gr.Button("Procesar")
177
  btn_reset = gr.Button("Reset contador")
@@ -181,9 +201,11 @@ with gr.Blocks() as demo:
181
  out_txt = gr.Textbox(label="Resultado individual")
182
  out_total = gr.Textbox(label="Resultado acumulado")
183
 
184
- btn.click(process,
185
- inputs=[inp, thresh, min_area, max_area_single],
186
- outputs=[out_img, out_txt, out_total])
 
 
187
  btn_reset.click(reset, [], [out_total])
188
 
189
  demo.launch(debug=True)
 
1
+ # Contador de larvas – versión alineada a 2047x1148
2
  # Autor: Cesarria & ChatGPT
3
 
4
  import gradio as gr
 
6
  import numpy as np
7
  import statistics
8
 
9
+ # --- CONFIG ---
10
  IMG_W = 2047
11
  IMG_H = 1148
12
+ BORDER = 6 # recorte de marco
13
  global_count = 0
14
  median_single_area = None
15
 
16
+
17
+ # =============== helpers ===============
18
+ def ellipse_ratio(cnt):
19
+ """Relación eje menor / eje mayor de la elipse (0..1)."""
20
+ if len(cnt) < 5:
21
+ return None
22
+ (_, _), (MA, ma), _ = cv2.fitEllipse(cnt)
23
+ return min(MA, ma) / max(MA, ma)
24
+
25
+
26
+ def contour_solidity(cnt):
27
+ """Solidez = área / área del hull. Sirve para descartar harina 'desflecada'."""
28
+ area = cv2.contourArea(cnt)
29
+ if area <= 0:
30
+ return 0.0
31
+ hull = cv2.convexHull(cnt)
32
+ hull_area = cv2.contourArea(hull)
33
+ if hull_area == 0:
34
+ return 0.0
35
+ return float(area) / float(hull_area)
36
+
37
+
38
+ def preprocess(image_bgr):
39
  """
40
+ - redimensiona a 2047x1148
41
+ - recorta bordes
42
+ - pasa a gris
43
+ - CLAHE
44
+ - resta de fondo
45
+ - normaliza
46
  """
47
+ img = cv2.resize(image_bgr, (IMG_W, IMG_H), interpolation=cv2.INTER_LINEAR)
 
 
 
48
 
49
+ # recorte
50
+ roi = img[BORDER:IMG_H - BORDER, BORDER:IMG_W - BORDER]
 
 
51
 
52
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
53
 
54
+ # realzar los puntitos blancos
55
+ clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
56
  gray = clahe.apply(gray)
57
 
58
+ # quitar gradiente
59
+ bg = cv2.GaussianBlur(gray, (25, 25), 0)
60
  sub = cv2.subtract(gray, bg)
61
 
62
+ # normalizar
63
+ sub = cv2.normalize(sub, None, 0, 255, cv2.NORM_MINMAX)
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ # pequeño blur para bajar granitos de harina
66
+ sub = cv2.medianBlur(sub, 3)
67
 
68
+ return sub, img # devolvemos también la imagen redimensionada completa
 
 
 
 
 
69
 
70
 
71
+ def detect_larvas(image_bgr,
 
72
  thresh_value=10,
73
+ min_area=6,
74
+ max_area_single=40,
75
+ shape_min=0.55,
76
+ shape_max=0.95,
77
+ min_solidity=0.7):
78
+ """
79
+ Detecta y cuenta larvas.
80
+ - trabaja SIEMPRE en 2047x1148
81
+ - filtra por área, forma y solidez
82
+ """
83
  global median_single_area
84
 
85
+ gray_proc, base_img = preprocess(image_bgr) # base_img ya es 2047x1148
86
+ h_proc, w_proc = gray_proc.shape[:1][0], gray_proc.shape[:1][0]
87
+
88
+ # umbral
89
+ if thresh_value == 0:
90
+ _, th = cv2.threshold(gray_proc, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
91
+ else:
92
+ _, th = cv2.threshold(gray_proc, int(thresh_value), 255, cv2.THRESH_BINARY)
93
 
94
+ # limpiar un poco
95
+ kernel = np.ones((3, 3), np.uint8)
96
+ th = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel, iterations=1)
97
 
98
+ # contornos en la imagen recortada
99
+ contours, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
 
 
100
 
101
  good = []
102
  areas_all = []
103
+ areas_single = []
104
 
105
  for c in contours:
106
  area = cv2.contourArea(c)
107
+ if area < min_area or area > 5000:
108
  continue
109
+
110
+ # forma (descartar harina despareja)
111
+ ratio = ellipse_ratio(c)
112
+ if ratio is None or not (shape_min <= ratio <= shape_max):
113
  continue
114
 
115
+ # solidez (descarta cosas con bordes poco definidos)
116
+ sol = contour_solidity(c)
117
+ if sol < min_solidity:
 
 
 
 
118
  continue
119
 
120
  good.append(c)
121
  areas_all.append(area)
122
  if area <= max_area_single:
123
+ areas_single.append(area)
124
 
125
+ # estimar área típica
126
+ if areas_single:
127
+ median_single_area = statistics.median_low(areas_single)
128
  elif areas_all:
129
  median_single_area = statistics.median_low(areas_all)
130
  else:
131
+ # no detectamos nada
132
+ out = base_img.copy()
133
+ cv2.putText(out, "LARVAS: 0", (40, 80),
134
+ cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3)
135
+ return out, 0
136
 
137
  total = 0
138
  for c in good:
 
140
  if a <= max_area_single:
141
  total += 1
142
  else:
143
+ est = int(round(a / median_single_area))
144
+ total += max(1, est)
145
 
146
+ # dibujar sobre la imagen COMPLETA (ya 2047x1148)
147
+ out = base_img.copy()
148
  for c in good:
149
+ # como trabajamos sobre la imagen recortada, hay que desplazar
150
+ c_shifted = c + np.array([[BORDER, BORDER]])
151
  cv2.drawContours(out, [c_shifted], -1, (0, 255, 0), 1)
152
+
153
+ cv2.putText(out, f"LARVAS: {total}", (40, 80),
154
+ cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3)
155
+
156
  return out, total
157
 
158
 
159
+ # =============== gradio wrappers ===============
160
  def process(image, thresh, min_a, max_a):
161
  global global_count
162
  if image is None:
163
  return None, "No subiste imagen", f"Conteo total: {global_count}"
164
 
165
  img_bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
166
+
167
+ out_img_bgr, n = detect_larvas(
168
+ img_bgr,
169
+ thresh_value=int(thresh),
170
+ min_area=int(min_a),
171
+ max_area_single=int(max_a)
172
+ )
173
  global_count += n
174
+ out_img_rgb = cv2.cvtColor(out_img_bgr, cv2.COLOR_BGR2RGB)
175
+ return out_img_rgb, f"Larvas en la imagen: {n}", f"Conteo total: {global_count}"
176
 
177
 
178
  def reset():
 
181
  return f"Conteo total: {global_count}"
182
 
183
 
184
+ # =============== interfaz ===============
185
  with gr.Blocks() as demo:
186
+ gr.Markdown("## Contador de larvas – v6 (rangos amplios y alineado)")
187
 
188
  with gr.Row():
189
  with gr.Column(scale=1):
190
  inp = gr.Image(label="Subí la foto")
191
 
192
  thresh = gr.Slider(0, 300, 10, 1, label="Umbral (0=Otsu auto)")
193
+ min_area = gr.Slider(0, 300, 6, 1, label="Min área px²")
194
+ max_area_single = gr.Slider(0, 300, 40, 1, label="Máx área 1 larva px²")
195
 
196
  btn = gr.Button("Procesar")
197
  btn_reset = gr.Button("Reset contador")
 
201
  out_txt = gr.Textbox(label="Resultado individual")
202
  out_total = gr.Textbox(label="Resultado acumulado")
203
 
204
+ btn.click(
205
+ process,
206
+ inputs=[inp, thresh, min_area, max_area_single],
207
+ outputs=[out_img, out_txt, out_total]
208
+ )
209
  btn_reset.click(reset, [], [out_total])
210
 
211
  demo.launch(debug=True)