# infer.py (batendo com o treino: RAW->uint8 HWC, LetterBox, forward direto, NMS, masks, unletterbox) import cv2 import torch import numpy as np from pathlib import Path import yaml from ultralytics import YOLO from ultralytics.utils import ops # ============================ # CONFIG # ============================ with Path("data.yaml").open("r", encoding="utf-8") as f: cfg = yaml.safe_load(f) or {} CHANNELS = int(cfg.get("channels", 4)) # 4 ou 5 SCALE = cfg.get("scale", "m") IMG_SZ = int(cfg.get("size", 800)) names_dict = cfg.get("names", {}) colors_dict = cfg.get("colors", {}) CLASS_NAMES = [names_dict[k] for k in sorted(names_dict.keys())] if names_dict else [] CLASS_COLORS = [ tuple(map(int, colors_dict[k].strip("()").split(","))) for k in sorted(colors_dict.keys()) ] if colors_dict else [] # Ajuste conforme seu run WEIGHTS_PATH = f"C:/ZendionInc/agrobot_base/Python/yolov8-seg/runs/segment/ch_{CHANNELS}/sc_{SCALE}/sz_{IMG_SZ}/weed_detector_segformer/weights/best.pt" TEST_DIR = Path(r"C:\ZendionInc\agrobot_base\Python\yolov8-seg\images\val") # ============================ # RAW settings (iguais do treino) # ============================ RAW_DTYPE = np.float32 RAW_ALREADY_01 = True RAW_MAX_VAL = 1.0 if RAW_ALREADY_01 else 65535.0 # se algum dia usar uint16 # ============================ # Letterbox (multi-canal) # ============================ def letterbox_multi( img: np.ndarray, new_shape: int = 640, color: int = 114 ): """ img: HWC uint8 (H,W,C) Retorna: img_lb: letterboxed HWC uint8 (new_shape, new_shape, C) ratio: (r, r) pad: (dw, dh) em pixels (float) new_unpad: (w_new, h_new) """ assert img.ndim == 3, "img deve ser HWC" h0, w0 = img.shape[:2] # scale ratio (new / old) r = min(new_shape / h0, new_shape / w0) w_new = int(round(w0 * r)) h_new = int(round(h0 * r)) # compute padding dw = (new_shape - w_new) / 2 dh = (new_shape - h_new) / 2 # resize if (w0, h0) != (w_new, h_new): img = cv2.resize(img, (w_new, h_new), interpolation=cv2.INTER_LINEAR) # pad (top, bottom, left, right) top = int(round(dh - 0.1)) bottom = int(round(dh + 0.1)) left = int(round(dw - 0.1)) right = int(round(dw + 0.1)) # padding constante para qualquer número de canais img_lb = np.pad( img, pad_width=((top, bottom), (left, right), (0, 0)), mode="constant", constant_values=color ).astype(img.dtype, copy=False) ratio = (r, r) pad = (dw, dh) return img_lb, ratio, pad, (w_new, h_new), (top, bottom, left, right) def unletterbox_mask_to_original( masks_lb: torch.Tensor, # (N, Hlb, Wlb) bool/0-1 orig_hw: tuple, # (h0, w0) new_unpad_wh: tuple, # (w_new, h_new) pads_tblr: tuple # (top, bottom, left, right) ): """ Remove padding e volta masks para tamanho original. """ h0, w0 = orig_hw w_new, h_new = new_unpad_wh top, bottom, left, right = pads_tblr # crop padding masks_crop = masks_lb[:, top:top + h_new, left:left + w_new] # (N, h_new, w_new) # resize para original com interpolate (torch) masks_crop = masks_crop.unsqueeze(1).float() # (N,1,h_new,w_new) masks_up = torch.nn.functional.interpolate( masks_crop, size=(h0, w0), mode="bilinear", align_corners=False ) return (masks_up[:, 0] > 0.5) # (N,h0,w0) bool # ============================ # RAW loading (igual treino) # ============================ def load_raw_as_uint8_hwc(img_path: Path, canais: int = CHANNELS) -> np.ndarray: """ Lê RAW (4 canais físicos R,G,IR,B) como CHW no arquivo, normaliza fixo e retorna HWC uint8 com C=4 ou C=5 (com NDVI em 0..255). """ # usa o preview só pra pegar H,W (igual treino) preview = cv2.imread(str(img_path), cv2.IMREAD_UNCHANGED) if preview is None: raise FileNotFoundError(f"Não consegui ler preview: {img_path}") h0, w0 = preview.shape[:2] raw_path = img_path.with_suffix(".raw") if not raw_path.exists(): raise FileNotFoundError(f"RAW não encontrado: {raw_path}") num_raw_channels = 4 arr_flat = np.fromfile(str(raw_path), dtype=RAW_DTYPE) expected_size = num_raw_channels * h0 * w0 if arr_flat.size != expected_size: raise ValueError(f"RAW size errado {raw_path}: esperado {expected_size}, veio {arr_flat.size}") raw_chw = arr_flat.reshape(num_raw_channels, h0, w0) # (4,H,W) arr = np.transpose(raw_chw, (1, 2, 0)).astype(np.float32, copy=False) # (H,W,4) # normalização fixa if RAW_ALREADY_01: arr_norm = arr else: arr_norm = arr / float(RAW_MAX_VAL) arr_norm = np.clip(arr_norm, 0.0, 1.0) # NDVI opcional (5ch) if canais == 5: R = arr_norm[..., 0] IR = arr_norm[..., 2] eps = 1e-6 ndvi = (IR - R) / (IR + R + eps) # [-1,1] ndvi = np.clip(ndvi, -1.0, 1.0) ndvi01 = (ndvi + 1.0) / 2.0 # [0,1] arr_norm = np.concatenate([arr_norm, ndvi01[..., None]], axis=-1) # (H,W,5) elif canais != 4: raise ValueError(f"canais deve ser 4 ou 5, veio {canais}") im_uint8 = (arr_norm * 255.0).round().clip(0, 255).astype(np.uint8) return im_uint8 # HWC uint8 def make_bgr_preview_from_raw_uint8(raw_hwc_uint8: np.ndarray) -> np.ndarray: """ Preview BGR (OpenCV) a partir de HWC uint8: assume ordem [R,G,IR,B,(NDVI)]. Usa B=canal 3, G=canal 1, R=canal 0. """ R = raw_hwc_uint8[..., 0] G = raw_hwc_uint8[..., 1] B = raw_hwc_uint8[..., 3] return np.dstack([B, G, R]) # ============================ # Drawing # ============================ def draw_result(vis_bgr: np.ndarray, boxes_xyxy: np.ndarray, cls_ids: np.ndarray, confs: np.ndarray, masks: np.ndarray | None): out = vis_bgr.copy() # máscaras (se houver) if masks is not None: for i in range(masks.shape[0]): cls = int(cls_ids[i]) color = CLASS_COLORS[cls] if cls < len(CLASS_COLORS) else (0, 255, 0) m = masks[i].astype(bool) overlay = np.array(color, dtype=np.float32) out[m] = (out[m] * 0.4 + overlay * 0.6).astype(np.uint8) # boxes + labels for i in range(len(boxes_xyxy)): x1, y1, x2, y2 = boxes_xyxy[i].astype(int) cls = int(cls_ids[i]) conf = float(confs[i]) color = CLASS_COLORS[cls] if cls < len(CLASS_COLORS) else (0, 255, 0) name = CLASS_NAMES[cls] if cls < len(CLASS_NAMES) else str(cls) cv2.rectangle(out, (x1, y1), (x2, y2), color, 2) txt = f"{name} {conf:.2f}" cv2.putText(out, txt, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, cv2.LINE_AA) return out # ============================ # Inference core # ============================ @torch.no_grad() def infer_one(model: YOLO, raw_uint8_hwc: np.ndarray, conf_thres=0.25, iou_thres=0.7, max_det=300): """ raw_uint8_hwc: HWC uint8 com C=4 ou 5 Retorna boxes/masks já no tamanho original (h0,w0) """ device = next(model.model.parameters()).device h0, w0 = raw_uint8_hwc.shape[:2] # 1) LetterBox igual treino (mantém proporção) lb_img, ratio, pad, new_unpad_wh, pads_tblr = letterbox_multi(raw_uint8_hwc, new_shape=IMG_SZ, color=114) # 2) Tensor (B,C,H,W) float32 0..1 x = torch.from_numpy(lb_img).to(device) # (H,W,C) uint8 x = x.permute(2, 0, 1).contiguous().float() / 255.0 # (C,H,W) float x = x.unsqueeze(0) # (1,C,H,W) # 3) Forward direto out = model.model(x) # 4) separa preds/proto if isinstance(out, (list, tuple)): preds_raw = out[0] proto = out[1] if len(out) > 1 else None else: preds_raw = out proto = None if preds_raw.ndim == 2: preds_raw = preds_raw.unsqueeze(0) nc = len(model.names) if hasattr(model, "names") else len(CLASS_NAMES) det_list = ops.non_max_suppression( preds_raw, conf_thres=conf_thres, iou_thres=iou_thres, classes=None, agnostic=False, max_det=max_det, nc=nc ) det = det_list[0] if det is None or len(det) == 0: return None # det: [x1,y1,x2,y2,conf,cls, ...mask coeffs] boxes_lb = det[:, :4] confs = det[:, 4] cls_ids = det[:, 5].to(torch.int64) # 5) Máscaras no espaço letterboxed masks_orig = None if proto is not None and det.shape[1] > 6: # proto pode vir com batch proto_t = proto while isinstance(proto_t, (list, tuple)): proto_t = proto_t[0] if len(proto_t) else None if proto_t is None: break if isinstance(proto_t, torch.Tensor): if proto_t.ndim == 4: proto_t = proto_t[0] # (C,Hm,Wm) proto_t = proto_t.to(det.device) mask_coeffs = det[:, 6:].to(det.device) # garante C_proto == C_mask c_proto = proto_t.shape[0] c_mask = mask_coeffs.shape[1] if c_proto != c_mask: if c_proto > c_mask: proto_t = proto_t[:c_mask] else: mask_coeffs = mask_coeffs[:, :c_proto] # gera máscaras no tamanho do input (letterboxed) masks_lb = ops.process_mask(proto_t, mask_coeffs, boxes_lb, x.shape[2:], upsample=True) # (N,Hlb,Wlb) bool # desfaz letterbox -> original masks_orig_t = unletterbox_mask_to_original(masks_lb, (h0, w0), new_unpad_wh, pads_tblr) masks_orig = masks_orig_t.detach().cpu().numpy().astype(np.uint8) # 0/1 # 6) Boxes: letterbox -> original (usa ratio_pad) # ratio_pad esperado: (ratio, pad) onde pad é (dw, dh) ratio_pad = (ratio, pad) boxes_scaled = ops.scale_boxes(x.shape[2:], boxes_lb.clone(), (h0, w0), ratio_pad=ratio_pad) boxes_xyxy = boxes_scaled.detach().cpu().numpy() return { "boxes": boxes_xyxy, "confs": confs.detach().cpu().numpy(), "cls": cls_ids.detach().cpu().numpy(), "masks": masks_orig } # ============================ # Main loop viewer # ============================ def main(): device = "cuda" if torch.cuda.is_available() else "cpu" print(f"Usando dispositivo: {device}") model = YOLO(WEIGHTS_PATH) model.to(device) model.fuse() # sanity check do patch first_conv = model.model.model[0].conv print("First conv weight shape:", tuple(first_conv.weight.shape)) # deve ser [48, C, 3, 3] img_paths = sorted([p for p in TEST_DIR.iterdir() if p.suffix.lower() in [".jpg", ".jpeg", ".png"]]) if not img_paths: print(f"Nenhuma imagem encontrada em {TEST_DIR}") return idx = 0 n = len(img_paths) CONF = 0.45 IOU = 0.7 cv2.namedWindow(f"YOLO {CHANNELS}ch Viewer", cv2.WINDOW_NORMAL) while True: img_path = img_paths[idx] print("\n======================================") print(f"Processando: {img_path}") try: raw_uint8 = load_raw_as_uint8_hwc(img_path, canais=CHANNELS) # HWC uint8 preview_bgr = make_bgr_preview_from_raw_uint8(raw_uint8) res = infer_one(model, raw_uint8, conf_thres=CONF, iou_thres=IOU) if res is None: vis = preview_bgr.copy() cv2.putText(vis, "SEM DETECCOES", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,0,255), 2, cv2.LINE_AA) else: vis = draw_result( preview_bgr, boxes_xyxy=res["boxes"], cls_ids=res["cls"], confs=res["confs"], masks=res["masks"] ) txt = f"[{idx+1}/{n}] {img_path.name} | A/D navega | +/- conf {CONF:.2f} | Q/ESC sai" cv2.putText(vis, txt, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 2, cv2.LINE_AA) cv2.imshow(f"YOLO {CHANNELS}ch Viewer", vis) except Exception as e: print(f"Erro ao processar {img_path}: {e}") blank = np.zeros((480, 900, 3), dtype=np.uint8) cv2.putText(blank, f"Erro em {img_path.name}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2) cv2.putText(blank, str(e)[:120], (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2) cv2.imshow(f"YOLO {CHANNELS}ch Viewer", blank) k = cv2.waitKey(0) & 0xFF if k in (ord("q"), 27): # q ou ESC break elif k == ord("a"): idx = (idx - 1) % n elif k == ord("d"): idx = (idx + 1) % n elif k in (ord("+"), ord("=")): CONF = min(0.99, CONF + 0.05) elif k in (ord("-"), ord("_")): CONF = max(0.01, CONF - 0.05) cv2.destroyAllWindows() if __name__ == "__main__": main()