ajuste no uso de cpu a 100% no weed worker

This commit is contained in:
Diego Freitas 2026-06-18 09:26:19 -03:00
parent afc91db881
commit 632f672ce1
7 changed files with 469 additions and 20 deletions

View File

@ -237,6 +237,40 @@ class RawProcessorCore:
self.rgb_processing_config = {
"mode": "linear_demosaic", # "linear_demosaic", "linear_demosaic_half" ou "bayer_planes"
"demosaic_algorithm": "ea", # "ea" ou "bilinear"
# Pós-processamento RGB opcional, aplicado logo após o decode/debayer
# e antes da fusão com RE/NIR.
#
# backend aceitos:
# "none" / None / false
# "hybrid" / "hybrid:balanced" / "hybrid_balanced"
# "hybrid_soft", "hybrid_strong"
#
# Observação:
# - Por padrão fica desligado para manter compatibilidade total.
# - Para o experimento atual, use backend="hybrid" e preset="balanced".
"enhancement": {
"enabled": False,
"backend": "none",
"preset": "balanced", # "soft", "balanced", "strong"
"apply_to_preview_input": False,
# Mantém o pipeline parecido com o script de preview/regens:
# float01 -> uint8 -> OpenCV -> float01.
"use_u8_pipeline": True,
# Desligado por padrão para preservar escala radiométrica.
# Se quiser reproduzir exatamente o visual do script de previews,
# pode ligar este auto_stretch.
"auto_stretch": {
"enabled": False,
"low_pct": 0.2,
"high_pct": 99.8
},
"clip_output": True,
"save_debug": True
}
}
self.fusion_config = {
@ -389,6 +423,7 @@ class RawProcessorCore:
self.last_decode_perf = {}
self._last_decode_perf_log_ts = 0.0
self.last_rgb_enhancement_result = None
if calibration_json_path:
self.load_config_json(calibration_json_path)
@ -553,6 +588,301 @@ class RawProcessorCore:
return rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
# ============================================================
# RGB ENHANCEMENT / REGEN BACKENDS
# ============================================================
def _get_rgb_enhancement_config(self) -> dict:
"""
Resolve a configuração de pós-processamento RGB dentro de rgb_processing.
Contrato recomendado no module_params.json:
"rgb_processing": {
"mode": "bayer_planes",
"demosaic_algorithm": "ea",
"enhancement": {
"enabled": true,
"backend": "hybrid",
"preset": "balanced",
"use_u8_pipeline": true,
"auto_stretch": {
"enabled": false,
"low_pct": 0.2,
"high_pct": 99.8
},
"clip_output": true
}
}
Compatibilidade:
- também aceita rgb_processing.enhancement_backend = "hybrid:balanced"
- também aceita rgb_processing.enhancement_preset = "balanced"
"""
rgb_cfg = getattr(self, "rgb_processing_config", {}) or {}
enh = rgb_cfg.get("enhancement", {}) or {}
if not isinstance(enh, dict):
enh = {}
# Atalhos opcionais no nível de rgb_processing.
if "enhancement_backend" in rgb_cfg and "backend" not in enh:
enh["backend"] = rgb_cfg.get("enhancement_backend")
if "enhancement_preset" in rgb_cfg and "preset" not in enh:
enh["preset"] = rgb_cfg.get("enhancement_preset")
if "enhancement_enabled" in rgb_cfg and "enabled" not in enh:
enh["enabled"] = bool(rgb_cfg.get("enhancement_enabled"))
backend = enh.get("backend", "none")
backend_norm, preset_norm = self._resolve_rgb_enhancement_backend_and_preset(
backend=backend,
preset=enh.get("preset", "balanced"),
)
enabled = bool(enh.get("enabled", False))
if backend_norm in ("none", "", "off", "disabled"):
enabled = False
auto_stretch = enh.get("auto_stretch", {}) or {}
if not isinstance(auto_stretch, dict):
auto_stretch = {}
return {
"enabled": enabled,
"backend": backend_norm,
"preset": preset_norm,
"apply_to_preview_input": bool(enh.get("apply_to_preview_input", False)),
"use_u8_pipeline": bool(enh.get("use_u8_pipeline", True)),
"auto_stretch": {
"enabled": bool(auto_stretch.get("enabled", False)),
"low_pct": float(auto_stretch.get("low_pct", 0.2)),
"high_pct": float(auto_stretch.get("high_pct", 99.8)),
},
"clip_output": bool(enh.get("clip_output", True)),
"save_debug": bool(enh.get("save_debug", True)),
}
def _resolve_rgb_enhancement_backend_and_preset(self, backend, preset="balanced") -> tuple[str, str]:
"""
Aceita strings amigáveis:
none
hybrid
hybrid:balanced
hybrid_balanced
hybrid-soft
"""
if backend is None or backend is False:
return "none", "balanced"
b = str(backend).strip().lower()
p = str(preset or "balanced").strip().lower()
if b in ("", "none", "off", "false", "disabled", "raw"):
return "none", "balanced"
# hybrid:balanced
if ":" in b:
parts = [x.strip() for x in b.split(":") if x.strip()]
if len(parts) >= 1:
b = parts[0]
if len(parts) >= 2:
p = parts[1]
# hybrid_balanced / hybrid-balanced
for sep in ("_", "-"):
if b.startswith(f"hybrid{sep}"):
p = b.split(sep, 1)[1]
b = "hybrid"
if p not in ("soft", "balanced", "strong"):
p = "balanced"
if b not in ("hybrid",):
raise ValueError(
f"rgb_processing.enhancement.backend inválido: {backend}. "
"Use 'none' ou 'hybrid'."
)
return b, p
def _rgb_float01_to_u8_for_enhancement(self, rgb: np.ndarray, cfg: dict) -> np.ndarray:
"""
Converte RGB float01 para uint8.
Opcionalmente aplica auto_stretch por percentil para reproduzir melhor
o visual dos scripts de preview/regens.
Por padrão auto_stretch fica desligado, porque preservar a escala do tensor
tende a ser mais seguro para treino/inferência.
"""
x = np.asarray(rgb, dtype=np.float32)
auto = (cfg or {}).get("auto_stretch", {}) or {}
if bool(auto.get("enabled", False)):
low_pct = float(auto.get("low_pct", 0.2))
high_pct = float(auto.get("high_pct", 99.8))
lo = np.percentile(x, low_pct)
hi = np.percentile(x, high_pct)
if hi <= lo + 1e-6:
lo = float(np.min(x))
hi = float(np.max(x))
x = (x - float(lo)) / max(float(hi - lo), 1e-6)
x = np.clip(x, 0.0, 1.0)
return np.clip(x * 255.0 + 0.5, 0, 255).astype(np.uint8)
def _gray_world_wb_u8(self, rgb_u8: np.ndarray, strength: float = 0.55) -> np.ndarray:
img = rgb_u8.astype(np.float32)
means = img.reshape(-1, 3).mean(axis=0)
target = float(means.mean())
gains = target / np.maximum(means, 1e-6)
gains = np.clip(gains, 0.60, 1.70)
gains = 1.0 + (gains - 1.0) * float(strength)
return np.clip(img * gains[None, None, :], 0, 255).astype(np.uint8)
def _apply_gamma_u8(self, rgb_u8: np.ndarray, gamma: float = 0.94) -> np.ndarray:
x = rgb_u8.astype(np.float32) / 255.0
y = np.power(np.clip(x, 0.0, 1.0), float(gamma))
return np.clip(y * 255.0 + 0.5, 0, 255).astype(np.uint8)
def _clahe_luminance_u8(self, rgb_u8: np.ndarray, clip_limit: float = 1.7, tile_grid_size: int = 8) -> np.ndarray:
lab = cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(
clipLimit=float(clip_limit),
tileGridSize=(int(tile_grid_size), int(tile_grid_size)),
)
l2 = clahe.apply(l)
return cv2.cvtColor(cv2.merge([l2, a, b]), cv2.COLOR_LAB2RGB)
def _denoise_fast_u8(self, rgb_u8: np.ndarray, strength: float = 3.0) -> np.ndarray:
bgr = cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
out = cv2.bilateralFilter(
bgr,
d=5,
sigmaColor=float(strength) * 12.0,
sigmaSpace=3.0,
)
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
def _unsharp_u8(self, rgb_u8: np.ndarray, sigma: float = 0.9, amount: float = 0.85, threshold: int = 2) -> np.ndarray:
img = rgb_u8.astype(np.float32)
blur = cv2.GaussianBlur(img, (0, 0), sigmaX=float(sigma), sigmaY=float(sigma))
sharp = img + float(amount) * (img - blur)
if int(threshold) > 0:
diff = np.max(np.abs(img - blur), axis=2)
mask = diff >= int(threshold)
out = img.copy()
out[mask] = sharp[mask]
else:
out = sharp
return np.clip(out, 0, 255).astype(np.uint8)
def _local_contrast_u8(self, rgb_u8: np.ndarray, sigma: float = 9.0, amount: float = 0.14) -> np.ndarray:
img = rgb_u8.astype(np.float32)
blur = cv2.GaussianBlur(img, (0, 0), sigmaX=float(sigma), sigmaY=float(sigma))
return np.clip(img + float(amount) * (img - blur), 0, 255).astype(np.uint8)
def _hybrid_enhance_u8(self, rgb_u8: np.ndarray, preset: str = "balanced") -> np.ndarray:
preset = str(preset or "balanced").lower()
if preset == "soft":
wb, gamma, clahe, den, lc, us = 0.35, 0.96, 1.35, 1.8, 0.08, 0.55
elif preset == "strong":
wb, gamma, clahe, den, lc, us = 0.65, 0.90, 2.25, 3.2, 0.22, 1.25
else:
wb, gamma, clahe, den, lc, us = 0.50, 0.94, 1.75, 2.5, 0.14, 0.85
out = self._gray_world_wb_u8(rgb_u8, strength=wb)
out = self._apply_gamma_u8(out, gamma=gamma)
out = self._clahe_luminance_u8(out, clip_limit=clahe, tile_grid_size=8)
out = self._denoise_fast_u8(out, strength=den)
out = self._local_contrast_u8(out, sigma=9.0, amount=lc)
out = self._unsharp_u8(out, sigma=0.85, amount=us, threshold=2)
return out
def apply_rgb_enhancement_to_hwc_float01(
self,
rgb: np.ndarray,
stage: str = "after_decode",
source_kind: str = "raw",
) -> np.ndarray:
"""
Aplica pós-processamento RGB opcional em imagem HWC float32 0..1.
Local correto no pipeline:
- depois do debayer/decode RGB;
- depois do rgb_calibration;
- antes de flatfield native/fusão/crop/resize final.
Isso garante que treinamento e inferência usem exatamente a mesma transformação
quando ambos usam o mesmo module_params.json.
"""
cfg = self._get_rgb_enhancement_config()
self.last_rgb_enhancement_result = {
"enabled": bool(cfg.get("enabled", False)),
"applied": False,
"stage": stage,
"source_kind": source_kind,
"config": cfg,
"warnings": [],
}
if not cfg.get("enabled", False):
return rgb
if source_kind == "preview" and not cfg.get("apply_to_preview_input", False):
self.last_rgb_enhancement_result["warnings"].append("skipped_preview_input")
return rgb
if rgb is None or not isinstance(rgb, np.ndarray) or rgb.ndim != 3 or rgb.shape[2] != 3:
self.last_rgb_enhancement_result["warnings"].append(
f"invalid_rgb_shape:{None if rgb is None else rgb.shape}"
)
return rgb
t0 = time.perf_counter()
backend = cfg.get("backend", "none")
preset = cfg.get("preset", "balanced")
if backend == "hybrid":
if bool(cfg.get("use_u8_pipeline", True)):
rgb_u8 = self._rgb_float01_to_u8_for_enhancement(rgb, cfg)
out_u8 = self._hybrid_enhance_u8(rgb_u8, preset=preset)
out = out_u8.astype(np.float32) / 255.0
else:
# Caminho defensivo. Hoje mantemos o u8 como padrão porque é
# exatamente o mesmo tipo de operação usado no script visual.
rgb_u8 = self._rgb_float01_to_u8_for_enhancement(rgb, cfg)
out_u8 = self._hybrid_enhance_u8(rgb_u8, preset=preset)
out = out_u8.astype(np.float32) / 255.0
else:
raise ValueError(f"RGB enhancement backend não suportado: {backend}")
if bool(cfg.get("clip_output", True)):
np.clip(out, 0.0, 1.0, out=out)
dt_ms = (time.perf_counter() - t0) * 1000.0
self.last_rgb_enhancement_result.update({
"applied": True,
"backend": backend,
"preset": preset,
"time_ms": float(dt_ms),
"input_shape": list(rgb.shape),
"output_shape": list(out.shape),
"output_dtype": str(out.dtype),
})
return out.astype(np.float32, copy=False)
def build_training_rgb(
self,
raw16: np.ndarray,
@ -585,8 +915,19 @@ class RawProcessorCore:
g = g * float(gains.get("G", 1.0))
b = b * float(gains.get("B", 1.0))
chw = np.stack([r, g, b], axis=0).astype(np.float32)
chw = np.clip(chw, 0.0, 1.0)
rgb_hwc = np.stack([r, g, b], axis=2).astype(np.float32)
np.clip(rgb_hwc, 0.0, 1.0, out=rgb_hwc)
# Pós-processamento RGB opcional.
# Aplicado aqui para o caminho de treino/offline que chama build_training_rgb().
rgb_hwc = self.apply_rgb_enhancement_to_hwc_float01(
rgb_hwc,
stage="build_training_rgb.after_decode",
source_kind="raw",
)
chw = np.transpose(rgb_hwc, (2, 0, 1)).astype(np.float32, copy=False)
np.clip(chw, 0.0, 1.0, out=chw)
if output_dtype == "float32":
return chw
@ -639,9 +980,34 @@ class RawProcessorCore:
cam_id = meta.get("cam_id") or meta.get("camera_id") or meta.get("id") or role
if role == "rgb":
# Caminho offline: se o RGB veio como RAW16 Bayer 2D,
# monta RGB de treino usando a mesma configuração de rgb_processing.
if isinstance(data, np.ndarray) and data.ndim == 2:
rgb_chw = self.build_training_rgb(
data,
output_dtype="float32",
bit_depth=bit_depth,
)
rgb_img = np.transpose(rgb_chw, (1, 2, 0)).astype(np.float32, copy=False)
# Compatibilidade: se já veio HWC RGB processado.
elif isinstance(data, np.ndarray) and data.ndim == 3 and data.shape[2] == 3:
rgb_img = data.astype(np.float32)
if rgb_img.max() > 1.5:
rgb_img /= 255.0
rgb_img = np.clip(rgb_img, 0.0, 1.0)
rgb_img = self.apply_rgb_enhancement_to_hwc_float01(
rgb_img,
stage="decode_bins_cameras.preview_or_processed_input",
source_kind="preview",
)
else:
raise RuntimeError(f"RGB offline inválido em {cam_id}: shape={getattr(data, 'shape', None)}")
decoded[cam_id] = {
"name": "RGB",
"image": data.astype(np.float32) / max_val,
"image": rgb_img,
"meta": meta,
}
@ -798,6 +1164,14 @@ class RawProcessorCore:
else:
raise ValueError(f"rgb_processing.mode inválido: {rgb_mode}")
# Pós-processamento RGB opcional.
# Este é o caminho principal de inferência RAW_BRUTO.
rgb_hwc = self.apply_rgb_enhancement_to_hwc_float01(
rgb_hwc,
stage="decode_stream_cameras.after_raw_decode",
source_kind="raw",
)
decoded[cam_id] = {
"name": "RGB",
"role": "rgb",
@ -812,10 +1186,21 @@ class RawProcessorCore:
rgb = data[:, :, ::-1].astype(np.float32) / 255.0
rgb = np.clip(rgb, 0.0, 1.0)
# Por padrão não mexe em preview/RGB já processado.
# Se quiser aplicar também neste caminho, use:
# rgb_processing.enhancement.apply_to_preview_input=true
rgb = self.apply_rgb_enhancement_to_hwc_float01(
rgb,
stage="decode_stream_cameras.preview_input",
source_kind="preview",
)
decoded[cam_id] = {
"name": "RGB",
"role": "rgb",
"image": np.clip(rgb, 0.0, 1.0),
"image": rgb,
"meta": cam_meta,
}

View File

@ -190,6 +190,18 @@ class MultiSpecSegformerService:
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
#sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
#sess_options.intra_op_num_threads = 1
#sess_options.inter_op_num_threads = 1
#sess_options.add_session_config_entry(
# "session.intra_op.allow_spinning",
# "0",
#)
#sess_options.add_session_config_entry(
# "session.inter_op.allow_spinning",
# "0",
#)
trt_cache_dir = str(self.config.get("trt_cache_dir", "trt_engine_cache"))
trt_fp16 = bool(self.config.get("trt_fp16", True))

View File

@ -2,11 +2,27 @@ import sys
import os
import time
# ============================================================
# Controle de paralelismo nativo
# Precisa ser definido antes de importar NumPy, OpenCV,
# Numba, ONNX Runtime ou módulos que os importem internamente.
# ============================================================
os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE")
os.environ.setdefault("KMP_BLOCKTIME", "0")
# Limita kernels Numba, caso o Visual Worker utilize Numba
# direta ou indiretamente.
os.environ.setdefault("NUMBA_NUM_THREADS", "4")
def main():
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
print("🔎 Caminho sys.path:", os.path.abspath(os.path.join(os.path.dirname(__file__), "..")), "VisualWorker")
# Importar e configurar OpenCV antes dos módulos do Visual Worker,
# pois eles podem carregar OpenCV internamente.
import cv2
cv2.setNumThreads(2)
cv2.ocl.setUseOpenCL(False)
from shared.enums import VisualWorkerCommandType, TipoFrameCamera
from shared.utils import encode_image_base64
from visual_worker.config import mostrar_log, get_camera_manager, iniciar_camera_manager

View File

@ -863,13 +863,14 @@ class CameraManager:
def _iniciar_loop_inferencia(self, freq=25.0):
def loop():
periodo = 1.0 / max(float(freq), 0.1)
while True:
t0_wall = time.time()
t_loop0 = time.perf_counter()
try:
if self.iniciando:
time.sleep(0.05)
time.sleep(0.02)
continue
if not self.operante or self.camera is None:
@ -882,15 +883,24 @@ class CameraManager:
if tensor5 is None or tensor_ts is None:
self.perf.inc("infer_sem_tensor_novo")
time.sleep(0.005)
# Espera curta, sem aplicar novamente um período inteiro.
time.sleep(0.001)
continue
t_inf0 = time.perf_counter()
predictions = self.model_svc.infer_tensor_fast(tensor5, keep_probs=False)
predictions = self.model_svc.infer_tensor_fast(
tensor5,
keep_probs=False,
)
t_inf1 = time.perf_counter()
infer_ms = (t_inf1 - t_inf0) * 1000.0
infer_full = getattr(self.model_svc, "_ultimo_predictions_full", {}) or {}
infer_full = getattr(
self.model_svc,
"_ultimo_predictions_full",
{},
) or {}
infer_forward_ms = infer_full.get(
"forward_ms",
@ -941,15 +951,24 @@ class CameraManager:
infer_gpu_ms=float(infer_forward_ms or 0.0),
tensor_ts=tensor_ts,
frame_ts=pred_ts,
idade_tensor_ms=(time.time() - tensor_ts) * 1000.0 if tensor_ts else None,
idade_tensor_ms=(
(time.time() - tensor_ts) * 1000.0
if tensor_ts else None
),
)
except Exception as e:
self.mostrar_log(f"❌ Erro no loop_inferencia weed: {e}")
# Só limita depois de uma inferência realmente executada.
gasto = time.perf_counter() - t_loop0
restante = periodo - gasto
finally:
lat = time.time() - t0_wall
time.sleep(max(0.0, (1.0 / freq) - lat))
if restante > 0:
time.sleep(restante)
except Exception as e:
self.mostrar_log(
f"❌ Erro no loop_inferencia weed: {e}"
)
time.sleep(0.02)
threading.Thread(target=loop, daemon=True).start()
@ -976,7 +995,7 @@ class CameraManager:
if pred_cache is None:
self.perf.inc("det_sem_prediction_nova")
time.sleep(0.005)
time.sleep(0.001)
continue
predictions = pred_cache.get("predictions")

View File

@ -60,7 +60,7 @@ WEED_DEFAULT_CONFIG = {
"deteccao_fps": 25.0,
# Supervisor/performance. Não precisa ser igual ao pipeline.
"analise_fps": 15.0,
"analise_fps": 10.0,
# Publicação Redis. Mantém baixo para não virar ruído.
"publicacao_fps": 5.0,
@ -73,10 +73,10 @@ WEED_DEFAULT_CONFIG = {
"camera_height": 800,
# FPS solicitado na câmera/OAK. Pode ser maior que o pipeline.
"camera_fps": 40,
"camera_fps": 25,
# Tamanho final do tensor entregue ao modelo: [W, H].
"ia_resolution": [1024, 640],
"ia_resolution": [640, 400],
# Ordem oficial do tensor multiespectral.
# Deve bater com o modelo ONNX exportado.

View File

@ -2,11 +2,28 @@ import sys
import os
import time
# ============================================================
# Controle de paralelismo nativo
# Precisa ser definido antes de importar NumPy, OpenCV,
# Numba, ONNX Runtime ou módulos que os importem internamente.
# ============================================================
os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE")
os.environ.setdefault("KMP_BLOCKTIME", "0")
# Limita kernels Numba, caso o Visual Worker utilize Numba
# direta ou indiretamente.
os.environ.setdefault("NUMBA_NUM_THREADS", "4")
def main():
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
print("🔎 Caminho sys.path:", os.path.abspath(os.path.join(os.path.dirname(__file__), "..")), "WeedWorker")
# Importar e configurar OpenCV antes dos módulos do Visual Worker,
# pois eles podem carregar OpenCV internamente.
import cv2
cv2.setNumThreads(2)
cv2.ocl.setUseOpenCL(False)
from weed_worker.config import mostrar_log, get_camera_manager, iniciar_camera_manager
from shared.enums import WeedWorkerCommandType, TipoFrameCamera
from shared.utils import encode_image_base64

View File

@ -8,7 +8,7 @@ Exporta o checkpoint PyTorch do SegFormer Multi-Head OAK-FCC-3 para ONNX.
Exemplo:
python _10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/target_teached/stacked_raw5/best_score.pt --out backup/segformer_b1/target_teached/stacked_raw5/best_score.onnx --train-script _8_train_multihead.py --opset 17 --device cuda
python _10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/target_teached/stacked_raw5/best_score.pt --out backup/segformer_b1/target_teached/stacked_raw5/best_score.onnx --train-script _8_train_multihead.py --opset 17 --device cuda --include-norm --postprocess argmax_fullres
"""
from __future__ import annotations