# 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)