agrobot_base/Python/yolov8-seg/infer.py

379 lines
13 KiB
Python
Raw Permalink Normal View History

2026-02-24 13:38:14 +00:00
# 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:
"""
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 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()