ContadorLarvas1 / app.py
cesar's picture
Update app.py
3f5e07e verified
# 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)