Spaces:
Sleeping
Sleeping
| # 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) | |