Spaces:
Sleeping
Sleeping
Download test.py from pjsr/wings_length: direct link, hf CLI and curl.
- Browser
- Download file 23.1 kB
-
https://huggingface.co/spaces/pjsr/wings_length/resolve/main/test.py
- Command line
-
hf download hf://spaces/pjsr/wings_length/test.py
-
curl -L -o test.py https://huggingface.co/spaces/pjsr/wings_length/resolve/main/test.py
23.1 kB
| import gradio as gr | |
| from ultralytics import YOLO | |
| import torch | |
| import cv2 | |
| import numpy as np | |
| import os | |
| import re | |
| import math | |
| import pandas as pd | |
| from datetime import datetime | |
| # =========================================================================== | |
| # ESCALA IMPRESSA + RECORTE DA ASA (antes em asa_lib.py, agora integrado) | |
| # =========================================================================== | |
| try: | |
| import pytesseract | |
| OCR_DISPONIVEL = True | |
| except Exception: | |
| OCR_DISPONIVEL = False | |
| # =========================================================================== | |
| # 1) ESCALA | |
| # =========================================================================== | |
| _UNIT_MM = {"mm": 1.0, "cm": 10.0, "um": 1e-3, "µm": 1e-3, "μm": 1e-3, "nm": 1e-6} | |
| _WL = "0123456789.,mµumcn " | |
| # largura plausível do campo de visão (mm) — usada para desempatar leituras | |
| # ambíguas do OCR, ex. "1 mm" lido como "1 um" (m e u confundem-se). | |
| _LARGURA_MM_MIN, _LARGURA_MM_MAX = 0.5, 2000.0 | |
| def _valor_mm(texto): | |
| """(valor_mm, tem_decimal, match) ou (None, False, None).""" | |
| if not texto: | |
| return None, False, None | |
| t = texto.lower().replace(",", ".").replace("μ", "µ") | |
| m = (re.search(r"(\d+(?:\.\d+)?)\s*(mm|cm|µm|um|nm)\b", t) | |
| or re.search(r"(\d+(?:\.\d+)?)\s*(mm|cm|µm|um|nm)", t)) | |
| if not m: | |
| return None, False, None | |
| num = m.group(1) | |
| return float(num) * _UNIT_MM.get(m.group(2), 1.0), ("." in num), m.group(0) | |
| def _ampliar(gray, alvo_h=170): | |
| h = max(1, gray.shape[0]) | |
| f = min(10.0, max(2.0, alvo_h / h)) | |
| return cv2.resize(gray, None, fx=f, fy=f, interpolation=cv2.INTER_CUBIC) | |
| def _ocr(im, psm): | |
| """(texto, confiança_média) usando image_to_data.""" | |
| cfg = f"--psm {psm} -c tessedit_char_whitelist={_WL}" | |
| d = pytesseract.image_to_data(im, config=cfg, output_type=pytesseract.Output.DICT) | |
| palavras, confs = [], [] | |
| for i, t in enumerate(d["text"]): | |
| t = t.strip() | |
| if not t: | |
| continue | |
| palavras.append(t) | |
| try: | |
| c = float(d["conf"][i]) | |
| except (TypeError, ValueError): | |
| c = -1.0 | |
| if c >= 0: | |
| confs.append(c) | |
| return " ".join(palavras), (float(np.mean(confs)) if confs else 1.0) | |
| def _candidatos_valor(crop_bgr, verbose=None): | |
| """Recolhe leituras do rótulo -> {valor: [peso, tem_decimal]}.""" | |
| if not OCR_DISPONIVEL or crop_bgr is None or crop_bgr.size == 0: | |
| return {} | |
| gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY) | |
| big = _ampliar(gray) | |
| _, otsu = cv2.threshold(big, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) | |
| nit = cv2.morphologyEx(otsu, cv2.MORPH_CLOSE, np.ones((2, 2), np.uint8)) | |
| cands = {} | |
| for im in (otsu, big, nit): | |
| for psm in (7, 6, 8, 11, 13): | |
| try: | |
| raw, conf = _ocr(im, psm) | |
| except Exception: | |
| continue | |
| v, dec, _ = _valor_mm(raw) | |
| if verbose is not None and raw: | |
| verbose.append(f" OCR '{raw}' (conf {conf:.0f}) -> {v}") | |
| if v is None: | |
| continue | |
| e = cands.setdefault(v, [0.0, False]) | |
| e[0] += max(conf, 1.0) | |
| e[1] = e[1] or dec | |
| return cands | |
| def _escolher_valor(cands, barra_px, largura_img): | |
| """Escolhe o valor mais credível: prefere leituras fisicamente plausíveis, | |
| depois com decimal, depois com maior peso (confiança acumulada).""" | |
| if not cands: | |
| return None, None | |
| def plausivel(v): | |
| if not barra_px or not largura_img: | |
| return True | |
| larg_mm = largura_img * (v / barra_px) | |
| return _LARGURA_MM_MIN <= larg_mm <= _LARGURA_MM_MAX | |
| itens = [(v, p, d, plausivel(v)) for v, (p, d) in cands.items()] | |
| ok = [i for i in itens if i[3]] or itens | |
| ok.sort(key=lambda i: (i[2], i[1]), reverse=True) # decimal, depois peso | |
| escolhido = ok[0][0] | |
| nota = "plausível" if ok[0][3] else "sem candidato plausível" | |
| if len(itens) > 1: | |
| nota += f" (de {len(itens)} leituras: {sorted(v for v, _, _, _ in itens)})" | |
| return escolhido, nota | |
| def _candidatos_barra(img_bgr): | |
| h, w = img_bgr.shape[:2] | |
| gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) | |
| _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| num, _, stats, _ = cv2.connectedComponentsWithStats(bw, connectivity=8) | |
| cands = [] | |
| for i in range(1, num): | |
| x, y, ww, hh, area = stats[i] | |
| if hh == 0 or ww < 25 or ww > 0.6 * w or (ww / hh) < 5: | |
| continue | |
| m = 8 | |
| viz = gray[max(0, y - hh - m):min(h, y + 2 * hh + m), | |
| max(0, x - m):min(w, x + ww + m)] | |
| if viz.size == 0 or np.mean(viz) < 180: | |
| continue | |
| cands.append((int(x), int(y), int(ww), int(hh))) | |
| cands.sort(key=lambda b: (b[0] + b[2]) + b[1], reverse=True) | |
| return cands | |
| def _rotulo_da_barra(img_bgr, bar, verbose=None): | |
| x, y, ww, hh = bar | |
| H, W = img_bgr.shape[:2] | |
| banda = int(max(45, 0.6 * ww)) | |
| px = int(0.3 * ww) | |
| x1, x2 = max(0, x - px), min(W, x + ww + px) | |
| todos = {} | |
| for onde, yy1, yy2 in (("acima", max(0, y - banda), y), | |
| ("abaixo", y + hh, min(H, y + hh + banda))): | |
| if yy2 - yy1 < 6: | |
| continue | |
| if verbose is not None: | |
| verbose.append(f" rótulo {onde} da barra @({x},{y},{ww},{hh}):") | |
| c = _candidatos_valor(img_bgr[yy1:yy2, x1:x2], verbose) | |
| for v, (p, d) in c.items(): | |
| e = todos.setdefault(v, [0.0, False]) | |
| e[0] += p | |
| e[1] = e[1] or d | |
| if todos: | |
| break | |
| return todos | |
| def _bar_bbox_em_roi(img_bgr, x1, y1, x2, y2): | |
| roi = img_bgr[y1:y2, x1:x2] | |
| if roi.size == 0: | |
| return None | |
| gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) | |
| _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| num, _, stats, _ = cv2.connectedComponentsWithStats(bw, connectivity=8) | |
| best = (0, None) | |
| for i in range(1, num): | |
| bx, by, bwd, bht, _ = stats[i] | |
| if bht and (bwd / bht) >= 4 and bwd >= 0.15 * roi.shape[1] and bwd > best[0]: | |
| best = (bwd, (x1 + bx, y1 + by, bwd, bht)) | |
| return best[1] | |
| def _medir_barra_precisa(img_bgr, bbox, pad=3): | |
| """Distância CENTRO-A-CENTRO das serifas (remove o viés da espessura das | |
| marcas); se não houver serifas nítidas, largura da bounding box. | |
| -> (comprimento_px, metodo, largura_bbox_px, centros)""" | |
| x, y, w, h = bbox | |
| H, W = img_bgr.shape[:2] | |
| x1, y1 = max(0, x - pad), max(0, y - pad) | |
| x2, y2 = min(W, x + w + pad), min(H, y + h + pad) | |
| gray = cv2.cvtColor(img_bgr[y1:y2, x1:x2], cv2.COLOR_BGR2GRAY) | |
| _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| num, lab, stats, _ = cv2.connectedComponentsWithStats(bw, connectivity=8) | |
| if num <= 1: | |
| return float(w), "bounding-box", float(w), None | |
| idx = 1 + int(np.argmax(stats[1:, cv2.CC_STAT_AREA])) | |
| mask = (lab == idx).astype(np.uint8) | |
| cols = np.where(mask.any(axis=0))[0] | |
| if cols.size == 0: | |
| return float(w), "bounding-box", float(w), None | |
| largura_bbox = float(cols.max() - cols.min() + 1) | |
| alt = mask.sum(axis=0)[cols] | |
| linha_th, max_h = float(np.median(alt)), float(alt.max()) | |
| if max_h < 2 * max(linha_th, 1) or (max_h - linha_th) < 2: | |
| return largura_bbox, "bounding-box", largura_bbox, None | |
| altas = cols[alt >= linha_th + 0.5 * (max_h - linha_th)] | |
| if altas.size < 2: | |
| return largura_bbox, "bounding-box", largura_bbox, None | |
| g = int(np.argmax(np.diff(altas))) | |
| esq, dire = altas[:g + 1], altas[g + 1:] | |
| if esq.size == 0 or dire.size == 0 or (dire.min() - esq.max()) < 0.3 * largura_bbox: | |
| return largura_bbox, "bounding-box", largura_bbox, None | |
| cxe, cxd = float(esq.mean()), float(dire.mean()) | |
| return (cxd - cxe), "centro-a-centro", largura_bbox, \ | |
| (int(x1 + cxe), int(x1 + cxd), int((y1 + y2) // 2)) | |
| def detectar_escala(img_bgr, debug=False, verbose=None): | |
| """mm por pixel a partir da escala impressa (ou None).""" | |
| H, W = img_bgr.shape[:2] | |
| cands, bar_bbox, origem = {}, None, None | |
| if OCR_DISPONIVEL: | |
| try: | |
| for bar in _candidatos_barra(img_bgr): # 1) barra-primeiro | |
| if verbose is not None: | |
| verbose.append(f" candidato barra @{bar}") | |
| c = _rotulo_da_barra(img_bgr, bar, verbose) | |
| if c: | |
| cands, bar_bbox, origem = c, bar, "barra-primeiro" | |
| break | |
| if not cands: # 2) ROI fundo-direita | |
| roi = img_bgr[int(H * 0.80):, int(W * 0.55):] | |
| c = _candidatos_valor(roi, verbose) | |
| if c: | |
| cands, origem = c, "roi-fundo" | |
| except Exception: | |
| pass | |
| if bar_bbox is None: | |
| bar_bbox = _bar_bbox_em_roi(img_bgr, int(W * 0.55), int(H * 0.80), W, H) | |
| barra_px = metodo = barra_bbox_px = centros = None | |
| if bar_bbox is not None: | |
| barra_px, metodo, barra_bbox_px, centros = _medir_barra_precisa(img_bgr, bar_bbox) | |
| valor_mm, nota = _escolher_valor(cands, barra_px, W) | |
| mm_por_pixel = (valor_mm / barra_px | |
| if (valor_mm and barra_px and barra_px > 0) else None) | |
| if debug: | |
| return mm_por_pixel, {"valor_mm": valor_mm, "nota": nota, "barra_px": barra_px, | |
| "barra_bbox_px": barra_bbox_px, "metodo_barra": metodo, | |
| "mm_por_pixel": mm_por_pixel, "bar_bbox": bar_bbox, | |
| "serif_centros": centros, "origem": origem} | |
| return mm_por_pixel | |
| # =========================================================================== | |
| # 2) RECORTE DA ASA (grande plano) | |
| # =========================================================================== | |
| def _superficie_fundo(sub, fg, passo=4): | |
| """Superfície quadrática ajustada ao fundo (corrige iluminação irregular).""" | |
| H, W = sub.shape | |
| ys, xs = np.nonzero(~fg[::passo, ::passo]) | |
| if ys.size < 200: | |
| return np.full_like(sub, float(np.percentile(sub, 80))) | |
| ys, xs = ys * passo, xs * passo | |
| z = sub[ys, xs].astype(np.float64) | |
| xn, yn = xs / max(W - 1, 1), ys / max(H - 1, 1) | |
| A = np.stack([np.ones_like(xn), xn, yn, xn * xn, yn * yn, xn * yn], axis=1) | |
| coef, *_ = np.linalg.lstsq(A, z, rcond=None) | |
| gy, gx = np.mgrid[0:H, 0:W] | |
| gxn, gyn = gx / max(W - 1, 1), gy / max(H - 1, 1) | |
| return (coef[0] + coef[1] * gxn + coef[2] * gyn + coef[3] * gxn * gxn + | |
| coef[4] * gyn * gyn + coef[5] * gxn * gyn).astype(np.float32) | |
| def _mascara_especime(img_bgr, delta=None): | |
| gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) | |
| sub = cv2.GaussianBlur(gray, (0, 0), 2.0).astype(np.float32) | |
| nivel = float(np.percentile(sub, 80)) | |
| fg = (nivel - sub) > max(3.0, 0.02 * nivel) | |
| for _ in range(2): | |
| fundo = _superficie_fundo(sub, fg) | |
| res = fundo - sub | |
| bg = res[~fg] | |
| if bg.size: | |
| mad = float(np.median(np.abs(bg - np.median(bg)))) | |
| d = delta if delta is not None else max(3.0, 5.0 * mad) | |
| else: | |
| d = delta if delta is not None else 3.0 | |
| fg = res > d | |
| m = fg.astype(np.uint8) | |
| k = max(3, (int(0.004 * max(img_bgr.shape[:2])) | 1)) | |
| m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, np.ones((k, k), np.uint8)) | |
| m = cv2.morphologyEx(m, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) | |
| return m | |
| def recortar_asa(img_bgr, margem=0.06, limpar_fundo=True, dilatar=0.02): | |
| """Recorta a asa em grande plano e uniformiza o fundo à volta. | |
| Normaliza a entrada do modelo: a asa passa a ocupar sempre a mesma fração do | |
| enquadramento e artefactos (pó, 2ª asa, brilho do fundo) desaparecem — o que | |
| evita que a segmentação varie alguns pixels entre fotos da mesma asa. | |
| Devolve (crop_bgr, (x0, y0), info). Em caso de falha devolve a imagem | |
| original e (0, 0), para o pipeline continuar a funcionar. | |
| """ | |
| H, W = img_bgr.shape[:2] | |
| info = {"ok": False, "bbox": None, "crop_box": None, "area_frac": None, "motivo": None} | |
| mask = _mascara_especime(img_bgr) | |
| cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| if not cnts: | |
| info["motivo"] = "sem contorno" | |
| return img_bgr, (0, 0), info | |
| cnt = max(cnts, key=cv2.contourArea) | |
| cheia = np.zeros_like(mask) | |
| cv2.drawContours(cheia, [cnt], -1, 1, thickness=-1) # preenche a asa toda | |
| x, y, w, h = cv2.boundingRect(cnt) | |
| area_frac = float(cheia.sum()) / (H * W) | |
| if w < 0.15 * W or h < 0.10 * H or area_frac < 0.02: | |
| info["motivo"] = f"objeto pequeno demais (w={w}, h={h}, área={area_frac:.3f})" | |
| return img_bgr, (0, 0), info | |
| img = img_bgr | |
| if limpar_fundo: | |
| d = max(3, int(dilatar * max(w, h)) | 1) | |
| sup = cv2.dilate(cheia, np.ones((d, d), np.uint8)) | |
| val = int(round(float(np.percentile(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY), 80)))) | |
| img = img_bgr.copy() | |
| img[sup == 0] = val | |
| m = int(margem * max(w, h)) | |
| x0, y0 = max(0, x - m), max(0, y - m) | |
| x1, y1 = min(W, x + w + m), min(H, y + h + m) | |
| info.update(ok=True, bbox=(int(x), int(y), int(w), int(h)), | |
| crop_box=(int(x0), int(y0), int(x1), int(y1)), area_frac=area_frac) | |
| return img[y0:y1, x0:x1], (x0, y0), info | |
| # Caminho para o modelo treinado | |
| modelo_path = "runs/segment/meu_yolo_seg20/weights/best.pt" | |
| model_cache = {} | |
| def carregar_modelo(device): | |
| if device not in model_cache: | |
| model = YOLO(modelo_path) | |
| model.to(device) | |
| model_cache[device] = model | |
| return model_cache[device] | |
| def contar_pixels_conexos(mask, minimo): | |
| bin_mask = (mask > 0).astype(np.uint8) | |
| num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(bin_mask) | |
| return sum(stat[cv2.CC_STAT_AREA] for stat in stats[1:] if stat[cv2.CC_STAT_AREA] >= minimo) | |
| def calcular_distancia(p1, p2): | |
| return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) | |
| def mascaras_da_predicao(r, cw, ch, metodo="auto"): | |
| """Máscaras binárias nas coordenadas da imagem dada ao modelo (cw x ch). | |
| metodo="auto" -> usa `masks.xy` (polígonos já em coordenadas da imagem de | |
| entrada). Evita o erro do letterbox: `masks.data` vem no | |
| tamanho interno do YOLO, com barras de padding, e | |
| redimensioná-lo desloca as máscaras de forma que depende | |
| do rácio de aspeto (logo, varia de foto para foto). | |
| metodo="legado" -> comportamento original (redimensiona `masks.data`). | |
| """ | |
| if r.masks is None: | |
| return [] | |
| saida = [] | |
| if metodo != "legado": | |
| xy = getattr(r.masks, "xy", None) | |
| if xy is not None and len(xy): | |
| for poly in xy: | |
| if poly is None or len(poly) < 3: | |
| continue | |
| m = np.zeros((ch, cw), np.uint8) | |
| cv2.fillPoly(m, [np.asarray(poly, dtype=np.int32)], 1) | |
| if m.any(): | |
| saida.append(m) | |
| if saida: | |
| return saida | |
| dados = getattr(r.masks, "data", None) | |
| if dados is None: | |
| return [] | |
| for mask in dados: | |
| m = mask.cpu().numpy().astype(np.float32) if hasattr(mask, "cpu") else np.asarray(mask, np.float32) | |
| m = cv2.resize(m, (cw, ch), interpolation=cv2.INTER_LINEAR) | |
| saida.append((m > 0.5).astype(np.uint8)) | |
| return saida | |
| def processar_uma_imagem(imagem_path, dispositivo, conf_min, pixels_minimos, | |
| usar_recorte=True, limpar_fundo=False, metodo_mascara="auto"): | |
| device = "cuda" if dispositivo == "GPU" and torch.cuda.is_available() else "cpu" | |
| model = carregar_modelo(device) | |
| img_bgr = cv2.imread(imagem_path) | |
| if img_bgr is None: | |
| return None, None, None, None, "erro ao ler", "-" | |
| # --- escala: SEMPRE na imagem completa (a barra fica fora do recorte) --- | |
| mm_por_pixel, escala_info = detectar_escala(img_bgr, debug=True) | |
| # --- recorte da asa em grande plano (normaliza a entrada do modelo) --- | |
| if usar_recorte: | |
| entrada, (off_x, off_y), rec_info = recortar_asa(img_bgr, limpar_fundo=limpar_fundo) | |
| else: | |
| entrada, (off_x, off_y), rec_info = img_bgr, (0, 0), {"ok": False, "motivo": "desligado"} | |
| results = model.predict(source=entrada, device=device, conf=conf_min, verbose=False) | |
| r = results[0] | |
| img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) | |
| ch, cw = entrada.shape[:2] | |
| # --- máscaras (em coordenadas do recorte) --- | |
| mascaras_binarias = [] | |
| for m in mascaras_da_predicao(r, cw, ch, metodo_mascara): | |
| if contar_pixels_conexos(m * 255, pixels_minimos) > 0: | |
| mascaras_binarias.append(m) | |
| mascaras_fundidas = [] | |
| for m in mascaras_binarias: | |
| fundida = False | |
| for i in range(len(mascaras_fundidas)): | |
| if np.count_nonzero(cv2.bitwise_and(m, mascaras_fundidas[i])) > 0: | |
| mascaras_fundidas[i] = cv2.bitwise_or(mascaras_fundidas[i], m) | |
| fundida = True | |
| break | |
| if not fundida: | |
| mascaras_fundidas.append(m) | |
| # --- centróides em float, convertidos para coordenadas da imagem original --- | |
| centroides = [] | |
| for m in mascaras_fundidas: | |
| M = cv2.moments(m) | |
| if M["m00"] != 0: | |
| cx = M["m10"] / M["m00"] + off_x # float: não arredondar aqui | |
| cy = M["m01"] / M["m00"] + off_y | |
| centroides.append((cx, cy)) | |
| cv2.circle(img_rgb, (int(round(cx)), int(round(cy))), 12, (0, 255, 0), 2) | |
| distancia_px = None | |
| distancia_mm = None | |
| if len(centroides) == 2: | |
| p1 = (int(round(centroides[0][0])), int(round(centroides[0][1]))) | |
| p2 = (int(round(centroides[1][0])), int(round(centroides[1][1]))) | |
| cv2.line(img_rgb, p1, p2, (0, 0, 255), 2) | |
| for p in (p1, p2): | |
| cv2.circle(img_rgb, p, 6, (0, 0, 255), -1) | |
| distancia_px = calcular_distancia(centroides[0], centroides[1]) # float | |
| if mm_por_pixel: | |
| distancia_mm = distancia_px * mm_por_pixel | |
| # --- textos informativos --- | |
| if escala_info["valor_mm"] and escala_info["barra_px"]: | |
| met = "c2c" if escala_info["metodo_barra"] == "centro-a-centro" else "bbox" | |
| escala_str = f"{escala_info['valor_mm']:g} mm / {escala_info['barra_px']:.0f} px ({met})" | |
| else: | |
| escala_str = "não detetada" | |
| if rec_info.get("ok"): | |
| bx, by, bw, bh = rec_info["bbox"] | |
| recorte_str = f"{bw}x{bh} px" | |
| else: | |
| recorte_str = f"sem recorte ({rec_info.get('motivo', '-')})" | |
| nome = os.path.basename(imagem_path) | |
| output_path = os.path.splitext(nome)[0] + "_out.png" | |
| cv2.imwrite(output_path, cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)) | |
| return output_path, nome, distancia_px, distancia_mm, escala_str, recorte_str | |
| def processar_varias(imagens, dispositivo, conf_min, pixels_minimos, | |
| usar_recorte=True, limpar_fundo=False, metodo_mascara="auto"): | |
| # sem imagens (ex.: alternar a opção antes de carregar ficheiros) | |
| if not imagens: | |
| vazio = pd.DataFrame({"Imagem": [], "Escala (real)": [], "Asa recortada": [], | |
| "Distância (px)": [], "Distância (mm)": []}) | |
| return [], vazio, pd.DataFrame({"Métrica": [], "Valor (mm)": []}), None | |
| imagens_resultado = [] | |
| nomes, pxs, mms, escalas, recortes = [], [], [], [], [] | |
| for imagem in imagens: | |
| out_path, nome, dist_px, dist_mm, escala_str, recorte_str = processar_uma_imagem( | |
| imagem, dispositivo, conf_min, pixels_minimos, usar_recorte, | |
| limpar_fundo, metodo_mascara) | |
| imagens_resultado.append(out_path) | |
| nomes.append(nome) | |
| pxs.append(f"{dist_px:.1f}" if dist_px else "-") | |
| mms.append(f"{dist_mm:.3f}" if dist_mm else "-") | |
| escalas.append(escala_str) | |
| recortes.append(recorte_str) | |
| valores_mm = [float(x) for x in mms if x != "-"] | |
| media = np.mean(valores_mm) if valores_mm else 0 | |
| desvio = np.std(valores_mm) if valores_mm else 0 | |
| tabela = pd.DataFrame({ | |
| "Imagem": nomes, | |
| "Escala (real)": escalas, | |
| "Asa recortada": recortes, | |
| "Distância (px)": pxs, | |
| "Distância (mm)": mms, | |
| }) | |
| resumo_df = pd.DataFrame({"Métrica": ["Média", "Desvio padrão"], | |
| "Valor (mm)": [f"{media:.3f}", f"{desvio:.3f}"]}) | |
| agora = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") | |
| resultados_xlsx_path = f"resultados_medidas_{agora}.xlsx" | |
| with pd.ExcelWriter(resultados_xlsx_path) as writer: | |
| tabela.to_excel(writer, sheet_name="Resultados", index=False) | |
| resumo_df.to_excel(writer, sheet_name="Resumo", index=False) | |
| pasta_saida = f"imagens_processadas_{agora}" | |
| os.makedirs(pasta_saida, exist_ok=True) | |
| for img_path in imagens_resultado: | |
| os.replace(img_path, os.path.join(pasta_saida, os.path.basename(img_path))) | |
| imagens_resultado = [os.path.join(pasta_saida, os.path.basename(p)) for p in imagens_resultado] | |
| legendas = [f"{n} - {m} mm" for n, m in zip(nomes, mms)] | |
| return list(zip(imagens_resultado, legendas)), tabela, resumo_df, resultados_xlsx_path | |
| # Interface Gradio | |
| with gr.Blocks(title="Medição de asas múltiplas") as demo: | |
| gr.Markdown("## Medição automática de asas de abelha com normalização à escala impressa") | |
| imagens_input = gr.File(label="Subir imagens", file_types=[".png", ".jpg", ".jpeg"], file_count="multiple") | |
| dispositivo = gr.Radio(["CPU", "GPU"], label="Dispositivo", value="CPU", visible=False) | |
| conf_slider = gr.Slider(0.01, 1.0, value=0.1, step=0.01, label="Confiança mínima", visible=False) | |
| pixel_slider = gr.Slider(1, 1000, value=10, step=1, label="Pixels conectados mínimos por máscara", visible=False) | |
| imagens_saida = gr.Gallery(label="Resultados das imagens segmentadas", columns=4, preview=True, show_label=True) | |
| tabela_resultados = gr.Dataframe(label="Resultados", interactive=False, | |
| headers=["Imagem", "Escala (real)", "Asa recortada", | |
| "Distância (px)", "Distância (mm)"]) | |
| tabela_resumo = gr.Dataframe(label="Resumo estatístico", interactive=False, headers=["Métrica", "Valor (mm)"]) | |
| download_btn = gr.File(label="Download XLSX") | |
| imagens_input.change( | |
| # fixo: máscaras em "auto" (masks.xy), recorte da asa ligado, | |
| # sem uniformizar o fundo (isso descaracteriza a imagem e o modelo falha) | |
| fn=lambda imagens: processar_varias(imagens, "CPU", 0.1, 10, | |
| usar_recorte=True, | |
| limpar_fundo=False, | |
| metodo_mascara="auto"), | |
| inputs=imagens_input, | |
| outputs=[imagens_saida, tabela_resultados, tabela_resumo, download_btn] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(share=False) |