177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
|
|
import os
|
||
|
|
import cv2
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
# ========= CONFIG =========
|
||
|
|
RAW_PATH = r"dataset/ds/raws/20260115_152748_007.raw" # ajuste se precisar
|
||
|
|
W = 2592 # largura do RAW (pixels do mosaico)
|
||
|
|
H = 2056 # altura do RAW
|
||
|
|
UPSCALE = 2 # aumenta preview (2x fica bom)
|
||
|
|
|
||
|
|
# overlay
|
||
|
|
ALPHA = 0.45 # transparência da máscara
|
||
|
|
MIN_IR = 15 # ignora pixels muito escuros no IR (ruído)
|
||
|
|
MIN_G = 20 # ignora pixels muito escuros no G (ruído)
|
||
|
|
|
||
|
|
# ========= RAW decode =========
|
||
|
|
def read_raw_mosaic(path, w, h):
|
||
|
|
raw = np.fromfile(path, dtype=np.uint8)
|
||
|
|
if raw.size != w * h:
|
||
|
|
raise RuntimeError(f"RAW size mismatch: got {raw.size}, expected {w*h}. "
|
||
|
|
f"Confira W/H.")
|
||
|
|
return raw.reshape(h, w)
|
||
|
|
|
||
|
|
def split_4ch(raw):
|
||
|
|
# 2x2 pattern:
|
||
|
|
# [R, G]
|
||
|
|
# [IR,B]
|
||
|
|
R = raw[0::2, 0::2]
|
||
|
|
G = raw[0::2, 1::2]
|
||
|
|
IR = raw[1::2, 0::2]
|
||
|
|
B = raw[1::2, 1::2]
|
||
|
|
return R, G, IR, B
|
||
|
|
|
||
|
|
def norm8(x, p_lo=2, p_hi=98):
|
||
|
|
lo = np.percentile(x, p_lo)
|
||
|
|
hi = np.percentile(x, p_hi)
|
||
|
|
if hi <= lo + 1:
|
||
|
|
return x.astype(np.uint8)
|
||
|
|
y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo))
|
||
|
|
return np.clip(y, 0, 255).astype(np.uint8)
|
||
|
|
|
||
|
|
def make_rgb_preview(R, G, B, upscale=2):
|
||
|
|
Rn, Gn, Bn = norm8(R), norm8(G), norm8(B)
|
||
|
|
bgr = np.dstack([Bn, Gn, Rn]) # OpenCV = BGR
|
||
|
|
if upscale != 1:
|
||
|
|
bgr = cv2.resize(bgr, (bgr.shape[1]*upscale, bgr.shape[0]*upscale), interpolation=cv2.INTER_NEAREST)
|
||
|
|
return bgr
|
||
|
|
|
||
|
|
# ========= Simple spectral classifier =========
|
||
|
|
def classify_cane_weed(G, IR, thr_ratio, thr_ir_bias):
|
||
|
|
"""
|
||
|
|
Retorna mask_cane, mask_weed em resolução H/2 x W/2.
|
||
|
|
|
||
|
|
Padrões:
|
||
|
|
- ERVA: ratio = G/(IR+1) maior
|
||
|
|
- CANA: IR relativamente maior + ratio menor
|
||
|
|
|
||
|
|
thr_ratio: limiar principal de G/IR
|
||
|
|
thr_ir_bias: adicional: favorece CANA quando IR está alto
|
||
|
|
"""
|
||
|
|
Gf = G.astype(np.float32)
|
||
|
|
IRf = IR.astype(np.float32)
|
||
|
|
|
||
|
|
ratio = Gf / (IRf + 1.0)
|
||
|
|
|
||
|
|
valid = (Gf >= MIN_G) & (IRf >= MIN_IR)
|
||
|
|
|
||
|
|
# regra: erva se ratio > thr_ratio
|
||
|
|
weed = valid & (ratio >= thr_ratio)
|
||
|
|
|
||
|
|
# cana: ratio baixo OU IR alto (bias)
|
||
|
|
# IR alto relativo: IR > (G - thr_ir_bias) ajuda puxar cana
|
||
|
|
cane = valid & (ratio < thr_ratio)
|
||
|
|
|
||
|
|
|
||
|
|
# resolve conflitos: se cair em ambos, usa ratio como desempate
|
||
|
|
both = weed & cane
|
||
|
|
if np.any(both):
|
||
|
|
# se ratio alto -> weed, senão -> cane
|
||
|
|
weed[both] = ratio[both] >= thr_ratio
|
||
|
|
cane[both] = ~weed[both]
|
||
|
|
|
||
|
|
# pixels válidos mas não classificados: decide pelo ratio
|
||
|
|
undec = valid & ~(weed | cane)
|
||
|
|
if np.any(undec):
|
||
|
|
weed[undec] = ratio[undec] >= thr_ratio
|
||
|
|
cane[undec] = ~weed[undec]
|
||
|
|
|
||
|
|
return cane, weed, ratio, valid
|
||
|
|
|
||
|
|
def morph_cleanup(mask, k=3):
|
||
|
|
if k <= 1:
|
||
|
|
return mask
|
||
|
|
ker = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
|
||
|
|
m = mask.astype(np.uint8) * 255
|
||
|
|
m = cv2.medianBlur(m, 3)
|
||
|
|
m = cv2.morphologyEx(m, cv2.MORPH_OPEN, ker, iterations=1)
|
||
|
|
m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, ker, iterations=1)
|
||
|
|
return m > 0
|
||
|
|
|
||
|
|
def overlay_classes(bgr, cane_mask, weed_mask, upscale=2):
|
||
|
|
# sobe masks pro tamanho do preview
|
||
|
|
h2, w2 = cane_mask.shape
|
||
|
|
if upscale != 1:
|
||
|
|
cane = cv2.resize(cane_mask.astype(np.uint8)*255, (w2*upscale, h2*upscale), interpolation=cv2.INTER_NEAREST)
|
||
|
|
weed = cv2.resize(weed_mask.astype(np.uint8)*255, (w2*upscale, h2*upscale), interpolation=cv2.INTER_NEAREST)
|
||
|
|
else:
|
||
|
|
cane = cane_mask.astype(np.uint8)*255
|
||
|
|
weed = weed_mask.astype(np.uint8)*255
|
||
|
|
|
||
|
|
out = bgr.copy()
|
||
|
|
|
||
|
|
# cores (BGR): cana=azul, erva=verde
|
||
|
|
cane_col = np.zeros_like(out)
|
||
|
|
cane_col[:, :, 0] = cane # Blue
|
||
|
|
|
||
|
|
weed_col = np.zeros_like(out)
|
||
|
|
weed_col[:, :, 1] = weed # Green
|
||
|
|
|
||
|
|
# combina overlays
|
||
|
|
mask_any = (cane > 0) | (weed > 0)
|
||
|
|
overlay = np.clip(cane_col + weed_col, 0, 255).astype(np.uint8)
|
||
|
|
|
||
|
|
out[mask_any] = (out[mask_any].astype(np.float32) * (1 - ALPHA) + overlay[mask_any].astype(np.float32) * ALPHA).astype(np.uint8)
|
||
|
|
return out
|
||
|
|
|
||
|
|
def main():
|
||
|
|
raw = read_raw_mosaic(RAW_PATH, W, H)
|
||
|
|
R, G, IR, B = split_4ch(raw)
|
||
|
|
|
||
|
|
base = make_rgb_preview(R, G, B, upscale=UPSCALE)
|
||
|
|
|
||
|
|
cv2.namedWindow("overlay", cv2.WINDOW_NORMAL)
|
||
|
|
cv2.namedWindow("debug", cv2.WINDOW_NORMAL)
|
||
|
|
|
||
|
|
# sliders
|
||
|
|
# ratio em escala 0..300 -> 0.00..3.00
|
||
|
|
cv2.createTrackbar("thr_ratio x100", "overlay", 270, 500, lambda v: None) # 2.70 inicial
|
||
|
|
cv2.createTrackbar("ir_bias", "overlay", 5, 100, lambda v: None) # 5 inicial
|
||
|
|
cv2.createTrackbar("morph_k", "overlay", 5, 21, lambda v: None) # 5 inicial
|
||
|
|
|
||
|
|
while True:
|
||
|
|
thr_ratio = cv2.getTrackbarPos("thr_ratio x100", "overlay") / 100.0
|
||
|
|
thr_ir_bias = float(cv2.getTrackbarPos("ir_bias", "overlay"))
|
||
|
|
mk = cv2.getTrackbarPos("morph_k", "overlay")
|
||
|
|
if mk % 2 == 0:
|
||
|
|
mk += 1
|
||
|
|
|
||
|
|
cane, weed, ratio, valid = classify_cane_weed(G, IR, thr_ratio, thr_ir_bias)
|
||
|
|
|
||
|
|
cane2 = morph_cleanup(cane, k=mk)
|
||
|
|
weed2 = morph_cleanup(weed, k=mk)
|
||
|
|
|
||
|
|
out = overlay_classes(base, cane2, weed2, upscale=UPSCALE)
|
||
|
|
|
||
|
|
# debug views
|
||
|
|
ratio_vis = norm8(ratio, 2, 98)
|
||
|
|
if UPSCALE != 1:
|
||
|
|
ratio_vis = cv2.resize(ratio_vis, (ratio_vis.shape[1]*UPSCALE, ratio_vis.shape[0]*UPSCALE), interpolation=cv2.INTER_NEAREST)
|
||
|
|
|
||
|
|
# desenha texto rápido
|
||
|
|
txt = f"thr_ratio={thr_ratio:.2f} ir_bias={thr_ir_bias:.0f} morph_k={mk}"
|
||
|
|
cv2.putText(out, txt, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,0), 3, cv2.LINE_AA)
|
||
|
|
cv2.putText(out, txt, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255,255,255), 2, cv2.LINE_AA)
|
||
|
|
|
||
|
|
cv2.imshow("overlay", out)
|
||
|
|
cv2.imshow("debug", ratio_vis)
|
||
|
|
|
||
|
|
k = cv2.waitKey(10) & 0xFF
|
||
|
|
if k in (ord('q'), 27):
|
||
|
|
break
|
||
|
|
|
||
|
|
cv2.destroyAllWindows()
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|