Implementacao das cameras e segformer

This commit is contained in:
Diego Freitas 2026-02-24 10:38:14 -03:00
parent 5e9155c3ed
commit ffeeafcca7
55 changed files with 9395 additions and 278 deletions

6
.gitignore vendored
View File

@ -64,6 +64,12 @@ Python/OAK/datasets/oak-d/backup/
Python/OAK/datasets/gal5000/dataset/
Python/OAK/datasets/gal5000/backup/
Python/yolov8-seg/__pycache__/
Python/yolov8-seg/venv/
Python/yolov8-seg/images/
Python/yolov8-seg/labels/
Python/yolov8-seg/runs/
!AgroBase/AgroBase/bin/x64/Debug/Python/
AgroBase/AgroBase/bin/x64/Debug/Python/venv/

View File

@ -360,5 +360,17 @@
MQ135 = 2,
}
public enum TipoFrameCamera
{
Rgb = 0,
Segmentacao = 1,
Overlay = 2,
Debug = 3,
Heatmap = 4,
MatrizCusto = 5,
Deteccoes = 6,
Corredor = 7,
}
}
}

View File

@ -69,7 +69,7 @@ namespace AgroBase.Models
return bmp;
}
public void SaveFrames(List<CameraFrameType> frames, string nome, string caminho)
public void SaveFrames(List<TipoFrameCamera> frames, string nome, string caminho)
{
switch (Dispositivo)
{

View File

@ -1546,7 +1546,8 @@ namespace AgroBase.Models
{
RedisService.AtualizarCampos(
RedisService.CamKey(Variaveis.OperacaoEmAndamento.DispSen.Dados.CameraCaminho.Id),
("streaming", controleBase.Tecla == BotoesJoystick.L1)
("streaming", controleBase.Tecla == BotoesJoystick.L1),
("frame_type", (TipoFrameCamera)controleBase._comp_value)
);
return;
}
@ -1555,7 +1556,8 @@ namespace AgroBase.Models
{
RedisService.AtualizarCampos(
RedisService.CamKey(Variaveis.OperacaoEmAndamento.DispSen.Dados.CamerasSolo.FirstOrDefault(x => !string.IsNullOrEmpty(x.Id)).Id),
("streaming", controleBase.Tecla == BotoesJoystick.L1)
("streaming", controleBase.Tecla == BotoesJoystick.L1),
("frame_type", (TipoFrameCamera)controleBase._comp_value)
);
return;
}
@ -1879,7 +1881,7 @@ namespace AgroBase.Models
string nome = Variaveis.OperacaoEmAndamento.idxLog.ToString();
Camera.SaveFrames(new List<CameraFrameType>() { CameraFrameType.Rgb, CameraFrameType.Segmentacao }, nome, Caminho);
Camera.SaveFrames(new List<TipoFrameCamera>() { TipoFrameCamera.Rgb, TipoFrameCamera.Segmentacao }, nome, Caminho);
var cameras_conectadas = JsonConvert.DeserializeObject<Dictionary<string, object>>(RedisService.Get(CtxKey.DadosCameras));
bool conectada = cameras_conectadas.ContainsKey(Camera?.Id ?? "");
@ -1903,7 +1905,7 @@ namespace AgroBase.Models
string nome = Variaveis.OperacaoEmAndamento.idxLog.ToString();
Camera.SaveFrames(new List<CameraFrameType>() { CameraFrameType.Rgb, CameraFrameType.Segmentacao }, nome, Caminho); // CameraFrameType.Heatmap, CameraFrameType.RadarTopDown
Camera.SaveFrames(new List<TipoFrameCamera>() { TipoFrameCamera.Rgb, TipoFrameCamera.Segmentacao }, nome, Caminho); // CameraFrameType.Heatmap, CameraFrameType.RadarTopDown
var cameras_conectadas = JsonConvert.DeserializeObject<Dictionary<string, object>>(RedisService.Get(CtxKey.DadosCameras));
bool conectada = cameras_conectadas.ContainsKey(Camera?.Id ?? "");

View File

@ -183,9 +183,12 @@ namespace AgroBase.Services.Operadores
("camera_solo_id", Variaveis.OperacaoEmAndamento.DispSen?.Dados?.CamerasSolo?.FirstOrDefault()?.Id ?? ""),
("path_ia_model_ruas_seg", VersionamentoService.ArquivoModeloSegStreetDetector.CaminhoCompleto),
("path_ia_labelmap_ruas_seg", VersionamentoService.ArquivoLabelmapSegStreetDetector.CaminhoCompleto),
("ia_backbone_ruas_seg", "nvidia/segformer-b0-finetuned-ade-512-512"),
("path_ia_model_ruas_det", VersionamentoService.ArquivoModeloDetStreetDetector.CaminhoCompleto),
("path_ia_model_ervas", VersionamentoService.ArquivoModeloWeedDetector.CaminhoCompleto),
("path_ia_labelmap_ervas", VersionamentoService.ArquivoModeloLabelmapWeedDetector.CaminhoCompleto),
("path_ia_norm_stats_ervas", VersionamentoService.ArquivoModeloNormstatsWeedDetector.CaminhoCompleto),
("ia_backbone_ervas", "nvidia/segformer-b1-finetuned-ade-512-512"),
("angulo_roll_max", VariaveisEquipamento.AnguloInclinacaoRollMax),
("angulo_pitch_max", VariaveisEquipamento.AnguloInclinacaoPitchMax)
);
@ -745,6 +748,18 @@ namespace AgroBase.Services.Operadores
("last_ec", DadosCan.UltimoErroCritico)
);
double MinOrDefault(List<double> items)
{
if (items.Count == 0) return 0;
return items.Min();
}
double MaxOrDefault(List<double> items)
{
if (items.Count == 0) return 0;
return items.Max();
}
var DadosBateria = DalyBMSService.DadosLeitura;
RedisService.AtualizarCampos(
RedisService.ModKey(T_Code.Bat), // ajusta o T_Code se o seu enum tiver outro nome
@ -764,8 +779,8 @@ namespace AgroBase.Services.Operadores
("cap_restante_ah", DadosBateria.Pack.CapacidadeRestante_Ah),
("ciclos_aproximados", DadosBateria.Pack.CiclosAproximados),
// Tensão de células
("tensao_min_celula_v", DadosBateria.TensaoMin > 0 ? DadosBateria.TensaoMin : DadosBateria.Celulas.Min(c => c.Tensao_V)),
("tensao_max_celula_v", DadosBateria.TensaoMax > 0 ? DadosBateria.TensaoMax : DadosBateria.Celulas.Max(c => c.Tensao_V)),
("tensao_min_celula_v", DadosBateria.TensaoMin > 0 ? DadosBateria.TensaoMin : MinOrDefault(DadosBateria.Celulas.Select(c => c.Tensao_V).ToList())),
("tensao_max_celula_v", DadosBateria.TensaoMax > 0 ? DadosBateria.TensaoMax : MaxOrDefault(DadosBateria.Celulas.Select(c => c.Tensao_V).ToList())),
("idx_celula_min", DadosBateria.Celulas.FirstOrDefault(c => c.EhMinima)?.Index ?? 0),
("idx_celula_max", DadosBateria.Celulas.FirstOrDefault(c => c.EhMaxima)?.Index ?? 0),
// Temperaturas

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static AgroBase.Models.Enums;
using static AgroBase.Models.Operadores.OperadoresModels;
namespace AgroBase.Services.Operadores
@ -125,7 +126,7 @@ namespace AgroBase.Services.Operadores
return _frame;
}
public static void SaveCameraFrames(List<CameraFrameType> frames, string nome, string caminho)
public static void SaveCameraFrames(List<TipoFrameCamera> frames, string nome, string caminho)
{
var comando = new
{

View File

@ -62,7 +62,7 @@ namespace AgroBase.Services.Operadores
bool comandoMudou = false;
var _Controle = Variaveis.OperacaoEmAndamento.Controle;
foreach (var bico in _Controle.Bicos)
foreach (var bico in _Controle.Bicos ?? new List<Models.Modules.AtuadorBicoModel>())
{
novoComando.TryGetValue(bico.Posicao, out bool atuado);
if (!(Variaveis.OperacaoEmAndamento.DispAtu?.Dados?.ContadorCarga?.PulverizadorLiberado ?? false)) atuado = false;
@ -107,7 +107,7 @@ namespace AgroBase.Services.Operadores
break;
case WeedWorkerCommandType.EnviarDadosControle:
if (Variaveis.OperacaoEmAndamento.Parametros.Controle.PulverizadorAutomatico)
if (Variaveis.OperacaoEmAndamento.Parametros.Controle?.PulverizadorAutomatico ?? false)
{
AtualizarControle(comando.@params);
}
@ -153,7 +153,7 @@ namespace AgroBase.Services.Operadores
return _frame;
}
public static void SaveCameraFrames(List<CameraFrameType> frames, string nome, string caminho)
public static void SaveCameraFrames(List<TipoFrameCamera> frames, string nome, string caminho)
{
var comando = new
{

View File

@ -72,6 +72,16 @@ namespace AgroBase.Services
}
}
}
public static VersaoArquivoModel ArquivoModeloNormstatsWeedDetector
{
get
{
lock (_ArquivoLock)
{
return _ArquivosVersionados.Where(x => x.TipoArquivo == TipoArquivoVersionado.ModeloIA_WeedDetector).Skip(2).FirstOrDefault();
}
}
}
public static VersaoArquivoModel ArquivoParametros(T_Code Dispositivo)
{
lock (_ArquivoLock)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,19 +3,19 @@
"id": 1,
"Arquivo": "model",
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".blob",
"Versao": "2_1",
"Extensao": ".pt",
"Versao": "3_1",
"TipoArquivo": 1,
"ArquivoDownload": "models/weed_detector_model-2_1.blob"
"ArquivoDownload": "models/weed_detector_model-3_1.pt"
},
{
"id": 2,
"Arquivo": "model",
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".txt",
"Versao": "2_1",
"Versao": "3_1",
"TipoArquivo": 1,
"ArquivoDownload": "models/weed_detector_labelmap-2_1.txt"
"ArquivoDownload": "models/weed_detector_labelmap-3_1.txt"
},
{
"id": 3,
@ -143,4 +143,13 @@
"TipoArquivo": 14,
"ArquivoDownload": "modelo_3d-1_0.mtl"
},
{
"id": 17,
"Arquivo": "model",
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".json",
"Versao": "1_1",
"TipoArquivo": 1,
"ArquivoDownload": "models/weed_detector_normstats-1_1.json"
},
]

View File

@ -0,0 +1,350 @@
import numpy as np
import time
from camera_worker.raw_segformer_service import make_bgr_preview_from_raw
from shared.enums import StatusModulo, T_Code, TipoFrameCamera
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
from camera_worker.gal_service import GalService
import subprocess
import cv2
GST_LAUNCH = r"C:\Program Files\gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe"
class CameraGal:
def __init__(self, mostrar_log, mx_id, raw_w=None, raw_h=None):
self.mx_id = mx_id
self.mostrar_log = mostrar_log
self.gst_proc = None
self.gst_WIDTH = 640
self.gst_HEIGHT = 360
self.gst_BIT_RATE = 500
self._ultimo_envio_gst = 0.0
self.dispositivo = T_Code.Vzo
self.modelo = "Desconhecido"
self.ultima_saude = {}
self._raw_cache_max_age = 0.25
self._rgb_cache_max_age = 0.25
self.timestamp_ultimo_frame_rgb = None
self.timestamp_ultima_segmentacao = None
self.timestamp_ultimo_frame_raw = None
self.ultimo_frame_raw = None
self.ultimo_frame_rgb = None
self.rodando = False
self.iniciado = False
self.cam = GalService(raw_w=raw_w, raw_h=raw_h)
if not self.cam:
return
#raise RuntimeError(f"Dispositivo com mxid {mx_id} não encontrado")
self.cam.open()
info = self.cam.get_device_info()
#self.mostrar_log(f"Status inicial: {self.cam.get_status()}")
#self.mostrar_log(f"Device info: {info}")
self.cam.configure_fps(30)
self.cam.start_streaming()
self.dispositivo = T_Code.Cam
self.modelo = info.get("model")
self.versao = info.get("version_word")
# Criar processo para transmissao de video
try:
_cfg = ContextoGlobalRedis.get_equipamento() or {}
_ip = _cfg.get("base_ip")
_porta = _cfg.get("base_porta_ervas")
if _ip is not None and _porta is not None:
self.mostrar_log(f"Iniciando GStreamer para {_ip}:{_porta}...")
gst_cmd = [
GST_LAUNCH,
"fdsrc", "fd=0",
"!", "videoparse",
f"width={self.gst_WIDTH}", f"height={self.gst_HEIGHT}",
"format=bgr",
f"framerate={self.gst_FPS}/1",
"!", "videoconvert",
"!", "videoscale",
"!", f"video/x-raw,width={self.gst_WIDTH},height={self.gst_HEIGHT}",
"!", "x264enc", "tune=zerolatency", "speed-preset=ultrafast",
f"bitrate={self.gst_BIT_RATE}", "key-int-max=20",
"!", "rtph264pay", "config-interval=-1", "pt=96",
#"!", "h264parse", "!", "mpegtsmux",
"!", "udpsink",
f"host={_ip}", f"port={_porta}",
"sync=false", "async=false",
]
self.gst_proc = subprocess.Popen(
gst_cmd,
stdin=subprocess.PIPE,
)
except Exception as e:
self.gst_proc = None
self.mostrar_log(f"Erro ao criar script de transmissao de video: {e}")
# Fase 2: criar pipeline e instanciar normalmente
try:
rgb_width = 2592
rgb_height = 2056
altura_camera = 116
fov_h = 72.2
fov_v = 57.9
largura_real = 2 * altura_camera * np.tan(np.radians(fov_h) / 2)
altura_real = 2 * altura_camera * np.tan(np.radians(fov_v) / 2)
cm_por_px_x = largura_real / rgb_width
cm_por_px_y = altura_real / rgb_height
self.parametros = {
"raw_w": raw_w,
"raw_h": raw_h,
"rgb_width": rgb_width,
"rgb_height": rgb_height,
"fov_h": np.radians(fov_h),
"fov_v": np.radians(fov_v),
"altura_camera": altura_camera,
"largura_real": largura_real,
"altura_real": altura_real,
"cm_por_px_x": cm_por_px_x,
"cm_por_px_y": cm_por_px_y,
}
self.ultima_saude["timestamp"] = time.time()
self.ultima_saude["status"] = StatusModulo.OPERANTE.value
self.iniciado = True
except Exception as e:
self.mostrar_log(f"Erro ao iniciar camera: {e}")
pass
#ContextoGlobalRedis.set(ContextoGlobalRedis.CamKey(self.mx_id), _camera)
def requisitar_frame_raw(self, force: bool = False, max_age_s: float = None):
"""
Se force=False, reaproveita o último frame se ainda for "novo".
Se force=True, sempre busca um frame novo na câmera.
"""
try:
agora = time.time()
if max_age_s is None:
max_age_s = self._raw_cache_max_age
# Se NÃO for forçado e tem frame recente em cache, reaproveita
if (not force
and self.ultimo_frame_raw is not None
and self.timestamp_ultimo_frame_raw is not None
and (agora - self.timestamp_ultimo_frame_raw) < max_age_s):
return self.ultimo_frame_raw
# Caso contrário, busca frame novo
raw4_base, dbg = self.cam.grab_raw4(
out_h=self.parametros["raw_h"],
out_w=self.parametros["raw_w"],
timeout_ms=500
)
self.ultimo_frame_raw = raw4_base
self.timestamp_ultimo_frame_raw = agora
return raw4_base
except Exception as e:
self.mostrar_log(f"Erro ao requisitar frame raw: {e}")
return None
def requisitar_frame_rgb(self, force: bool = False, max_age_s: float = None):
"""
- Se force=False:
* Se existir RGB recente em cache, devolve ele (barato).
* Se estiver "velho" demais, recalcula a partir do raw (sem necessariamente ir na câmera).
- Se force=True:
* Recalcula RGB agora, usando o raw mais novo possível.
"""
try:
agora = time.time()
if max_age_s is None:
max_age_s = self._rgb_cache_max_age
# 1) Se NÃO for forçado e tenho frame RGB "fresco", só devolvo ele
if (not force
and self.ultimo_frame_rgb is not None
and self.timestamp_ultimo_frame_rgb is not None
and (agora - self.timestamp_ultimo_frame_rgb) < max_age_s):
dur = 0.0 # basicamente só acesso memória
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": True
}
return self.ultimo_frame_rgb, resultado
# 2) Se preciso atualizar o RGB agora
start = time.time()
# Aqui a sacada: NÃO precisa obrigatoriamente ir na câmera.
# Usa o raw em cache (que a segmentação acabou de usar) se possível.
raw4_base = self.requisitar_frame_raw(force=False)
if raw4_base is None:
raise RuntimeError("raw4_base veio None em requisitar_frame_rgb")
frame = make_bgr_preview_from_raw(raw4_base, rgirb=True, preview_fast=True)
self.ultimo_frame_rgb = frame
self.timestamp_ultimo_frame_rgb = time.time()
dur = self.timestamp_ultimo_frame_rgb - start
self.enviar_frame_stream(frame)
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": frame is not None and frame.size > 0
}
return frame, resultado
except Exception as e:
self.mostrar_log(f"Erro ao requisitar frame RGB: {e}")
return None, {
"erro": str(e),
"duracao": 0,
"frame_valido": False
}
def enviar_frame_stream(self, frame):
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
if self.gst_proc is not None and _stream_on:
self._ultimo_envio_gst = time.time()
if frame is not None and frame.size > 0:
if frame.shape[1] != self.gst_WIDTH or frame.shape[0] != self.gst_HEIGHT:
frame_stream = cv2.resize(frame, (self.gst_WIDTH, self.gst_HEIGHT), interpolation=cv2.INTER_AREA)
else:
frame_stream = frame
try:
self.gst_proc.stdin.write(frame_stream.tobytes())
except BrokenPipeError:
self.mostrar_log("GStreamer fechou o pipe. Encerrando.")
def atualizar_saude(self):
# 1) Ver se “existe” no contexto e se o handle da GAL está aberto
ctx_cam = ContextoGlobalRedis.get_cameras().get(self.mx_id) or {}
conectado = (self.cam is not None and self.cam.handle is not None and ctx_cam is not None)
saude = 50
motivos: list[str] = []
performance = {
"temperatura": 0.0,
"memoria_usada": 0.0, # GAL não é um mini-PC, então deixamos 0
"executando": False,
"velocidade": "",
}
resultado = {
"erro": None,
"duracao": 0.0,
"frame_valido": False,
}
# 2) Se estiver conectado, tentamos:
# - ler temperatura (se existir)
# - pegar 1 frame RGB de teste
if conectado:
try:
# Temperatura, se o SDK expuser
temp = None
try:
#temp = self.cam.get_temperature_c()
pass
except Exception:
temp = None
if temp is not None:
performance["temperatura"] = float(temp)
# Penalizações de temperatura (ajusta como quiser)
if temp >= 80:
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
saude -= 30
elif temp >= 70:
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
saude -= 15
elif temp >= 60:
motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C")
saude -= 5
# Frame de teste
frame, resultado = self.requisitar_frame_rgb()
performance["executando"] = bool(resultado["frame_valido"])
if resultado["duracao"] > 0:
fps = 1.0 / resultado["duracao"]
performance["velocidade"] = f"{fps:.1f} FPS"
except Exception as e:
self.mostrar_log(f"[{self.mx_id}] Erro ao atualizar saúde da GAL: {e}")
resultado = {
"erro": str(e),
"duracao": 0.0,
"frame_valido": False,
}
# Se for erro de comunicação, podemos marcar como desconectado
if "Communication" in str(e) or "X_LINK_ERROR" in str(e) or "falhou, ret=" in str(e):
conectado = False
# 3) Regras de saúde baseadas no resultado do frame
if not conectado:
motivos.append("desconectado")
saude = 0
elif resultado["erro"]:
motivos.append(resultado["erro"])
saude = 0
elif not resultado["frame_valido"]:
motivos.append("Frame inválido ou vazio")
saude = 0
else:
# Frame ok → bônus grosso
saude += 50
# Latência muito alta penaliza
if resultado["duracao"] > 1.0:
saude -= 20
motivos.append(f"Tempo elevado para captura: {resultado['duracao']:.2f}s")
elif resultado["duracao"] > 0.4:
saude -= 10
motivos.append(f"Tempo moderado para captura: {resultado['duracao']:.2f}s")
saude = min(max(saude, 0), 100)
# 4) Status semafórico
status = StatusModulo.OPERANTE
if not conectado:
status = StatusModulo.DESCONECTADO
elif saude <= 0:
status = StatusModulo.FALHA
elif saude < 80:
status = StatusModulo.ALERTA
agora = time.time()
# 5) “rodando” baseado em timestamp do último frame
timeout = 2.0
ts_rgb = self.timestamp_ultimo_frame_rgb or 0
self.rodando = conectado and ((agora - ts_rgb) <= timeout)
saude_geral = {
"timestamp": agora,
"conectado": conectado,
"status": status.value,
"saude": saude,
"motivos": motivos,
"saude_individual": [], # se quiser detalhar por componente depois
}
self.ultima_saude = saude_geral
# 6) Enviar pro health_worker
from camera_worker.manager import definir_saude_camera
definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo)

View File

@ -3,7 +3,7 @@ import depthai as dai
import time
from health_worker.modulos.imu import IMUCamera
from shared.enums import StatusModulo, T_Code
from shared.enums import StatusModulo, T_Code, TipoFrameCamera
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
import subprocess
@ -19,7 +19,6 @@ class CameraOak:
self.gst_proc = None
self.gst_WIDTH = 640
self.gst_HEIGHT = 360
self.gst_FPS = 10
self.gst_BIT_RATE = 500
self._ultimo_envio_gst = 0.0
@ -394,31 +393,7 @@ class CameraOak:
#self.timestamp_ultimo_frame_rgb = time.time()
dur = time.time() - start
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
if self.gst_proc is not None and _stream_on:
# --- CONTROLE DE FPS PARA STREAMING ---
enviar_para_gst = True
if getattr(self, "gst_FPS", None) and self.gst_FPS > 0:
intervalo_min = 1.0 / float(self.gst_FPS)
agora = time.time()
if (agora - self._ultimo_envio_gst) < intervalo_min:
enviar_para_gst = False
else:
self._ultimo_envio_gst = agora
if frame is not None and frame.size > 0 and enviar_para_gst:
# --- RESIZE SÓ PARA O STREAM ---
if frame.shape[1] != self.gst_WIDTH or frame.shape[0] != self.gst_HEIGHT:
frame_stream = cv2.resize(
frame,
(self.gst_WIDTH, self.gst_HEIGHT),
interpolation=cv2.INTER_AREA
)
else:
frame_stream = frame
try:
self.gst_proc.stdin.write(frame_stream.tobytes())
except BrokenPipeError:
self.mostrar_log("GStreamer fechou o pipe. Encerrando.")
self.enviar_frame_stream(frame)
resultado = {
"erro": None,
@ -436,6 +411,20 @@ class CameraOak:
"frame_valido": False
}
def enviar_frame_stream(self, frame):
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
if self.gst_proc is not None and _stream_on:
self._ultimo_envio_gst = time.time()
if frame is not None and frame.size > 0:
if frame.shape[1] != self.gst_WIDTH or frame.shape[0] != self.gst_HEIGHT:
frame_stream = cv2.resize(frame, (self.gst_WIDTH, self.gst_HEIGHT), interpolation=cv2.INTER_AREA)
else:
frame_stream = frame
try:
self.gst_proc.stdin.write(frame_stream.tobytes())
except BrokenPipeError:
self.mostrar_log("GStreamer fechou o pipe. Encerrando.")
def requisitar_frame_depth(self):
if not self.tem_depth:
return None, {

View File

@ -0,0 +1,996 @@
# gal5000_camera.py
# Driver da câmera GAL5000-60ucNIR com:
# - abertura/fechamento
# - captura RAW8 mosaic
# - conversão para RAW4 normalizado
# - autoexposure (exposição + ganhos)
#
# Uso típico:
#
# from gal5000_camera import Gal5000Camera
#
# cam = Gal5000Camera(
# dll_dir=r"C:\ZendionInc\agrobot_base\Python\gal5000\dlls",
# raw_w=2592,
# raw_h=2056,
# )
# with cam:
# raw4, dbg = cam.grab_raw4(512, 512)
# # raw4 = np.ndarray (4,512,512) float32 em 0..1
# # dbg = dict com exp_raw, gain_a, gain_d, p95 etc.
import ctypes
import os
import math
import time
import ctypes as C
from ctypes import wintypes as W
from collections import deque
import threading
import numpy as np
import cv2
# -----------------------------
# Constantes de parâmetros
# -----------------------------
PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010
PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020
PARAM_ID_SENSOR_GAINDIGITRAW = 0x0000302A
PARAM_ID_SFNC_BINNINGHORIZONTAL = 0x00001119
PARAM_ID_SFNC_BINNINGVERTICAL = 0x0000111B
PARAM_ID_SFNC_DECIMATIONHORIZONTAL = 0x0000111D
PARAM_ID_SFNC_DECIMATIONVERTICAL = 0x0000111F
PARAM_ID_SFNC_ACQUISITIONFRAMERATE = 0x00001208
PARAM_ID_SFNC_ACQUISITIONFRAMERATEENABLE = 0x00001209
PARAM_ID_SFNC_SENSORWIDTH = 0x00001101
PARAM_ID_SFNC_SENSORHEIGHT = 0x00001102
PARAM_ID_SFNC_WIDTH = 0x00001111
PARAM_ID_SFNC_HEIGHT = 0x00001112
PARAM_ID_SFNC_OFFSETX = 0x00001113
PARAM_ID_SFNC_OFFSETY = 0x00001114
BUF_SIZE = 256
VALUE_INT = 0
VALUE_FLOAT = 1
VALUE_STRING = 2
DEVICE_USB = 3
DEVICE_UDEF = 0
DEVICE_INDEX = 0
DATA_RAW = 0
# Limites de exposição em unidades RAW (linhas)
EXP_MIN = 1
EXP_MAX = 20000
EXP_MARGIN = 200 # exemplo, em unidades de exp_raw
# Ganho analógico
GAIN_A_MIN = 0
GAIN_A_MAX = 50
GAIN_A_BASE = 0
# Ganho digital
GAIN_D_MIN = 0
GAIN_D_MAX = 8
# ROI para análise de brilho
ROI_Y0_FRAC = 0.0
ROI_Y1_FRAC = 1.0
ROI_X0_FRAC = 0.0
ROI_X1_FRAC = 1.0
# Alvo de brilho / saturação
TARGET_P95 = 140.0 # alvo de brilho (0..255)
DEADBAND = 6.0 # zona morta
SAT_LIMIT = 0.02 # máx fração de pixels saturados
# Controle log / suavização
K_LOG = 0.12
MAX_STEP = 0.10
EMA_ALPHA = 0.20
class VT_FRAMEINFO(C.Structure):
_fields_ = [
("lFrameID", W.DWORD),
("lBufSize", W.DWORD),
("lWidth", W.DWORD),
("lHeight", W.DWORD),
("lPixBits", C.c_ubyte),
("_pad0", C.c_ubyte * 3),
("pBufPtr", C.POINTER(C.c_ubyte)),
("lFrameStatus", W.DWORD),
("lPixType", W.DWORD),
("lTimeStamp", W.DWORD),
("_reserve", W.DWORD * 8),
]
FRAME_CALLBACK = C.WINFUNCTYPE(
W.DWORD, # retorno
W.HANDLE, # hDev
VT_FRAMEINFO, # frame info (by value)
C.c_void_p, # contexto
)
class VT_DEVPARAM(C.Structure):
_fields_ = [
("bUseName", W.BOOL),
("lParamByID", W.DWORD),
("lParamByName", C.c_char * BUF_SIZE),
]
class VT_DEVINFO_USB(ctypes.Structure):
_fields_ = [
("iDevGlobalIndex", ctypes.c_ubyte),
("cDevVendorName", ctypes.c_char * BUF_SIZE),
("cDevName", ctypes.c_char * BUF_SIZE),
("cDevUID", ctypes.c_char * BUF_SIZE),
("iDevVersion", ctypes.c_ushort),
("iDevPID", ctypes.c_ushort),
("iDevVID", ctypes.c_ushort),
("cDevSN", ctypes.c_char * 20),
("iDevChipIndex", ctypes.c_ubyte),
("iTimeoutImageRequest", ctypes.c_long),
("iTimeoutImageReceived", ctypes.c_long),
]
class VT_DEVINFO_UNION(ctypes.Union):
_fields_ = [
("pDevInfoGEV", ctypes.c_void_p),
("pDevInfoUSS", ctypes.c_void_p),
("pDevInfoUSB", ctypes.POINTER(VT_DEVINFO_USB)),
("pDevInfoU3V", ctypes.c_void_p),
("pDevInfoGRB", ctypes.c_void_p),
("pDevInfoNCM", ctypes.c_void_p),
]
class VT_DEVINFO(ctypes.Structure):
_fields_ = [
("bDevOnline", ctypes.c_int),
("bDevOpen", ctypes.c_int),
("eDevType", ctypes.c_int), # VT_DEVICETYPE
("u", VT_DEVINFO_UNION),
]
# -----------------------------
# Helpers de DLL / parâmetros
# -----------------------------
def _load_gal_dll(dll_dir: str, dll_name: str):
if dll_dir is None:
raise RuntimeError("dll_dir é obrigatório para carregar a VT_SDK64.dll")
os.add_dll_directory(dll_dir)
dll = C.WinDLL(os.path.join(dll_dir, dll_name))
# funções principais
dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int]
dll.VT_DeviceScan.restype = C.c_int
dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int]
dll.VT_DeviceOpen.restype = C.c_int
dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL]
dll.VT_SingleFrameCapture.restype = C.c_int
dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)]
dll.VT_DeviceClose.restype = C.c_int
dll.VT_CaptureStart.argtypes = [W.HANDLE]
dll.VT_CaptureStart.restype = C.c_int
dll.VT_CaptureStop.argtypes = [W.HANDLE]
dll.VT_CaptureStop.restype = C.c_int
dll.VT_SetFrameCallback.argtypes = [W.HANDLE, FRAME_CALLBACK, C.c_void_p, C.c_int]
dll.VT_SetFrameCallback.restype = C.c_int
# parâmetros
dll.VT_ParamGetValue.argtypes = [W.HANDLE, C.POINTER(VT_DEVPARAM), C.c_void_p, C.c_int]
dll.VT_ParamGetValue.restype = C.c_int
dll.VT_ParamSetValue.argtypes = [W.HANDLE, C.POINTER(VT_DEVPARAM), C.c_void_p, C.c_int]
dll.VT_ParamSetValue.restype = C.c_int
dll.VT_DeviceGetInfo.argtypes = [
ctypes.c_void_p, # PVOID pDevDescription
ctypes.POINTER(VT_DEVINFO), # PVT_DEVINFO
ctypes.c_int, # VT_DEVICEACCESSMODE
ctypes.c_int, # VT_DEVICETYPE
]
dll.VT_DeviceGetInfo.restype = ctypes.c_int
return dll
def _ck(ret: int, name: str, debug_mode: bool = False):
if ret != 0:
if (debug_mode):
print(f"{name} falhou, ret={ret}")
raise RuntimeError(f"{name} falhou, ret={ret}")
def _clamp(v, lo, hi):
return lo if v < lo else hi if v > hi else v
def _devparam_by_id(pid: int) -> VT_DEVPARAM:
p = VT_DEVPARAM()
p.bUseName = False
p.lParamByID = pid
p.lParamByName = b""
return p
def _devparam_by_name(name: str) -> VT_DEVPARAM:
p = VT_DEVPARAM()
p.bUseName = True
p.lParamByID = 0 # PARAM_ID_UNDEFINE no header é 0x00000000
encoded = name.encode("ascii")
if len(encoded) >= BUF_SIZE:
encoded = encoded[:BUF_SIZE - 1]
# preenche o array de chars inteiro
buf = encoded + b"\0" * (BUF_SIZE - len(encoded))
p.lParamByName = buf
return p
def _param_get_int(dll, h: W.HANDLE, pid: int) -> int:
p = _devparam_by_id(pid)
v = C.c_int(0)
ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_INT)
_ck(ret, f"VT_ParamGetValue({hex(pid)})")
return int(v.value)
def _param_set_int(dll, h: W.HANDLE, pid: int, value: int):
p = _devparam_by_id(pid)
v = C.c_int(int(value))
ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT)
_ck(ret, f"VT_ParamSetValue({hex(pid)})")
def _param_get_float_by_name(dll, h: W.HANDLE, name: str) -> float:
p = _devparam_by_name(name)
v = C.c_double(0.0)
ret = dll.VT_ParamGetValue(h, p, C.byref(v), VALUE_FLOAT)
_ck(ret, f"VT_ParamGetValue({name})")
return float(v.value)
def _param_get_str_by_name(dll, h: W.HANDLE, name: str) -> str:
p = _devparam_by_name(name)
buf = (C.c_char * BUF_SIZE)()
ret = dll.VT_ParamGetValue(h, p, C.byref(buf), VALUE_STRING)
_ck(ret, f"VT_ParamGetValue({name})")
# converte até o primeiro \0
return C.string_at(buf).decode("ascii", errors="ignore").strip()
def _param_set_float(dll, h: W.HANDLE, pid: int, value: float):
p = _devparam_by_id(pid)
v = C.c_double(float(value))
ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_FLOAT)
_ck(ret, f"VT_ParamSetValue({hex(pid)})")
def _param_set_float_by_name(dll, h: W.HANDLE, name: str, value: float):
p = _devparam_by_name(name)
v = C.c_double(float(value))
ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_FLOAT)
_ck(ret, f"VT_ParamSetValue({name})")
def _gal_open(dll, debug_mode: bool = False) -> W.HANDLE:
n = C.c_ubyte(0)
_ck(dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF), "VT_DeviceScan", debug_mode)
if n.value == 0:
raise RuntimeError("Nenhuma câmera encontrada.")
idx = C.c_ubyte(0)
h = W.HANDLE()
_ck(dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF), "VT_DeviceOpen", debug_mode)
return h
def _gal_close(dll, h: W.HANDLE):
try:
dll.VT_DeviceClose(C.byref(h))
except Exception:
pass
def _gal_capture_raw8_mosaic(dll, h: W.HANDLE, raw_w: int, raw_h: int, timeout_ms: int) -> np.ndarray:
fi = VT_FRAMEINFO()
_ck(dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, timeout_ms, True), "VT_SingleFrameCapture")
w, hh = int(fi.lWidth), int(fi.lHeight)
if (w != raw_w) or (hh != raw_h):
# só avisa, pode mudar ROI e afins
raw_w, raw_h = w, hh
buf = C.string_at(fi.pBufPtr, fi.lBufSize)
arr = np.frombuffer(buf, dtype=np.uint8)
needed = raw_w * raw_h
if arr.size < needed:
arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0)
arr = arr[:needed].reshape(raw_h, raw_w)
return arr
class AEController:
"""
Controlador de Auto Exposure em cima do MOSAIC cru.
Ajusta exposição, e opcionalmente ganho analógico/digital.
"""
def __init__(self,
exp_min=EXP_MIN,
exp_max=EXP_MAX,
target_p95=TARGET_P95,
deadband=DEADBAND,
k=K_LOG,
max_step=MAX_STEP,
ema_alpha=EMA_ALPHA,
sat_limit=SAT_LIMIT,
use_gain=True,
subsample=2):
"""
subsample:
1 -> usa todos os pixels do canal G
2 -> usa 1/4 dos pixels (subamostragem 2x2)
3 -> usa 1/9 dos pixels, etc.
Na prática, 2 costuma ser um ótimo equilíbrio
(muito rápido, métricas quase idênticas).
"""
self.run_each = 0.2
self._last_time = 0.0
self.exp_min = exp_min
self.exp_max = exp_max
self.target = target_p95
self.deadband = deadband
self.k = k
self.max_step = max_step
self.ema_alpha = ema_alpha
self.sat_limit = sat_limit
self.use_gain = use_gain
self.subsample = max(1, int(subsample))
self.p95_ema = None
# Buffers reutilizáveis para o histograma
self._hist = np.zeros(256, dtype=np.int32)
self._cdf = np.zeros(256, dtype=np.int32)
# --------- medição rápida de p90/p95/sat ---------
def _measure_raw_g_metrics_fast(self, mosaic_u8: np.ndarray):
"""
Mede p90, p95 e saturação usando:
- apenas canal G do mosaico
- histogram + CDF
- subamostragem opcional
Retorna: (p90, p95, sat) onde:
p90, p95 em escala 0..255 (float)
sat = fração de pixels saturados (=255) em 0..1
"""
if mosaic_u8.ndim != 2 or mosaic_u8.dtype != np.uint8:
g = np.asarray(mosaic_u8, dtype=np.uint8)
else:
g = mosaic_u8
# Extrai canal G do mosaico:
# padrão:
# R G
# IR B
# então G está em [0::2, 1::2]
g = g[0::2, 1::2]
# Subamostragem espacial opcional
s = self.subsample
if s > 1:
g = g[::s, ::s]
# Histogram 0..255 usando buffer interno
hist = self._hist
hist.fill(0)
# np.add.at acumula contagens sem criar array novo
np.add.at(hist, g.ravel(), 1)
total = int(hist.sum())
if total == 0:
# fallback besta, mas evita divisão por zero
return 0.0, 0.0, 0.0
# CDF no buffer
cdf = self._cdf
np.cumsum(hist, out=cdf)
# índices para 90% e 95% dos pixels
thr90 = 0.90 * total
thr95 = 0.95 * total
idx90 = int(np.searchsorted(cdf, thr90))
idx95 = int(np.searchsorted(cdf, thr95))
# saturação: fração de pixels em 255
sat = hist[255] / float(total)
return float(idx90), float(idx95), float(sat)
# --------- lógica de controle (quase igual a sua) ---------
def step(self,
mosaic_u8: np.ndarray,
exp_raw: int,
gain_a: int,
gain_d: int):
"""
Retorna (new_exp, new_gain_a, new_gain_d, dbg)
"""
# Aqui trocamos a função por uma versão rápida
p90, p95, sat = self._measure_raw_g_metrics_fast(mosaic_u8)
# EMA do p95
if self.p95_ema is None:
self.p95_ema = p95
else:
a = self.ema_alpha
self.p95_ema = (1.0 - a) * self.p95_ema + a * p95
e = self.target - self.p95_ema
# deadband: se está perto do alvo e sem saturação, não mexe
if abs(e) <= self.deadband and sat <= self.sat_limit:
new_gain_a = gain_a
if self.use_gain:
# Relaxar ganho em direção ao baseline quando está tudo ok
if gain_a > GAIN_A_BASE:
new_gain_a = max(GAIN_A_BASE, gain_a - 1)
elif gain_a < GAIN_A_BASE:
new_gain_a = min(GAIN_A_BASE, gain_a + 1)
dbg = {
"p90": p90,
"p95": p95,
"p95_ema": self.p95_ema,
"sat": sat,
"step": 0.0,
"hold": True,
}
self._last_time = time.time()
return exp_raw, new_gain_a, gain_d, dbg
# cálculo do passo em log
if sat > self.sat_limit:
step = -min(self.max_step, 0.12)
else:
ratio = (self.target + 1e-6) / (self.p95_ema + 1e-6)
step = self.k * math.log(ratio)
step = _clamp(step, -self.max_step, +self.max_step)
new_exp = int(round(exp_raw * math.exp(step)))
new_exp = _clamp(new_exp, self.exp_min, self.exp_max)
new_gain_a = gain_a
new_gain_d = gain_d
if self.use_gain:
# 1) Muito escuro e exp no teto -> sobe ganho
if new_exp >= self.exp_max - EXP_MARGIN and self.p95_ema < (self.target - self.deadband):
new_gain_a = _clamp(gain_a + 2, GAIN_A_MIN, GAIN_A_MAX)
# 2) Muito claro e exp no piso -> desce ganho
elif new_exp <= self.exp_min + EXP_MARGIN and (self.p95_ema > (self.target + self.deadband) or sat > self.sat_limit):
new_gain_a = _clamp(gain_a - 2, GAIN_A_MIN, GAIN_A_MAX)
dbg = {
"p90": p90,
"p95": p95,
"p95_ema": self.p95_ema,
"sat": sat,
"step": step,
"hold": False,
}
self._last_time = time.time()
return new_exp, new_gain_a, new_gain_d, dbg
# -----------------------------
# Conversão MOSAIC -> RAW4
# -----------------------------
_m2r_hw = None # (H, W) do mosaico atual (pares)
_m2r_out_hw = None # (out_h, out_w)
_m2r_tmp4_u8 = None # (H2, W2, 4) uint8
_m2r_resized4_u8 = None # (out_h, out_w, 4) uint8
_m2r_raw4_f32 = None # (4, out_h, out_w) float32
def mosaic_to_raw4_resized_buf(
mosaic_u8: np.ndarray,
out_h: int,
out_w: int,
interpolation=cv2.INTER_AREA,
) -> np.ndarray:
"""
mosaic_u8: (H,W) uint8 com padrão 2x2:
R G
IR B
Retorna raw4: (4,out_h,out_w) float32 em 0..1, ordem [R,G,IR,B].
Qualidade:
- Separa canais primeiro (H/2,W/2), depois faz resize 4ch.
- Não mistura canais. É equivalente a 4 resizes separados.
Performance:
- 1 resize apenas.
- Buffers reutilizáveis para evitar alocações.
"""
global _m2r_hw, _m2r_out_hw, _m2r_tmp4_u8, _m2r_resized4_u8, _m2r_raw4_f32
if mosaic_u8.ndim != 2 or mosaic_u8.dtype != np.uint8:
# Se vier com shape diferente, adapta aqui ou faz assert.
mosaic_u8 = np.asarray(mosaic_u8, dtype=np.uint8)
if mosaic_u8.ndim != 2:
raise ValueError(f"Esperava mosaico 2D uint8 (H,W), veio {mosaic_u8.shape}")
H, W = mosaic_u8.shape[:2]
# Garante dimensões pares (corte mínimo, sem interpolar mosaico)
if (H % 2) != 0:
H -= 1
if (W % 2) != 0:
W -= 1
if H != mosaic_u8.shape[0] or W != mosaic_u8.shape[1]:
mosaic_u8 = mosaic_u8[:H, :W]
H2, W2 = H // 2, W // 2
# (Re)aloca buffers se mudou H,W ou out_h,out_w
if _m2r_hw != (H, W) or _m2r_out_hw != (out_h, out_w):
_m2r_hw = (H, W)
_m2r_out_hw = (out_h, out_w)
_m2r_tmp4_u8 = np.empty((H2, W2, 4), dtype=np.uint8)
_m2r_resized4_u8 = np.empty((out_h, out_w, 4), dtype=np.uint8)
_m2r_raw4_f32 = np.empty((4, out_h, out_w), dtype=np.float32)
tmp4 = _m2r_tmp4_u8
# Separa canais (views) do mosaico (H2,W2)
# Importante: isso não copia; é slicing em visão
r = mosaic_u8[0::2, 0::2]
g = mosaic_u8[0::2, 1::2]
ir = mosaic_u8[1::2, 0::2]
b = mosaic_u8[1::2, 1::2]
# Empacota em 4ch uint8 (H2,W2,4)
tmp4[..., 0] = r
tmp4[..., 1] = g
tmp4[..., 2] = ir
tmp4[..., 3] = b
# UM resize multi-canal para (out_h,out_w,4)
# Usa buffer de saída para reduzir alocação
resized4 = cv2.resize(tmp4, (out_w, out_h), interpolation=interpolation)
# Normaliza e transpõe para (4,H,W) float32 em 0..1
# Evita stack/astype extra
raw4 = _m2r_raw4_f32
# resized4 é uint8 (out_h,out_w,4)
# Transpõe para (4,out_h,out_w) e converte
# astype aqui cria cópia; mas a gente já escreve no buffer raw4, então:
resized4_f32 = resized4.transpose(2, 0, 1).astype(np.float32)
np.multiply(resized4_f32, 1.0 / 255.0, out=resized4_f32)
raw4[...] = resized4_f32
return raw4
def mosaic_to_raw4_resized(
mosaic_u8: np.ndarray,
out_h: int,
out_w: int,
interpolation = cv2.INTER_AREA,
) -> np.ndarray:
"""
mosaic_u8: (H,W) uint8, padrão:
R G
IR B
Retorna raw4 float32 (4,out_h,out_w) em 0..1.
"""
H, W = mosaic_u8.shape[:2]
if (H % 2) != 0 or (W % 2) != 0:
mosaic_u8 = mosaic_u8[:H - (H % 2), :W - (W % 2)]
r = mosaic_u8[0::2, 0::2]
g = mosaic_u8[0::2, 1::2]
ir = mosaic_u8[1::2, 0::2]
b = mosaic_u8[1::2, 1::2]
r = cv2.resize(r, (out_w, out_h), interpolation=interpolation)
g = cv2.resize(g, (out_w, out_h), interpolation=interpolation)
ir = cv2.resize(ir, (out_w, out_h), interpolation=interpolation)
b = cv2.resize(b, (out_w, out_h), interpolation=interpolation)
raw4 = np.stack([r, g, ir, b], axis=0).astype(np.float32) / 255.0
return np.clip(raw4, 0.0, 1.0)
# -----------------------------
# Classe principal: Gal5000Camera
# -----------------------------
class GalService:
def __init__(
self,
dll_dir: str = r"C:\AgroBaseModels\Ervas\dlls",
dll_name: str = "VT_SDK64.dll",
raw_w: int = None,
raw_h: int = None,
use_auto_exposure: bool = True,
debug_mode: bool = False
):
if raw_w is None: raw_w = 2592
if raw_h is None: raw_h = 2056
self.dll_dir = dll_dir
self.dll_name = dll_name
self.raw_w = raw_w
self.raw_h = raw_h
self.debug_mode = debug_mode
self.dll = _load_gal_dll(dll_dir, dll_name)
self.handle: W.HANDLE | None = None
self.exp_raw: int | None = 1500
self.gain_a: int | None = 0
self.gain_d: int | None = 0
self.ae = AEController(exp_max=10000)
self.ae_enabled = use_auto_exposure
self._streaming = False
self._frame_queue = deque(maxlen=1)
self._frame_lock = threading.Lock()
self._frame_cb_c = None # segura a ref do callback
# context manager
def __enter__(self):
self.open()
return self
def __exit__(self, exc_type, exc, tb):
self.close()
# lifecycle
def open(self):
if self.handle is not None:
return
self.handle = _gal_open(self.dll, self.debug_mode)
# tenta ler parâmetros atuais
self._init_params()
def close(self):
if self.handle is None:
return
if self._streaming:
self.stop_streaming()
_gal_close(self.dll, self.handle)
self.handle = None
def configure_fps(self, fps: int):
# 1) tenta habilitar frame rate, mas se não tiver suporte, só avisa e segue
try:
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_ACQUISITIONFRAMERATEENABLE, 1)
except Exception as e:
print(f"[WARN] ACQ_FRAMERATE_ENABLE não suportado: {e}")
# 2) tenta primeiro via SFNC ID
try:
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_ACQUISITIONFRAMERATE, int(fps))
print(f"[INFO] AcquisitionFrameRate (SFNC) setado para {fps} fps")
except Exception as e_id:
print(f"[WARN] SFNC AcquisitionFrameRate falhou: {e_id}")
# 3) fallback via nome 'AcquisitionFrameRateAbs'
try:
_param_set_float_by_name(self.dll, self.handle, "AcquisitionFrameRateAbs", float(fps))
print(f"[INFO] AcquisitionFrameRateAbs setado para {fps} fps")
except Exception as e_name:
print(f"[WARN] AcquisitionFrameRateAbs também falhou: {e_name}")
def configure_binning_full_fov(self, bin_factor: int, fps: float | None = None):
if self.handle is None:
return
# 1) lê o tamanho máximo atual que o SDK considera como 'sensor'
sensor_w = _param_get_int(self.dll, self.handle, PARAM_ID_SFNC_SENSORWIDTH)
sensor_h = _param_get_int(self.dll, self.handle, PARAM_ID_SFNC_SENSORHEIGHT)
# 2) seta o binning (igual ao combo do viewer)
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_BINNINGHORIZONTAL, bin_factor)
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_BINNINGVERTICAL, bin_factor)
# 3) offset zerado para garantir FOV máximo
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_OFFSETX, 0)
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_OFFSETY, 0)
# 4) width/height cobrindo tudo
# OBS: dependendo do SDK, SensorWidth já pode estar "pós-binning".
# Se ao dividir por bin_factor você perder FOV, teste também sem dividir.
width = sensor_w // bin_factor
height = sensor_h // bin_factor
print(f'Bin factor: {bin_factor}, Sensor: {sensor_w}x{sensor_h}, Shape: {width}x{height}')
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_WIDTH, width)
_param_set_int(self.dll, self.handle, PARAM_ID_SFNC_HEIGHT, height)
# 5) fps opcional, como você já fez
if fps is not None:
self.configure_fps(fps)
# leitura inicial de exp/gain
def _init_params(self):
if self.handle is None:
return
try:
self.set_exposure(self.exp_raw)
self.set_gain_a(self.gain_a)
self.set_gain_d(self.gain_d)
except Exception:
print("Erro ao definir parametros iniciais de AE")
try:
self.exp_raw = _param_get_int(self.dll, self.handle, PARAM_ID_SENSOR_EXPOSURETIMERAW)
except Exception:
self.exp_raw = 1500
try:
self.gain_a = _param_get_int(self.dll, self.handle, PARAM_ID_SENSOR_GAINANALOGRAW)
except Exception:
self.gain_a = 0
try:
self.gain_d = _param_get_int(self.dll, self.handle, PARAM_ID_SENSOR_GAINDIGITRAW)
except Exception:
self.gain_d = 0
# getters / setters exp/gain
def get_exposure(self) -> int:
return int(self.exp_raw) if self.exp_raw is not None else 0
def set_exposure(self, new_exp: int) -> int:
if self.handle is None:
return 0
new_exp = _clamp(int(new_exp), EXP_MIN, EXP_MAX)
_param_set_int(self.dll, self.handle, PARAM_ID_SENSOR_EXPOSURETIMERAW, new_exp)
self.exp_raw = new_exp
return new_exp
def get_gain_a(self) -> int:
return int(self.gain_a) if self.gain_a is not None else 0
def set_gain_a(self, new_gain: int) -> int:
if self.handle is None:
return 0
new_gain = _clamp(int(new_gain), GAIN_A_MIN, GAIN_A_MAX)
#_param_set_int(self.dll, self.handle, PARAM_ID_SENSOR_GAINANALOGRAW, new_gain)
_param_set_int(self.dll, self.handle, PARAM_ID_SENSOR_GAINDIGITRAW, new_gain)
self.gain_a = new_gain
return new_gain
def get_gain_d(self) -> int:
return int(self.gain_d) if self.gain_d is not None else 0
def set_gain_d(self, new_gain: int) -> int:
if self.handle is None:
return 0
new_gain = _clamp(int(new_gain), GAIN_D_MIN, GAIN_D_MAX)
_param_set_int(self.dll, self.handle, PARAM_ID_SENSOR_GAINDIGITRAW, new_gain)
self.gain_d = new_gain
return new_gain
# auto exposure
def enable_auto_exposure(self, enabled: bool = True):
self.ae_enabled = enabled
def is_auto_exposure_enabled(self) -> bool:
return self.ae_enabled
# captura bruta
def grab_mosaic(self, timeout_ms: int = 2000) -> np.ndarray:
if self.handle is None:
raise RuntimeError("Câmera não está aberta. Chame open() antes.")
return _gal_capture_raw8_mosaic(self.dll, self.handle, self.raw_w, self.raw_h, timeout_ms)
# captura + AE + conversão para RAW4
def grab_raw4(
self,
out_h: int,
out_w: int,
timeout_ms: int = 2000,
do_ae: bool = True,
):
"""
Captura um frame, aplica AE se habilitado,
converte para RAW4 normalizado e retorna:
raw4: np.ndarray (4,out_h,out_w) float32 em 0..1
dbg: dict com métricas de AE (p95, sat, exp, gains)
"""
t0 = time.time()
if self._streaming:
mosaic = self.grab_mosaic_stream(timeout_ms)
#mosaic = self.grab_mosaic_latest()
else:
mosaic = self.grab_mosaic(timeout_ms)
t1 = time.time()
dbg_ae = None
do_ae_now = (
do_ae
and self.ae_enabled
and self.exp_raw is not None
and (t1 - self.ae._last_time) >= self.ae.run_each # máximo 10 Hz de AE
)
if do_ae_now and self.ae_enabled and self.exp_raw is not None:
new_exp, new_ga, new_gd, dbg_ae = self.ae.step(
mosaic,
self.exp_raw,
self.gain_a or 0,
self.gain_d or 0,
)
if new_exp != self.exp_raw:
self.set_exposure(new_exp)
if new_ga != self.gain_a:
self.set_gain_a(new_ga)
if new_gd != self.gain_d:
self.set_gain_d(new_gd)
t2 = time.time()
raw4 = mosaic_to_raw4_resized_buf(mosaic, out_h, out_w)
t3 = time.time()
dbg = {
"raw_shape": mosaic.shape,
"ae": dbg_ae,
"exp_raw": self.exp_raw,
"gain_a": self.gain_a,
"gain_d": self.gain_d,
"t_capture": t1 - t0,
"t_ae": t2 - t1,
"t_convert": t3 - t2,
"latency_s": t3 - t0,
}
return raw4, dbg
def get_device_info(self) -> dict:
# sempre index 0 no teu _gal_open
desc = ctypes.c_ubyte(0)
devinfo = VT_DEVINFO()
ret = self.dll.VT_DeviceGetInfo(
ctypes.byref(desc),
ctypes.byref(devinfo),
DEVICE_INDEX,
DEVICE_UDEF, # ou DEVICE_UDEF
)
_ck(ret, "VT_DeviceGetInfo")
info = {}
if devinfo.eDevType == DEVICE_USB and devinfo.u.pDevInfoUSB:
usb = devinfo.u.pDevInfoUSB.contents
info["vendor"] = usb.cDevVendorName.decode("ascii", "ignore").strip()
info["model"] = usb.cDevName.decode("ascii", "ignore").strip()
info["serial"] = usb.cDevSN.decode("ascii", "ignore").strip()
info["vid"] = usb.iDevVID
info["pid"] = usb.iDevPID
info["version_word"] = usb.iDevVersion
self.dev_info = info
return info
def get_status(self) -> dict:
"""
Retorna um snapshot simples do estado da câmera.
"""
return {
"opened": self.handle is not None,
"exp_raw": self.exp_raw,
"gain_a": self.gain_a,
"gain_d": self.gain_d,
"ae_enabled": self.ae_enabled,
"raw_w": self.raw_w,
"raw_h": self.raw_h,
}
def get_temperature_c(self) -> float | None:
"""
a temperatura do dispositivo, se o parâmetro existir.
"""
if self.handle is None:
return None
try:
return _param_get_float_by_name(self.dll, self.handle, "DeviceTemperature")
except Exception:
return None
# streaming
def _on_frame(self, hDev, fi: VT_FRAMEINFO, ctx):
"""
Callback chamado pelo SDK a cada frame.
Converte o buffer RAW8 mosaic para np.ndarray e põe na fila.
Mantém o trabalho aqui o mais leve possível.
"""
try:
w = int(fi.lWidth)
h = int(fi.lHeight)
size = int(fi.lBufSize)
buf = C.string_at(fi.pBufPtr, size)
arr = np.frombuffer(buf, dtype=np.uint8)
needed = w * h
if arr.size < needed:
arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0)
elif arr.size > needed:
arr = arr[:needed]
mosaic = arr.reshape(h, w)
with self._frame_lock:
self._frame_queue.append((mosaic, time.time()))
except Exception as e:
print(f"[FRAME_CB ERROR] {e}")
return 0
def start_streaming(self):
if self.handle is None:
raise RuntimeError("Câmera não está aberta.")
if self._streaming:
return
# cria callback C e segura referência
self._frame_cb_c = FRAME_CALLBACK(self._on_frame)
_ck(self.dll.VT_SetFrameCallback(self.handle, self._frame_cb_c, None, DATA_RAW), "VT_SetFrameCallback")
_ck(self.dll.VT_CaptureStart(self.handle), "VT_CaptureStart")
self._streaming = True
def stop_streaming(self):
if not self._streaming or self.handle is None:
return
try:
self.dll.VT_CaptureStop(self.handle)
except Exception:
pass
self._streaming = False
def grab_mosaic_stream(self, timeout_ms: int = 2000) -> np.ndarray:
"""
o último frame da fila de streaming.
"""
if not self._streaming:
raise RuntimeError("Streaming não está ativo. Chame start_streaming().")
deadline = time.time() + timeout_ms / 1000.0
last = None
while time.time() < deadline:
with self._frame_lock:
if self._frame_queue:
last = self._frame_queue[-1]
if last is not None:
mosaic, t_cap = last
return mosaic
time.sleep(0.001)
raise TimeoutError("Timeout aguardando frame de streaming.")
def grab_mosaic_latest(self) -> np.ndarray:
if not self._streaming:
raise RuntimeError("Streaming não está ativo.")
with self._frame_lock:
if not self._frame_queue:
raise TimeoutError("Nenhum frame disponível ainda.")
mosaic, t_cap = self._frame_queue[-1]
return mosaic

View File

@ -1,4 +1,5 @@
import time
from camera_worker.gal_service import GalService
import depthai as dai
from shared.enums import StatusModulo, T_Code, VisualWorkerCommandType, WeedWorkerCommandType
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey, CmdKey
@ -18,6 +19,15 @@ class CameraManager:
dispositivos_serializados = [d.getMxId() for d in dispositivos]
except Exception as e:
dispositivos_serializados = []
try:
with GalService() as cam:
if cam:
info = cam.get_device_info()
if info:
dispositivos_serializados.append(info.get("serial"))
except Exception as e:
pass
cameras_mapeadas = ContextoGlobalRedis.get_cameras()
@ -50,13 +60,13 @@ class CameraManager:
if visual_worker_camera_id is not None and visual_worker_camera_id in cameras_mapeadas:
ContextoGlobalRedis.publicar_comando(CmdKey.VisualWorkerRx, { "cmd": VisualWorkerCommandType.IniciarCameraManager.value, "params": visual_worker_camera_id } )
weed_worker_camera_id = ContextoGlobalRedis.get_equipamento().get("camera_solo_id")
if weed_worker_camera_id is not None and weed_worker_camera_id in cameras_mapeadas:
if (weed_worker_camera_id is not None and weed_worker_camera_id in cameras_mapeadas):
ContextoGlobalRedis.publicar_comando(CmdKey.WeedWorkerRx, { "cmd": WeedWorkerCommandType.IniciarCameraManager.value, "params": weed_worker_camera_id } )
def definir_saude_camera(mx_id: str, status: StatusModulo, saude: int, motivos: list, rodando: bool, performance: dict, conectado: bool, ts=None, disp: T_Code = None):
#print(f"Definindo saude da camera {mx_id}")
_camera = ContextoGlobalRedis.get_camera(mx_id)
_camera = ContextoGlobalRedis.get_camera(mx_id) or {}
#print(_cameras)
saude_geral = {
"timestamp": time.time() if ts is None else ts,

View File

@ -0,0 +1,149 @@
import time
from PIL import Image
import cv2
import numpy as np
import torch
from transformers import SegformerForSemanticSegmentation
class SegformerNavRunner:
IMAGENET_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
IMAGENET_STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
def __init__(self, seg_config, device="cuda"):
from shared.utils import carregar_labelmap_completo
self.device = torch.device(device if torch.cuda.is_available() else "cpu")
self.cor_para_id, self.colormap_rgb, self.classes, self.ignore_rgb = carregar_labelmap_completo(seg_config["ia_labelmap_path"])
self.model = self.load_segformer_from_checkpoint(seg_config, self.device)
self.resolucao = tuple(seg_config["ia_resolution"])
self.roi_inicio = seg_config["ia_roi_begin"]
self.roi_tamanho = seg_config["ia_roi_size"]
self.last_infer = None
def _extract_state_dict(self, ckpt):
"""
Aceita:
- state_dict puro (dict de tensores)
- checkpoint com chaves comuns: state_dict / model_state_dict / model
"""
if not isinstance(ckpt, dict):
return None
# caso já seja um state_dict puro
if any(isinstance(v, torch.Tensor) for v in ckpt.values()):
return ckpt
for k in ("state_dict", "model_state_dict", "model"):
if k in ckpt and isinstance(ckpt[k], dict):
return ckpt[k]
return None
def load_segformer_from_checkpoint(
self,
seg_config: dict,
device: torch.device,
):
"""
Carrega um SegFormer (B0, B1, B2, B3...) compatível com o treino:
- Cria o modelo via from_pretrained(backbone, num_labels=num_classes)
- Carrega o state_dict salvo pelo script de treino
"""
pt_path = seg_config.get("ia_model_path")
backbone = seg_config.get("ia_backbone")
num_classes = len(self.classes)
ckpt = torch.load(pt_path, map_location="cpu", weights_only=True)
state_dict = self._extract_state_dict(ckpt)
if state_dict is None:
raise RuntimeError(f"Não consegui extrair state_dict de {pt_path}. keys={list(ckpt.keys())}")
# limpar prefixos comuns
cleaned = {}
for k, v in state_dict.items():
nk = k
if nk.startswith("model."):
nk = nk[len("model."):]
if nk.startswith("module."):
nk = nk[len("module."):]
cleaned[nk] = v
model = SegformerForSemanticSegmentation.from_pretrained(
backbone,
num_labels=num_classes,
ignore_mismatched_sizes=True,
use_safetensors=True
)
missing, unexpected = model.load_state_dict(cleaned, strict=False)
print(f"[load] missing={len(missing)} unexpected={len(unexpected)}")
if missing:
print("[load] missing sample:", missing[:10])
if unexpected:
print("[load] unexpected sample:", unexpected[:10])
model.to(device).eval()
return model
def normalize_img(self, img: torch.Tensor) -> torch.Tensor:
return (img - self.IMAGENET_MEAN.to(img.device)) / self.IMAGENET_STD.to(img.device)
def compute_roi_indices(self, H: int, zona_inicio: float, faixa_atuacao: float):
y_inicio = int((1.0 - zona_inicio) * H)
y_fim = int((1.0 - (zona_inicio + faixa_atuacao)) * H)
y_fim = max(0, min(H, y_fim))
y_inicio = max(0, min(H, y_inicio))
if y_fim >= y_inicio:
y_fim = max(0, y_inicio - 1)
return y_fim, y_inicio
def resize_keep_width(self, img: np.ndarray, new_w: int, min_h: int, interpolation: int) -> np.ndarray:
h, w = img.shape[:2]
new_h = int(round(new_w * (h / w)))
if min_h is not None and new_h < min_h:
new_h = min_h
return cv2.resize(img, (new_w, new_h), interpolation=interpolation)
@torch.no_grad()
def segformer_predict_ids(self, img_tensor):
"""
img_tensor: [1,3,H,W] float32 normalizado.
retorna: pred_ids [H,W] (numpy int)
"""
out = self.model(pixel_values=img_tensor)
logits = out.logits # [B, C, h, w] (pode ser menor que input)
# Upsample logits para o tamanho do input
logits = torch.nn.functional.interpolate(
logits,
size=img_tensor.shape[-2:],
mode="bilinear",
align_corners=False
)
pred = torch.argmax(logits, dim=1) # [B,H,W]
return pred.squeeze(0).cpu().numpy().astype(np.uint8)
def infer_ids(self, frame_rgb):
if False:
img_rgb = np.array(Image.open(r"C:\ZendionInc\agrobot_base\t_cor\108_rgb.jpeg").convert("RGB"))
H, W = img_rgb.shape[:2]
y_fim, y_inicio = self.compute_roi_indices(H, self.roi_inicio, self.roi_tamanho)
roi = img_rgb[y_fim:y_inicio, 0:W]
else:
H, W = frame_rgb.shape[:2]
y_fim, y_inicio = self.compute_roi_indices(H, self.roi_inicio, self.roi_tamanho)
roi = frame_rgb[y_fim:y_inicio, 0:W]
roi_resized = self.resize_keep_width(roi, self.resolucao[0], self.resolucao[1], cv2.INTER_AREA)
roi_norm = roi_resized.astype(np.float32) / 255.0
img_tensor = torch.from_numpy(roi_norm).permute(2, 0, 1).unsqueeze(0).to(self.device)
img_tensor = self.normalize_img(img_tensor).float()
pred_ids = self.segformer_predict_ids(img_tensor) # (H,W)
self.last_infer = time.time()
return pred_ids, self.last_infer, roi_resized, (y_fim, y_inicio)

View File

@ -178,3 +178,13 @@ class CameraFrameType(IntEnum):
RadarTopDown = 3
Segmentacao = 4
Debug = 5
class TipoFrameCamera(IntEnum):
Rgb = 0
Segmentacao = 1
Overlay = 2
Debug = 3
Heatmap = 4
MatrizCusto = 5
Deteccoes = 6
Corredor = 7

View File

@ -134,22 +134,44 @@ def carregar_labelmap_completo(caminho):
return cor_para_id, cores_rgb, id_para_nome, ignore_rgb
def converter_mask_rgb_para_ids(img_rgb, mapa_rgb, ignore_id):
h, w, _ = img_rgb.shape
mask = np.ones((h, w), dtype=np.uint8) * ignore_id # Inicializa como ignore
# Cria um mapa 256^3 para IDs (usa int32 para indexar)
lut = np.full((256**3,), ignore_id, dtype=np.uint8)
for cor, classe_id in mapa_rgb.items():
r, g, b = cor
cond = (img_rgb[:,:,0]==r) & (img_rgb[:,:,1]==g) & (img_rgb[:,:,2]==b)
mask[cond] = classe_id
# Pixels brancos (ou ignore_bgr) continuam como 255
return mask
lut[(r << 16) + (g << 8) + b] = classe_id
def converter_mask_ids_para_rgb(mask_ids: np.ndarray, mapa_rgb: dict, ignore_id: int = 255) -> np.ndarray:
h, w = mask_ids.shape
rgb = np.zeros((h, w, 3), dtype=np.uint8)
for class_id, color in enumerate(mapa_rgb):
rgb[mask_ids == class_id] = color
rgb[mask_ids == ignore_id] = [255, 255, 255]
return rgb
# Converte RGB para índice único
flat_idx = (img_rgb[:,:,0].astype(np.int32) << 16) + \
(img_rgb[:,:,1].astype(np.int32) << 8) + \
img_rgb[:,:,2].astype(np.int32)
# Aplica LUT vetorizada
return lut[flat_idx]
def converter_mask_ids_para_rgb(mask_ids: np.ndarray, colormap_rgb: list, ignore_id: int = 255) -> np.ndarray:
# Criar lookup table (256 cores possíveis)
lut = np.zeros((256, 3), dtype=np.uint8)
for i, color in enumerate(colormap_rgb):
lut[i] = color
lut[ignore_id] = (255, 255, 255)
# Aplicar LUT direto (vetorizado)
return lut[mask_ids]
def converter_mask_ids_para_bgr(mask_ids: np.ndarray, colormap_rgb: list, ignore_id: int = 255) -> np.ndarray:
"""
Converte máscara de IDs para imagem BGR (uint8),
pronta para uso com OpenCV.
"""
lut = np.zeros((256, 3), dtype=np.uint8)
for i, (r, g, b) in enumerate(colormap_rgb):
lut[i] = (b, g, r) # RGB -> BGR
lut[ignore_id] = (255, 255, 255) # branco em BGR = RGB
return lut[mask_ids]
def desenhar_legenda_vertical(colormap_rgb, classes, largura=200):
"""

View File

@ -12,10 +12,10 @@ from visual_worker.processamento.analise_solo import AnaliseSoloManager
from visual_worker.processamento.analise_anomalias import AnaliseAnomaliasManager
from visual_worker.processamento.radar_top_down import Radar2DManager
from visual_worker.processamento.segmentacao_semantica import ClassesSegmentacao, SegmentacaoManager
from visual_worker.processamento.segformer_runner import SegformerNavRunner
from camera_worker.segformer_runner import SegformerNavRunner
from visual_worker.processamento.costmap_fuser import CostmapFuser, unpack_snapshot
from shared.enums import StatusModulo, T_Code, CameraFrameType
from shared.utils import analisar_linhas_por_profundidade, decode_image_base64, encode_image_base64, fazer_overlay, get_velocidade_atual_ms
from shared.enums import StatusModulo, T_Code, TipoFrameCamera
from shared.utils import analisar_linhas_por_profundidade, converter_mask_ids_para_bgr, decode_image_base64, encode_image_base64, fazer_overlay, get_velocidade_atual_ms
from shared.gps_handler import GPSHandler
from camera_worker.camera_oak import CameraOak
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
@ -37,8 +37,15 @@ class CameraManager:
self._ultima_analise_deteccao = {}
self._ultima_analise_matriz_confianca = {}
self._ultima_analise_matriz_custo = {}
self._ultimo_predictions = None
self._ultimo_detections = None
self._ultimo_snapshot = None
self._ultimo_rgb_frame = None
self._ultimo_depth_frame = None
self._ultimo_heatmap_frame = None
self._ultimo_frame_deteccoes = None
self._ultimo_frame_matriz_custo = None
self._ultimo_frame_corredor = None
self._ts_segmentacao_anterior = 0
self._ts_deteccao_anterior = 0
self._pool = ThreadPoolExecutor(max_workers=6)
@ -74,7 +81,7 @@ class CameraManager:
if self.camera is None:
self.mostrar_log(f"❌ Camera com ID {mx_id} não iniciada.")
else:
self.mostrar_log(f"📷 Camera visual selecionada: {self.camera.modelo} - {self.camera.mx_id}")
self.mostrar_log(f"📷 Camera selecionada: {self.camera.modelo} - {self.camera.mx_id}")
self.largura_robo_m = ContextoGlobalRedis.get(CtxKey.DadosEquipamento, {}).get("largura", 0.85)
self.operante = True
self._timestamp_analise = None
@ -94,6 +101,13 @@ class CameraManager:
self._ts_deteccao_anterior = 0
self._ultimo_rgb_frame = None
self._ultimo_depth_frame = None
self._ultimo_heatmap_frame = None
self._ultimo_predictions = None
self._ultimo_detections = None
self._ultimo_snapshot = None
self._ultimo_frame_deteccoes = None
self._ultimo_frame_matriz_custo = None
self._ultimo_frame_corredor = None
self._rgb_frame_necessario = True
self._depth_frame_necessario = True
self._nova_segmentacao_disponivel = False
@ -106,6 +120,7 @@ class CameraManager:
self._analisando_deteccao = False
self._iniciar_loop_analise_continua(15.0)
self._iniciar_loop_frame_stream(5.0)
self.iniciando = False
self.atualizar_saude_camera()
@ -170,7 +185,9 @@ class CameraManager:
def get_heatmap_frame(self):
frame, timestamp, res = self.get_depth_frame()
if frame is not None:
return gerar_heatmap(frame, self.camera.parametros["distancia_maxima"]), timestamp, res
heatmap = gerar_heatmap(frame, self.camera.parametros["distancia_maxima"])
self._ultimo_heatmap_frame = heatmap
return heatmap, timestamp, res
return None, None, None
def get_segmentation_predictions(self):
@ -183,6 +200,7 @@ class CameraManager:
# return predictions, self.camera.timestamp_ultima_segmentacao, res
rgb_frame, _ts, res = self.get_rgb_frame()
predictions, ts, roi_resized, (y_fim, y_inicio) = self.seg_runner.infer_ids(rgb_frame)
self._ultimo_predictions = predictions
if predictions is not None:
return predictions, ts, res
elif "X_LINK_ERROR" in res["erro"]:
@ -200,6 +218,7 @@ class CameraManager:
try:
detections, res = self.camera.requisitar_deteccao()
self._ultimo_detections = detections
if detections is not None:
return detections, self.camera.timestamp_ultima_deteccao, res
elif "X_LINK_ERROR" in res["erro"]:
@ -211,33 +230,26 @@ class CameraManager:
return None, None, None
def get_select_frame(self, tipo: CameraFrameType):
f = None
t = None
if tipo == CameraFrameType.Rgb:
f, t, _ = self.get_rgb_frame()
if f is not None:
f = encode_image_base64(f)
elif tipo == CameraFrameType.Heatmap:
f, t, _ = self.get_heatmap_frame()
if f is not None:
f = encode_image_base64(f)
elif tipo == CameraFrameType.RadarTopDown:
f = self._ultima_analise_radar.get("frame", {}).get("frame")
t = self._ultima_analise_radar.get("timestamp")
elif tipo == CameraFrameType.Segmentacao:
f = self._ultima_analise_segmentacao.get("frame", {}).get("frame")
t = self._ultima_analise_segmentacao.get("timestamp")
elif tipo == CameraFrameType.Debug:
frame_seg = self._ultima_analise_segmentacao.get("mask_color")
frame_rgb = self._ultimo_rgb_frame
if frame_seg is not None and frame_rgb is not None:
f = fazer_overlay(frame_rgb, frame_seg, alpha=0.35, out_size=(640, 360), seg_is_rgb=False)
if f is not None:
f = encode_image_base64(f)
t = self.camera.timestamp_ultimo_frame_rgb
return f, t
def get_selected_frame(self, _frame_type: TipoFrameCamera):
_frame = None
if (_frame_type == TipoFrameCamera.Rgb):
_frame = self._ultimo_rgb_frame
elif (_frame_type in [TipoFrameCamera.Segmentacao, TipoFrameCamera.Overlay]):
if _frame_type == TipoFrameCamera.Segmentacao: _alpha = 1.0
elif _frame_type == TipoFrameCamera.Overlay: _alpha = 0.5
_frame, _ = self._build_preview(self._ultimo_rgb_frame, self._ultimo_predictions, alpha=_alpha)
elif (_frame_type == TipoFrameCamera.Debug):
_frame = None
elif (_frame_type == TipoFrameCamera.Heatmap):
if (self._ultimo_depth_frame is not None):
_frame = gerar_heatmap(self._ultimo_depth_frame, self.camera.parametros["distancia_maxima"])
elif (_frame_type == TipoFrameCamera.MatrizCusto):
_frame, _ = self.debug_blockage_imshow(self._ultimo_rgb_frame, self._ultimo_snapshot, show=False)
elif (_frame_type == TipoFrameCamera.Deteccoes):
_frame, _, _ = self._overlay_deteccoes(self._ultimo_rgb_frame, self._ultimo_detections, show=False)
elif (_frame_type == TipoFrameCamera.Corredor):
_frame = self.segmentacao_manager.display_segmentation_debug(self._ultimo_rgb_frame, self.largura_robo_m, self.grid_ref, show=False)
return _frame
def _iniciar_loop_analise_continua(self, freq):
@ -279,6 +291,36 @@ class CameraManager:
threading.Thread(target=loop, daemon=True).start()
def _iniciar_loop_frame_stream(self, freq):
def loop():
while True:
if self.camera is None:
time.sleep(5)
continue
t0 = time.time()
try:
_frame_type = (TipoFrameCamera)((ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("frame_type", TipoFrameCamera.Rgb.value))
_frame = self.get_selected_frame(_frame_type)
if _frame is not None:
self.camera.enviar_frame_stream(_frame)
from visual_worker.config import load_seg_config, load_det_config
seg_config = load_seg_config()
det_config = load_det_config()
if seg_config.get("debug_visual"):
self.segmentacao_manager.display_segmentation_debug(self._ultimo_rgb_frame, self.largura_robo_m, self.grid_ref)
self.debug_blockage_imshow(self._ultimo_rgb_frame, self._ultimo_snapshot)
if det_config.get("debug_visual"):
self._overlay_deteccoes(self._ultimo_rgb_frame, self._ultimo_detections)
except Exception as e:
self.mostrar_log(f"Erro no loop de stream: {e}")
finally:
latencia = time.time() - t0
time.sleep(max(0, (1.0 / freq) - latencia))
threading.Thread(target=loop, daemon=True).start()
def _calcular_performance(self, t0, t1, analise):
latencia = t1 - t0
freq = 1.0 / max(latencia, 1e-6)
@ -313,9 +355,9 @@ class CameraManager:
t0 = time.time()
predictions, ts, res = self.get_segmentation_predictions()
if ts == self._ts_segmentacao_anterior: return
fps = 1.0 / (ts - self._ts_segmentacao_anterior)
self._ts_segmentacao_anterior = ts
if predictions is not None:
#rgb_frame, _ts, res = self.get_rgb_frame()
analise_segmentacao, log = self.segmentacao_manager.segmentar(predictions)
t1 = time.time()
if analise_segmentacao == None: self.mostrar_log(log)
@ -329,10 +371,6 @@ class CameraManager:
)
self._nova_segmentacao_disponivel = True
from visual_worker.config import load_seg_config
if load_seg_config().get("debug_visual", False):
self.segmentacao_manager.display_segmentation_debug(self._ultimo_rgb_frame, self.largura_robo_m, self.grid_ref)
# Enviar comando para atualizar os dados de controle sempre que um novo dado de segmentacao seja processado e a operacao seja do tipo MapeamentoVisual
#op_modo = ContextoGlobalRedis.get_operacao().get("modo", ModoOperacao.NaoDefinido.value)
#movimento_automatico = ContextoGlobalRedis.get_controle().get("movimento_automatico", False)
@ -352,6 +390,7 @@ class CameraManager:
t0 = time.time()
dets, ts, meta = self.get_detections()
if ts == self._ts_deteccao_anterior: return
fps = 1.0 / (ts - self._ts_deteccao_anterior)
self._ts_deteccao_anterior = ts
if dets is not None:
t1 = time.time()
@ -367,10 +406,6 @@ class CameraManager:
)
self._nova_deteccao_disponivel = True
from visual_worker.config import load_det_config
if load_det_config().get("debug_visual", False):
self._overlay_deteccoes(self._ultimo_rgb_frame, dets)
except Exception as e:
self.mostrar_log(f"❌ Erro na deteccao de objetos: {e}")
finally:
@ -399,6 +434,9 @@ class CameraManager:
Retorna: (frame_com_overlay, fps_state, keep_loop_bool)
"""
try:
if (rgb_frame is None or dets is None):
return None, None, None
img = cv2.resize(rgb_frame.copy(), (1280, 720))
H, W = img.shape[:2]
@ -493,6 +531,8 @@ class CameraManager:
k = cv2.waitKey(1) & 0xFF
keep = (k != 27) # ESC para sair
self._ultimo_frame_deteccoes = img
return img, fps_state, keep
except Exception as e:
self.mostrar_log(f"Erro ao gerar overlay de deteccoes")
@ -504,7 +544,7 @@ class CameraManager:
if depth_frame_np is None or depth_frame_np.size == 0: return
segmentacao = self._ultima_analise_segmentacao.get("classes")
if segmentacao is None: return
deteccoes = self._ultima_analise_deteccao.get("bboxes")
deteccoes = self._ultimo_detections
vel = get_velocidade_atual_ms()
@ -515,6 +555,7 @@ class CameraManager:
grid_conf["ultima_chamada"] = self._ultima_analise_matriz_confianca.get("ultima_chamada", t0)
self._calcular_performance(t0, t1, grid_conf)
snapshot = self.data_fuser.update(grid_conf, velocidade_ms=vel, status_seg=self._ultima_analise_segmentacao.get("dados_visuais", {}).get("status_corredor"))
self._ultimo_snapshot = snapshot
self._ultima_analise_matriz_confianca = grid_conf
ContextoGlobalRedis.atualizar_ctx_dict(
@ -523,10 +564,6 @@ class CameraManager:
matriz_confianca=snapshot
)
self._nova_grid_conf_disponivel = True
from visual_worker.config import load_seg_config
if load_seg_config().get("debug_visual", False):
vis, metrics = self.debug_blockage_imshow(self._ultimo_rgb_frame, snapshot, velocidade_media=vel)
except Exception as e:
self.mostrar_log(f"❌ Erro na geracao da matriz de confianca: {e}")
finally:
@ -784,11 +821,17 @@ class CameraManager:
near_is_bottom=True,
win_name="Block Debug",
alpha=0.35,
velocidade_media=0.97, # << NOVO (m/s)
velocidade_media=None, # << NOVO (m/s)
a_max_freio=0.8, # << NOVO (m/s²)
margem_parada=0.25, # << NOVO (m)
show: bool = True
):
try:
if (rgb_frame is None or snapshot is None):
return None, None
if (velocidade_media is None):
velocidade_media = get_velocidade_atual_ms()
# --- unpack do snapshot ---
custo_f, conf_f, anom_f, nav_f = unpack_snapshot(snapshot)
row_dist_m = np.asarray(snapshot.get("row_dist_m"), dtype=np.float32)
@ -972,8 +1015,12 @@ class CameraManager:
status_col = (0,0,255) if blocked else (0,255,0)
cv2.putText(vis, status_txt, (Wf - 40 - 8*len(status_txt), 24), cv2.FONT_HERSHEY_SIMPLEX, 0.6, status_col, 2, cv2.LINE_AA)
cv2.imshow(win_name, vis)
cv2.waitKey(1)
if (show):
cv2.imshow(win_name, vis)
cv2.waitKey(1)
self._ultimo_frame_matriz_custo = vis
return vis, metrics
except Exception as e:
self.mostrar_log(f"Erro ao criar debug blockage imshow: {e}")
@ -1004,6 +1051,39 @@ class CameraManager:
return over, mask_anom, mask_cost, mask_conf
def _build_preview(self, bgr, pred_ids, alpha):
t0 = time.time()
if bgr is None or pred_ids is None:
return None, 0.0
# Garante que pred_ids seja 2D
pred_ids = np.array(pred_ids)
if pred_ids.ndim == 3 and pred_ids.shape[-1] == 1:
pred_ids = pred_ids[..., 0]
pred_bgr = converter_mask_ids_para_bgr(pred_ids, self.seg_runner.colormap_rgb)
# 🔴 Aqui está a mágica: garantir mesmo tamanho
if pred_bgr.shape[:2] != bgr.shape[:2]:
pred_bgr = cv2.resize(
pred_bgr,
(bgr.shape[1], bgr.shape[0]), # (width, height)
interpolation=cv2.INTER_NEAREST, # mantém os IDs de classe
)
a = float(np.clip(alpha, 0.0, 1.0))
# Se alpha == 0, devolve só o RGB sem gastar CPU à toa
if a == 0.0:
overlay = bgr.copy()
else:
overlay = (bgr.astype(np.float32) * (1 - a) + pred_bgr.astype(np.float32) * a)
overlay = np.clip(overlay, 0, 255).astype(np.uint8)
dt = (time.time() - t0) * 1000.0 # ms
return overlay, dt
def salvar_frames(self, tipos: list, nome: str, pasta="frames_salvos"):
if not self.operante:
@ -1017,43 +1097,15 @@ class CameraManager:
if nome is not None and nome != "":
frame_name = nome
if CameraFrameType.Rgb.value in tipos:
rgb_frame, rgb_ts, rgb_res = self.get_rgb_frame()
if rgb_frame is not None and rgb_res["frame_valido"]:
nome_rgb = f"{frame_name}_rgb.jpeg"
caminho_rgb = os.path.join(pasta, nome_rgb)
cv2.imwrite(caminho_rgb, rgb_frame)
frames_salvos.append(caminho_rgb)
if CameraFrameType.Heatmap.value in tipos:
heatmap_frame, heatmap_ts, heatmap_res = self.get_heatmap_frame()
if heatmap_frame is not None and heatmap_res["frame_valido"]:
nome_heatmap = f"{frame_name}_heatmap.jpeg"
caminho_heatmap = os.path.join(pasta, nome_heatmap)
cv2.imwrite(caminho_heatmap, heatmap_frame)
frames_salvos.append(caminho_heatmap)
if CameraFrameType.RadarTopDown.value in tipos:
frame = self._salvar_frame_analise(frame_name, self._ultima_analise_radar, "radar", pasta)
if frame is not None:
frames_salvos.append(frame)
if CameraFrameType.Segmentacao.value in tipos:
frame = self._salvar_frame_analise(frame_name, self._ultima_analise_segmentacao, "segmentacao", pasta)
if frame is not None:
frames_salvos.append(frame)
for _frame_type in tipos:
_frame = self.get_selected_frame(_frame_type)
if _frame is not None and _frame.size > 0:
nome_frame = f"{frame_name}_{(TipoFrameCamera(_frame_type)).name}.jpeg"
caminho = os.path.join(pasta, nome_frame)
cv2.imwrite(caminho, _frame)
frames_salvos.append(caminho)
return frames_salvos
except Exception as e:
self.mostrar_log(f"❌ Erro ao salvar frames: {e}")
return []
def _salvar_frame_analise(self, frame_name, analise, sulfix, pasta):
base64frame = analise.get("frame", {}).get("frame")
frame = decode_image_base64(base64frame)
if frame is not None:
nome_analise = f"{frame_name}_{sulfix}.jpeg"
caminho_analise = os.path.join(pasta, nome_analise)
cv2.imwrite(caminho_analise, frame)
return caminho_analise
return None

View File

@ -72,7 +72,7 @@ def load_seg_config(force_reload=False):
# "kernel_morf": 3
# }
_CONFIG_CACHE = {
"debug_visual": True,
"debug_visual": False,
"ia_roi_begin": 0.0,
"ia_roi_size": 1.0,
"ia_resolution": [1024,576],
@ -81,7 +81,7 @@ def load_seg_config(force_reload=False):
}
_CONFIG_CACHE["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ruas_seg")
_CONFIG_CACHE["ia_labelmap_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_labelmap_ruas_seg")
_CONFIG_CACHE["ia_backbone"] = ContextoGlobalRedis.get_equipamento().get("ia_backbone_seg", "nvidia/segformer-b0-finetuned-ade-512-512")
_CONFIG_CACHE["ia_backbone"] = ContextoGlobalRedis.get_equipamento().get("ia_backbone_ruas_seg")
return _CONFIG_CACHE
@ -90,7 +90,7 @@ def reload_seg_config():
def load_det_config():
_CONFIG_DET = {
"debug_visual": True,
"debug_visual": False,
"ia_roi_begin": 0.0,
"ia_roi_size": 1.0,
"ia_resolution": [300,300],

View File

@ -7,7 +7,8 @@ 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")
from shared.enums import VisualWorkerCommandType, CameraFrameType
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
from shared.contexto_global_redis import ContextoGlobalRedis, CmdKey, CtxKey
@ -49,8 +50,9 @@ def main():
elif acao == VisualWorkerCommandType.AtualizarSaudeCamera:
get_camera_manager().atualizar_saude_camera()
elif acao == VisualWorkerCommandType.GetCameraFrame:
tipo = CameraFrameType(dados.get("params", CameraFrameType.Rgb.value))
base64_img, ts = get_camera_manager().get_select_frame(tipo)
tipo = TipoFrameCamera(dados.get("params", TipoFrameCamera.Rgb.value))
frame, ts = get_camera_manager().get_selected_frame(tipo)
base64_img = encode_image_base64(frame)
if base64_img is not None:
resposta = {
"frame": base64_img,

View File

@ -500,8 +500,11 @@ class SegmentacaoManager:
return int(frac * largura_frame_px)
def display_segmentation_debug(self, frame, largura_robo_m, grid_ref):
def display_segmentation_debug(self, frame, largura_robo_m, grid_ref, show: bool = True):
try:
if (frame is None):
return None
original = cv2.resize(frame, self.resolucao)
seg_color = np.zeros_like(original)
@ -555,8 +558,11 @@ class SegmentacaoManager:
cv2.putText(overlay, nome, (40, pos_y + 2), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 1, cv2.LINE_AA)
# Mostrar
cv2.imshow("Segmentacao - Overlay", overlay)
cv2.waitKey(1)
if (show):
cv2.imshow("Segmentacao - Overlay", overlay)
cv2.waitKey(1)
#cv2.destroyAllWindows()
return overlay
except Exception as e:
print(f"Erro ao gerar display_segmentation_debug: {e}")

View File

@ -4,9 +4,10 @@ import threading
import time
import cv2
from shared.enums import StatusModulo, CameraFrameType, StatusOperacao, T_Code, WeedWorkerCommandType
from camera_worker.raw_segformer_service import RawSegformerService
from shared.enums import StatusModulo, StatusOperacao, T_Code, TipoFrameCamera, WeedWorkerCommandType
from shared.utils import decode_image_base64, encode_image_base64, fazer_overlay
from camera_worker.camera_oak import CameraOak
from camera_worker.camera_gal import CameraGal
from shared.contexto_global_redis import CmdKey, ContextoGlobalRedis, CtxKey
from weed_worker.weed_detector import WeedDetector
from visual_worker.utils import converter_valores_numpy
@ -25,14 +26,19 @@ class CameraManager:
self._ultimo_rgb_frame = None
self._ultima_analise = {}
self._ts_segmentacao_anterior = 0
self._ts_ultima_analise = 0
self._ultimo_predictions = None
self._ultimo_raw_input = None
self._ultimo_controle = None
self._analisando_segmentacao = False
def inicializar(self, mx_id):
if self.iniciando:
return
_camera_conectada = ContextoGlobalRedis.get_cameras().get(mx_id) is not None
if not _camera_conectada:
return
#_camera_conectada = ContextoGlobalRedis.get_cameras().get(mx_id) is not None
#if not _camera_conectada:
# return
self.iniciando = True
if mx_id == None:
@ -46,7 +52,7 @@ class CameraManager:
seg_config = load_seg_config()
try:
nova = CameraOak(self.mostrar_log, mx_id, modelo_ia_seg=seg_config)
nova = CameraGal(self.mostrar_log, mx_id, raw_w=seg_config["ia_resolution"][0], raw_h=seg_config["ia_resolution"][1])
if nova.iniciado:
self.camera = nova
except Exception as e:
@ -56,15 +62,22 @@ class CameraManager:
if self.camera is None:
self.mostrar_log(f"❌ Camera com ID {mx_id} não iniciada.")
else:
self.mostrar_log(f"📷 Camera visual selecionada: {self.camera.modelo} - {self.camera.mx_id}")
self.mostrar_log(f"📷 Camera selecionada: {self.camera.modelo} - {self.camera.mx_id}")
self.operante = True
self._ultima_analise = {}
self._ultimo_rgb_frame = None
self._ultimo_predictions = None
self._ultimo_raw_input = None
self._ultimo_controle = None
self._analisando_segmentacao = False
self.weed_detector = WeedDetector(self.camera.modelo_ia_seg.get("colormap_rgb"), self.camera.modelo_ia_seg.get("classes"))
self.model_svc = RawSegformerService(model_config=seg_config)
classes = self.model_svc.get_classes()
colormap_rgb = self.model_svc.get_colormap()
self.weed_detector = WeedDetector(color_map=colormap_rgb, classes=classes)
self._iniciar_loop_analise_continua(20.0)
self._iniciar_loop_frame_stream(5.0)
self.iniciando = False
self.atualizar_saude_camera()
@ -103,10 +116,28 @@ class CameraManager:
return None, None, None
try:
predictions, res = self.camera.requisitar_segmentacao()
t0 = time.time()
raw4_base = self.camera.requisitar_frame_raw(force=True)
t1 = time.time()
r = raw4_base[0]
g = raw4_base[1]
ir = raw4_base[2]
b = raw4_base[3]
raw_input = self.model_svc.build_raw_input(r, g, ir, b)
t2 = time.time()
predictions = self.model_svc.infer_raw(raw_input)
ts = time.time()
res = {
"raw_input": raw_input
}
self._ultimo_predictions = predictions
self._ultimo_raw_input = raw_input
#print(f"[PREDICTIONS] t_total: {ts - t0:.5f} s, t_req: {t1 - t0:.5f} s, t_build_raw: {t2 - t1:.5f} s, t_infer: {ts - t2:.5f} s")
if predictions is not None:
self._ultimo_predictions = predictions
return predictions, self.camera.timestamp_ultima_segmentacao, res
return predictions, ts, res
elif "X_LINK_ERROR" in res["erro"]:
self.reiniciar_status()
except Exception as e:
@ -116,26 +147,18 @@ class CameraManager:
return None, None, None
def get_select_frame(self, tipo: CameraFrameType):
f = None
t = None
if tipo == CameraFrameType.Rgb:
f = self._ultimo_rgb_frame
if f is not None:
f = encode_image_base64(f)
t = self.camera.timestamp_ultimo_frame_rgb
elif tipo == CameraFrameType.Segmentacao:
f = self._ultima_analise.get("frame", {}).get("frame")
t = self._ultima_analise.get("timestamp")
elif tipo == CameraFrameType.Debug:
frame_seg = self._ultima_analise.get("mask_color")
frame_rgb = self._ultimo_rgb_frame
if frame_seg is not None and frame_rgb is not None:
f = fazer_overlay(frame_rgb, frame_seg, alpha=0.35, out_size=(640, 360), seg_is_rgb=False)
if f is not None:
f = encode_image_base64(f)
t = self.camera.timestamp_ultimo_frame_rgb
return f, t
def get_selected_frame(self, _frame_type: TipoFrameCamera):
_frame = None
if (_frame_type == TipoFrameCamera.Rgb):
_frame = self._ultimo_rgb_frame
elif (_frame_type in [TipoFrameCamera.Segmentacao, TipoFrameCamera.Overlay]):
if _frame_type == TipoFrameCamera.Segmentacao: _alpha = 1.0
elif _frame_type == TipoFrameCamera.Overlay: _alpha = 0.5
_, _, _frame, _, _ = self.model_svc.preview_infer_cached(self._ultimo_raw_input, self._ultimo_predictions, alpha=_alpha)
elif _frame_type == TipoFrameCamera.Debug:
self.weed_detector._mostrar_debug_bicos_overlay(_frame, [], self._ultimo_controle, show=False)
_frame = self.weed_detector._dbg_img
return _frame
def _iniciar_loop_analise_continua(self, freq):
def loop():
@ -161,46 +184,77 @@ class CameraManager:
threading.Thread(target=loop, daemon=True).start()
def _iniciar_loop_frame_stream(self, freq):
def loop():
while True:
if self.camera is None:
time.sleep(5)
continue
t0 = time.time()
try:
_frame_type = (TipoFrameCamera)((ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("frame_type", TipoFrameCamera.Rgb.value))
_frame = self.get_selected_frame(_frame_type)
if _frame is not None:
self.camera.enviar_frame_stream(_frame)
from weed_worker.config import load_seg_config
config = load_seg_config()
if (config.get("debug_visual")):
_frame = self.get_selected_frame(TipoFrameCamera.Overlay)
self.weed_detector._mostrar_debug_bicos_overlay(_frame, [], self._ultimo_controle, config, show=True)
except Exception as e:
self.mostrar_log(f"Erro no loop de stream: {e}")
finally:
latencia = time.time() - t0
time.sleep(max(0, (1.0 / freq) - latencia))
threading.Thread(target=loop, daemon=True).start()
def _realizar_analises(self):
_operacao = ContextoGlobalRedis.get_operacao()
status_operacao = StatusOperacao(_operacao.get("status", StatusOperacao.NaoIniciado.value))
finalizando = _operacao.get("finalizando", False)
pulverizador_automatico = ContextoGlobalRedis.get_controle().get("pulverizador_automatico", False)
if self._analisando_segmentacao: return
self._analisando_segmentacao = True
try:
t0 = time.time()
predictions, ts, res = self.get_segmentation_predictions()
if ts == self._ts_segmentacao_anterior: return # já analisado
fps = 1.0 / (ts - self._ts_segmentacao_anterior)
self._ts_segmentacao_anterior = ts
if predictions is not None:
analise_completa = self.detectar_ervas(predictions, None)
analise = analise_completa.get("dados_visuais", {})
ContextoGlobalRedis.atualizar_ctx_dict(
CtxKey.DadosWeedWorker,
ts_analise=ts,
fps_model=fps,
analise=converter_valores_numpy(analise)
)
_operacao = ContextoGlobalRedis.get_operacao()
status_operacao = StatusOperacao(_operacao.get("status", StatusOperacao.NaoIniciado.value))
finalizando = _operacao.get("finalizando", False)
pulverizador_automatico = ContextoGlobalRedis.get_controle().get("pulverizador_automatico", False)
if status_operacao == StatusOperacao.EmAndamento and not finalizando:
atuacao_bicos = analise.get("controle")
else:
atuacao_bicos = {i: False for i in analise.get("controle", {}).keys()}
analise["controle"] = atuacao_bicos
self._ultimo_controle = atuacao_bicos
ContextoGlobalRedis.atualizar_ctx_dict(
CtxKey.DadosControle,
controle_bicos=atuacao_bicos
)
if pulverizador_automatico:
ContextoGlobalRedis.publicar_comando(CmdKey.WeedWorkerTx, { "cmd": WeedWorkerCommandType.EnviarDadosControle.value, "params": atuacao_bicos })
predictions, ts, res = self.get_segmentation_predictions()
if ts == self._ts_segmentacao_anterior:
return # já analisado
fps = 1.0 / (ts - self._ts_segmentacao_anterior)
self._ts_segmentacao_anterior = ts
if predictions is not None:
rgb_frame, ts_frame, res_frame = self.get_rgb_frame()
analise_completa = self.detectar_ervas(predictions, rgb_frame)
analise = analise_completa.get("dados_visuais", {})
#self.mostrar_log(f"Deteccoes no radar: {len(analise.get('deteccoes', []))}")
ContextoGlobalRedis.atualizar_ctx_dict(
CtxKey.DadosWeedWorker,
ts_analise=ts,
fps_model=fps,
analise=converter_valores_numpy(analise)
)
if status_operacao == StatusOperacao.EmAndamento and not finalizando:
atuacao_bicos = analise.get("controle")
else:
atuacao_bicos = {i: False for i in analise.get("controle", {}).keys()}
analise["controle"] = atuacao_bicos
ContextoGlobalRedis.atualizar_ctx_dict(
CtxKey.DadosControle,
controle_bicos=atuacao_bicos
)
if pulverizador_automatico:
ContextoGlobalRedis.publicar_comando(CmdKey.WeedWorkerTx, { "cmd": WeedWorkerCommandType.EnviarDadosControle.value, "params": atuacao_bicos })
self._ultima_analise = analise_completa.copy()
self._ultima_analise = analise_completa.copy()
except Exception as e:
self.mostrar_log(f"❌ Erro na segmentacao semantica: {e}")
finally:
self._ts_ultima_analise = t0
self._analisando_segmentacao = False
def detectar_ervas(self, predictions, rgb_frame):
if self.weed_detector is None:
@ -224,31 +278,18 @@ class CameraManager:
if nome is not None and nome != "":
frame_name = nome
if CameraFrameType.Rgb.value in tipos:
rgb_frame, rgb_ts, rgb_res = self.get_rgb_frame()
if rgb_frame is not None and rgb_res["frame_valido"]:
nome_rgb = f"{frame_name}_rgb.jpeg"
caminho_rgb = os.path.join(pasta, nome_rgb)
cv2.imwrite(caminho_rgb, rgb_frame)
frames_salvos.append(caminho_rgb)
if CameraFrameType.Segmentacao.value in tipos:
frame = self._salvar_frame_analise(frame_name, self._ultima_analise, "segmentacao", pasta)
if frame is not None:
frames_salvos.append(frame)
for _frame_type in tipos:
_frame = self.get_selected_frame(_frame_type)
if _frame is not None and _frame.size > 0:
nome_frame = f"{frame_name}_{(TipoFrameCamera(_frame_type)).name}.jpeg"
caminho = os.path.join(pasta, nome_frame)
cv2.imwrite(caminho, _frame)
frames_salvos.append(caminho)
return frames_salvos
except Exception as e:
self.mostrar_log(f"❌ Erro ao salvar frames: {e}")
return []
def _salvar_frame_analise(self, frame_name, analise, sulfix, pasta):
base64frame = analise.get("frame", {}).get("frame")
frame = decode_image_base64(base64frame)
if frame is not None:
nome_analise = f"{frame_name}_{sulfix}.jpeg"
caminho_analise = os.path.join(pasta, nome_analise)
cv2.imwrite(caminho_analise, frame)
return caminho_analise
return None

View File

@ -80,7 +80,9 @@ def load_seg_config(force_reload=False):
"max_area_frac": 0.2,
"ia_roi_begin": 0.0,
"ia_roi_size": 1.0,
"ia_resolution": [512,288],
"ia_resolution": [672,544],
"ia_channels": 5,
"ia_use_ndvi": True,
"erva_top_band_frac": 0.30,
"erva_frac_ema": 0.3,
"erva_thresh_vel_gain": 0.4,
@ -91,8 +93,6 @@ def load_seg_config(force_reload=False):
"min_frac_erva_por_bico": 0.02,
"usar_morfologia": True,
"kernel_morf": 3,
"seg_every_n": 1,
"det_every_n": 0,
"usar_radar_global_gate": True,
"max_frac_cana_por_bico": 0.009,
@ -109,9 +109,12 @@ def load_seg_config(force_reload=False):
_CONFIG_CACHE["qtd_bicos"] = dadosAtu.get("qtd_bicos", 4)
_CONFIG_CACHE["velocidade_robo"] = contexto.get("Gerais", {}).get("velocidade_ms", 0.0)
_CONFIG_CACHE["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ervas", "C:/AgroBaseModels/Ervas/model-2_1.blob")
_CONFIG_CACHE["ia_labelmap_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_labelmap_ervas", "C:/AgroBaseModels/Ervas/model-2_1.txt")
_CONFIG_CACHE["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ervas")
_CONFIG_CACHE["ia_labelmap_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_labelmap_ervas")
_CONFIG_CACHE["ia_backbone"] = ContextoGlobalRedis.get_equipamento().get("ia_backbone_ervas")
_CONFIG_CACHE["ia_norm_stats_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_norm_stats_ervas")
_CONFIG_CACHE["faixa_atuacao_bicos"] = dadosAtu.get("percent_vertical_deteccao", 0.7)
_CONFIG_CACHE["area_atuacao_bicos"] = dadosAtu.get("height_area_deteccao", 0.1)
_CONFIG_CACHE["min_frac_erva_por_bico_on"] = dadosAtu.get("pct_erva_bico_on", 0.02)
_CONFIG_CACHE["min_frac_erva_por_bico_off"] = dadosAtu.get("pct_erva_bico_off", 0.01)

View File

@ -8,7 +8,8 @@ def main():
print("🔎 Caminho sys.path:", os.path.abspath(os.path.join(os.path.dirname(__file__), "..")), "WeedWorker")
from weed_worker.config import mostrar_log, get_camera_manager, iniciar_camera_manager
from shared.enums import WeedWorkerCommandType, CameraFrameType
from shared.enums import WeedWorkerCommandType, TipoFrameCamera
from shared.utils import encode_image_base64
from shared.contexto_global_redis import ContextoGlobalRedis, CmdKey, CtxKey
def loop_ativo():
@ -46,8 +47,9 @@ def main():
elif acao == WeedWorkerCommandType.AtualizarSaudeCamera:
get_camera_manager().atualizar_saude_camera()
elif acao == WeedWorkerCommandType.GetCameraFrame:
tipo = CameraFrameType(dados.get("params", CameraFrameType.Rgb.value))
base64_img, ts = get_camera_manager().get_select_frame(tipo)
tipo = TipoFrameCamera(dados.get("params", TipoFrameCamera.Rgb.value))
frame, ts = get_camera_manager().get_selected_frame(tipo)
base64_img = encode_image_base64(frame)
if base64_img is not None:
resposta = {
"frame": base64_img,

View File

@ -5,9 +5,9 @@ import numpy as np
from shared.utils import encode_image_base64
class ClassesSegmentacao(IntEnum):
ERVA = 0
CHAO = 0
CANA = 1
CHAO = 2
ERVA = 2
class WeedDetector:
def __init__(self, color_map, classes):
@ -30,7 +30,7 @@ class WeedDetector:
self.img_mock = "C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\bin\\x64\\Debug\\Operacoes\\25_07_2025_14_39_14\\Cam0\\85_rgb.jpeg"
self.predictions = None
self.dados_visuais = {}
self._dbg_img_shape = (640, 360)
self._dbg_img_shape = (706, 560)
self._mostrar_debug = True
self._dbg_last_ts = None
@ -131,8 +131,8 @@ class WeedDetector:
}
}
frame = rgb_frame if rgb_frame is not None else resultado["mask_color"]
self._mostrar_debug_bicos(frame, resultado["classes"], [], controle_bicos, config)
#frame = rgb_frame if rgb_frame is not None else resultado["mask_color"]
#self._mostrar_debug_bicos(frame, resultado["classes"], [], controle_bicos, config)
return resultado
@ -426,6 +426,141 @@ class WeedDetector:
except Exception as e:
print(f"Erro ao mostrar debug: {e}")
def _mostrar_debug_bicos_overlay(self, overlay_bgr, detections, atuacao_bicos, config=None, show: bool = True):
"""
Versão otimizada do debug: recebe o overlay BGR montado
(RGB + segmentação) e desenha faixa, bicos, bboxes e HUD.
"""
try:
dbg_fps = self._fps_update('_dbg_last_ts', '_dbg_fps_ema')
if not self._mostrar_debug:
# print(f"FPS dbg: {dbg_fps:.2f}")
return
if overlay_bgr is None:
return
# --- cache/config ---
if config is None:
from weed_worker.config import load_seg_config
config = load_seg_config()
qtd_bicos = int(config.get("qtd_bicos", 0))
zona_inicio = float(config.get("faixa_atuacao_bicos", 0.2))
faixa_atuacao = float(config.get("area_atuacao_bicos", 0.3))
# --- alocação única dos buffers de debug ---
if not hasattr(self, "_dbg_img") or self._dbg_img.shape[:2] != (self._dbg_img_shape[1], self._dbg_img_shape[0]):
# _dbg_img_shape = (W, H)
self._dbg_img = np.empty((self._dbg_img_shape[1], self._dbg_img_shape[0], 3), dtype=np.uint8)
self._layer = np.zeros_like(self._dbg_img)
# --- copia/resize do overlay para _dbg_img ---
h_src, w_src = overlay_bgr.shape[:2]
W, H = self._dbg_img_shape # (W, H)
if (w_src, h_src) != (W, H):
# redimensiona overlay para o tamanho de debug
cv2.resize(overlay_bgr, (W, H), dst=self._dbg_img, interpolation=cv2.INTER_AREA)
else:
# mesmo tamanho, só copia
self._dbg_img[...] = overlay_bgr
# a partir daqui, igualzinho ao método antigo, só usando _dbg_img como base
largura_bico = W / float(max(qtd_bicos, 1))
# --- faixa de atuação ---
y_inicio = int((1.0 - zona_inicio) * H)
y_fim = int((1.0 - (zona_inicio + faixa_atuacao)) * H)
y0, y1 = min(y_inicio, y_fim), max(y_inicio, y_fim)
self._layer.fill(0)
cv2.rectangle(self._layer, (0, y0), (W, y1), (220, 220, 100), thickness=-1)
cv2.addWeighted(self._layer, 0.18, self._dbg_img, 0.82, 0, dst=self._dbg_img)
cv2.rectangle(self._dbg_img, (0, y0), (W, y1), (180, 180, 80), 2)
cv2.putText(self._dbg_img, "Zona de Atuacao", (10, max(0, y0 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (180, 180, 80), 2)
# --- bicos ---
self._layer.fill(0)
CORES = [ (0,255,0), (255,0,0), (0,255,255), (255,128,0), (255,0,255), (0,128,255), (128,255,0), (0,0,255) ]
for i in range(qtd_bicos):
x0 = int(i * largura_bico)
x1 = int((i + 1) * largura_bico)
cor = CORES[i % len(CORES)]
if atuacao_bicos.get(i, False):
cv2.rectangle(self._layer, (x0, y0), (x1, y1), cor, thickness=-1)
cv2.addWeighted(self._layer, 0.15, self._dbg_img, 0.85, 0, dst=self._dbg_img)
# bordas + texto ON/OFF
for i in range(qtd_bicos):
x0 = int(i * largura_bico)
x1 = int((i + 1) * largura_bico)
cor = CORES[i % len(CORES)]
cv2.rectangle(self._dbg_img, (x0, y0), (x1, y1), cor, 1)
status = "ON" if atuacao_bicos.get(i, False) else "OFF"
cv2.putText(
self._dbg_img,
f"Bico {i} {status}",
(x0 + 5, min(H - 5, y1 + 20)),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
cor,
2
)
# --- bboxes (escala pro debug HxW) ---
# self.resolucao = (W_src_model, H_src_model)
sx = W / float(self.resolucao[0])
sy = H / float(self.resolucao[1])
for det in detections:
bx, by, bw, bh = det["bbox"]
x = int(bx * sx)
y = int(by * sy)
w = int(bw * sx)
h = int(bh * sy)
bbox_cor = (0, 0, 255)
cv2.rectangle(self._dbg_img, (x, y), (x + w, y + h), bbox_cor, 2)
cv2.putText(
self._dbg_img,
f'ID:{det.get("id")} {det.get("descricao","erva")} {det.get("confianca",0):.2f}',
(x, max(0, y - 5)),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
bbox_cor,
2
)
# --- HUD ---
cv2.putText(
self._dbg_img,
f"Ervas no radar: {'Sim' if self._ervas_no_radar else 'Nao'} ({(self._ervas_no_radar_percent * 100.0):.2f}%)",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(0, 255, 0),
2
)
cv2.putText(
self._dbg_img,
f"Dbg FPS: {dbg_fps:.1f}",
(10, 60),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(0, 255, 0),
2
)
if (show):
cv2.imshow("Debug Weed Worker", self._dbg_img)
cv2.waitKey(1)
except Exception as e:
print(f"Erro ao mostrar debug (overlay): {e}")
def _fps_update(self, last_ts_attr: str, ema_attr: str, alpha: float = 0.2):
"""Atualiza e retorna FPS (EMA) baseado no timestamp anterior salvo em self"""
import time

View File

@ -274,13 +274,14 @@ namespace OperationControl.Models
});
}
public static void EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code disp, bool ligar)
public static void EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code disp, bool ligar, AgroBase.Models.Enums.TipoFrameCamera tipoFrame)
{
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
{
Momento = DateTime.Now,
Dispositivo = disp,
Tecla = ligar ? AgroBase.Models.Enums.BotoesJoystick.L1 : AgroBase.Models.Enums.BotoesJoystick.Vazio
Tecla = ligar ? AgroBase.Models.Enums.BotoesJoystick.L1 : AgroBase.Models.Enums.BotoesJoystick.Vazio,
_comp_value = tipoFrame
});
}

View File

@ -827,6 +827,7 @@
<Canvas>
<Label Content="Câmera frontal | 12 fps | 230 kbps | 0,00% cana | 43 cm - 27 cm | 12,95° | CaminhandoRua" FontSize="10" HorizontalAlignment="Center" VerticalAlignment="Top" Canvas.Top="-3"/>
<vlc:VideoView x:Name="VideoFront" Background="Black" Height="237" Width="438" HorizontalAlignment="Center" Canvas.Top="25" VerticalAlignment="Top" Canvas.Left="10"/>
<ComboBox x:Name="cmbCameraFrontalFrameTipo" Canvas.Left="10" Canvas.Top="25" HorizontalAlignment="Left" VerticalAlignment="Center" Width="100" />
</Canvas>
</Border>
@ -834,6 +835,7 @@
<Canvas>
<Label Content="Câmera traseira | 10 fps | 193 kbps | 5,41% erva | 0,00% cana" FontSize="10" HorizontalAlignment="Center" VerticalAlignment="Top" Canvas.Top="-3"/>
<vlc:VideoView x:Name="VideoWeed" Background="Black" Height="200" Width="438" HorizontalAlignment="Center" Canvas.Top="25" VerticalAlignment="Top" Canvas.Left="10"/>
<ComboBox x:Name="cmbCameraTraseiraFrameTipo" Canvas.Left="10" Canvas.Top="25" HorizontalAlignment="Left" VerticalAlignment="Center" Width="100" />
</Canvas>
</Border>

View File

@ -53,6 +53,16 @@ namespace OperationControl.Windows
cmbNpc_CoolerModo.Items.Add("Manual");
cmbNpc_CoolerModo.SelectedIndex = 0;
cmbCameraFrontalFrameTipo.Items.Clear();
foreach (var m in Enum.GetNames(typeof(AgroBase.Models.Enums.TipoFrameCamera)))
cmbCameraFrontalFrameTipo.Items.Add(m);
cmbCameraFrontalFrameTipo.SelectedIndex = 0;
cmbCameraTraseiraFrameTipo.Items.Clear();
foreach (var m in Enum.GetNames(typeof(AgroBase.Models.Enums.TipoFrameCamera)))
cmbCameraTraseiraFrameTipo.Items.Add(m);
cmbCameraTraseiraFrameTipo.SelectedIndex = 0;
AtualizarListaDispositivos(new List<string>() { VariaveisControleOperacao.SelectedRoverId });
@ -1936,7 +1946,7 @@ namespace OperationControl.Windows
_playerFront.Play(_mediaFront);
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, true);
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, true, ((AgroBase.Models.Enums.TipoFrameCamera)cmbCameraFrontalFrameTipo.SelectedIndex));
}
private void PararStreamCameraFrontal(bool finalizar = false)
@ -1951,7 +1961,7 @@ namespace OperationControl.Windows
_playerFront = null;
}
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, false);
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, false, ((AgroBase.Models.Enums.TipoFrameCamera)cmbCameraFrontalFrameTipo.SelectedIndex));
}
#endregion
@ -1982,7 +1992,7 @@ namespace OperationControl.Windows
_playerWeed.Play(_mediaWeed);
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, true);
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, true, ((AgroBase.Models.Enums.TipoFrameCamera)cmbCameraTraseiraFrameTipo.SelectedIndex));
}
private void PararStreamCameraErvas(bool finalizar = false)
@ -1997,7 +2007,7 @@ namespace OperationControl.Windows
_playerWeed = null;
}
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, false);
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, false, ((AgroBase.Models.Enums.TipoFrameCamera)cmbCameraTraseiraFrameTipo.SelectedIndex));
}
#endregion

View File

@ -14,6 +14,7 @@ import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from torch.amp import autocast, GradScaler
import torch.nn.functional as F
from raw_segformer_service import (RawSegDataset, normalize_raw, patch_segformer_input_channels, build_raw_segformer_model)
@ -128,6 +129,51 @@ def default_collate(batch):
# ----------------------------
# Train/Val loop
# ----------------------------
def dice_loss(
logits: torch.Tensor, # (B, C, H, W)
target: torch.Tensor, # (B, H, W) com ids
num_classes: int,
ignore_index: int = -100,
smooth: float = 1.0
) -> torch.Tensor:
"""
Dice loss multi-classe (macro) com suporte a ignore_index.
Retorna um escalar.
"""
# Softmax -> probabilidades
probs = torch.softmax(logits, dim=1) # (B,C,H,W)
# Máscara de pixels válidos
valid = (target != ignore_index) # (B,H,W)
# Se por algum motivo não tem pixel válido no batch
if valid.sum() == 0:
return logits.new_tensor(0.0)
# One-hot do target (clamp só pra evitar index inválido)
target_clamped = target.clone()
target_clamped[~valid] = 0 # coloca qualquer classe nos ignore, pois vamos mascarar depois
# GARANTIR tipo inteiro pra one_hot
target_clamped = target_clamped.long()
target_1h = F.one_hot(target_clamped, num_classes=num_classes) # (B,H,W,C)
target_1h = target_1h.permute(0, 3, 1, 2).float() # (B,C,H,W)
# Aplica máscara de validade em probs e target
valid_f = valid.unsqueeze(1).float() # (B,1,H,W)
probs = probs * valid_f
target_1h = target_1h * valid_f
# Dice por classe (macro)
dims = (0, 2, 3) # soma em batch e espaço
inter = (probs * target_1h).sum(dims)
den = probs.sum(dims) + target_1h.sum(dims)
dice_per_class = (2.0 * inter + smooth) / (den + smooth) # (C,)
loss = 1.0 - dice_per_class.mean()
return loss
def run_one_epoch(
model: nn.Module,
loader: DataLoader,
@ -176,7 +222,11 @@ def run_one_epoch(
align_corners=False,
)
loss = criterion(logits, masks)
ce = criterion(logits, masks) # CrossEntropyLoss com weight/ignore_index
dice = dice_loss(logits=logits, target=masks, num_classes=num_classes, ignore_index=ignore_index, smooth=1.0)
loss = 0.7 * ce + 0.3 * dice
#loss = criterion(logits, masks)
if train and grad_accum > 1:
loss = loss / grad_accum
@ -259,6 +309,7 @@ def main():
parser.add_argument("--save_every", type=int, default=10)
parser.add_argument("--resume", action="store_true")
parser.add_argument("--resume_ckpt", type=str, default=None)
parser.add_argument("--amp", action="store_true")
parser.add_argument("--amp_val", action="store_true")
@ -511,9 +562,11 @@ def main():
best_miou = -1.0
best_main_iou = -1.0
if args.resume and os.path.exists(last_ckpt_path):
resume_path = args.resume_ckpt if args.resume_ckpt is not None else last_ckpt_path
if args.resume and os.path.exists(resume_path):
ckpt = load_checkpoint(
last_ckpt_path,
resume_path,
model,
optimizer,
scaler=scaler,
@ -522,7 +575,7 @@ def main():
start_epoch = int(ckpt["epoch"]) + 1
best_miou = float(ckpt.get("best_miou", -1.0))
best_main_iou = float(ckpt.get("best_main_iou", -1.0))
print(f"[RESUME] epoch={start_epoch} best_miou={best_miou:.4f} best_main_iou={best_main_iou:.4f}")
print(f"[RESUME] epoch={start_epoch} best_miou={best_miou:.4f} best_main_iou={best_main_iou:.4f} {resume_path}")
def pretty_iou(iou_list):
return " | ".join(

View File

@ -226,8 +226,8 @@ def main():
config = json.load(f)
MODELO = config["camera"] # ex: "gal5000"
#MODEL_NAME = config["model_name"] # ex: "segformer_b0"
MODEL_NAME = "pulv2_1008x800"
MODEL_NAME = config["model_name"] # ex: "segformer_b0"
#MODEL_NAME = "pulv_s3_1008x800"
modelo_folder = config["modelo"] # ex: "weed_1008x800"
USE_NDVI = bool(config.get("use_ndvi", False))

View File

@ -1,9 +1,9 @@
{
"camera": "gal5000",
"modelo": "segformer_b0",
"model_name": "pulv_n_1008x800",
"modelo": "segformer_b1",
"model_name": "pulv_ref",
"dual_head": false,
"main_class_name": "pulverizar",
"main_class_name": "cana",
"es_classes": "",
"model_to_use": "geral",
"raw_size": [1296, 1028],
@ -13,5 +13,5 @@
"shaves": 3,
"channels": 5,
"use_ndvi": true,
"backbone": "nvidia/segformer-b0-finetuned-ade-512-512"
"backbone": "nvidia/segformer-b1-finetuned-ade-512-512"
}

View File

@ -1,4 +1,5 @@
# label:color_rgb:parts:actions
naopulverizar:128,0,0::
pulverizar:0,128,0::
chao:128,0,0::
cana:0,0,128::
erva:0,128,0::
ignore:255,255,255::

View File

@ -1,4 +0,0 @@
# label:color_rgb:parts:actions
naopulverizar:128,0,0::
pulverizar:0,128,0::
ignore:255,255,255::

View File

@ -0,0 +1,344 @@
import json
import os
import argparse
from pathlib import Path
import numpy as np
import cv2
from PIL import Image, ImageDraw
import matplotlib.pyplot as plt
from utils import carregar_labelmap_completo
# =========================
# CONFIGURAÇÃO DE CLASSES
# =========================
# Ajuste aqui conforme suas máscaras:
# - Se sua máscara for "indexada" (modo P) ou grayscale com IDs por pixel:
# class_ids = {0: 1, 1: 2} # exemplo: erva=1, cana=2
# - Se sua máscara for RGB com cores fixas:
# class_colors = {0: (0,255,0), 1: (0,0,255)} # exemplo
#
# Por padrão abaixo: tenta RGB primeiro; se a máscara vier indexada, usa IDs.
def build_maps_from_labelmap(alpha: int = 90, ignore_names=None):
"""
labelmap e constrói maps dinâmicos:
- class_names: {new_id: name}
- class_colors_rgb: {new_id: (r,g,b)}
- class_ids: {new_id: new_id} (para máscaras indexed alinhadas com o new_id)
- overlay_rgba: {new_id: (r,g,b,alpha)}
- ignore_rgb: cor da classe "ignore" (se existir no labelmap original)
- id_old_to_new: {old_id: new_id} (útil se sua máscara indexed usa ids antigos)
- id_new_to_old: {new_id: old_id}
"""
if ignore_names is None:
ignore_names = []
ignore_set = {n.strip().lower() for n in ignore_names if n and n.strip()}
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config["camera"]
labelmap_path = os.path.join(MODELO, "dataset", "labelmap.txt")
# Lê tudo do labelmap (mantém seus ids originais)
cor_para_id, cores_rgb, id_para_nome, ignore_rgb = carregar_labelmap_completo(labelmap_path)
# --- Filtra classes por nome ---
kept_old_ids = []
for old_id, name in id_para_nome.items():
if name.strip().lower() in ignore_set:
continue
kept_old_ids.append(old_id)
# Reindexa para ficar 0..N-1
kept_old_ids = sorted(kept_old_ids)
id_old_to_new = {old_id: new_id for new_id, old_id in enumerate(kept_old_ids)}
id_new_to_old = {new_id: old_id for old_id, new_id in id_old_to_new.items()}
# Constrói maps novos (compactos)
class_names = {}
class_colors_rgb = {}
overlay_rgba = {}
# cor_para_id: { (r,g,b): old_id }
# id_para_nome: { old_id: name }
for cor_rgb, old_id in cor_para_id.items():
if old_id not in id_old_to_new:
continue
new_id = id_old_to_new[old_id]
class_names[new_id] = id_para_nome[old_id]
class_colors_rgb[new_id] = cor_rgb
overlay_rgba[new_id] = (cor_rgb[0], cor_rgb[1], cor_rgb[2], alpha)
# Para máscara indexed:
# - Se sua máscara indexed já usa os IDs NOVOS (compactos), isso aqui está ok.
# - Se ela usa IDs ANTIGOS, você precisa mapear (old -> new) antes de extrair polígonos.
class_ids = {new_id: new_id for new_id in class_names.keys()}
return {
"class_names": class_names,
"class_colors_rgb": class_colors_rgb,
"class_ids": class_ids,
"overlay_rgba": overlay_rgba,
"ignore_rgb": ignore_rgb,
"id_old_to_new": id_old_to_new,
"id_new_to_old": id_new_to_old,
"labelmap_path": labelmap_path,
}
maps = build_maps_from_labelmap(ignore_names=["chao", "ignore"])
CLASS_NAMES = maps["class_names"]
CLASS_IDS = maps["class_ids"]
CLASS_COLORS_RGB = maps["class_colors_rgb"]
OVERLAY_RGBA = maps["overlay_rgba"]
IGNORE_RGB = maps["ignore_rgb"]
# =========================
# UTILITÁRIOS
# =========================
def imread_unicode(path: Path) -> np.ndarray:
"""Lê imagem com caminho unicode no Windows."""
data = np.fromfile(str(path), dtype=np.uint8)
img = cv2.imdecode(data, cv2.IMREAD_UNCHANGED)
return img
def load_mask(mask_path: Path):
"""
Retorna:
mask_type: 'indexed' ou 'rgb'
mask_data:
- indexed: np.ndarray (H,W) int
- rgb: np.ndarray (H,W,3) uint8 em RGB
"""
pil = Image.open(mask_path)
if pil.mode == "P":
arr = np.array(pil, dtype=np.int32)
return "indexed", arr
if pil.mode in ("L", "I;16"):
arr = np.array(pil, dtype=np.int32)
return "indexed", arr
# RGB/RGBA
pil = pil.convert("RGBA")
rgba = np.array(pil, dtype=np.uint8)
rgb = rgba[:, :, :3]
return "rgb", rgb
def class_binary_from_mask(mask_type, mask_data, cls, class_ids, class_colors, rgb_tol=10):
"""Gera máscara binária (uint8 0/255) para uma classe."""
if mask_type == "indexed":
target_id = class_ids[cls]
bin_mask = (mask_data == target_id).astype(np.uint8) * 255
return bin_mask
# rgb
target = np.array(class_colors[cls], dtype=np.int16)
img = mask_data.astype(np.int16)
diff = np.abs(img - target[None, None, :])
ok = (diff[:, :, 0] <= rgb_tol) & (diff[:, :, 1] <= rgb_tol) & (diff[:, :, 2] <= rgb_tol)
return ok.astype(np.uint8) * 255
def simplify_contour(cnt, epsilon_px=1.0, epsilon_rel=0.001):
peri = cv2.arcLength(cnt, True)
eps = max(epsilon_px, epsilon_rel * peri)
return cv2.approxPolyDP(cnt, eps, True)
def contours_to_polygons(bin_mask, min_area_px=50, epsilon_px=2.0):
"""
bin_mask: uint8 0/255
Retorna lista de polígonos, cada um como array (N,2) em pixels (float).
"""
# limpa ruído e fecha pequenos buracos
kernel = np.ones((3, 3), np.uint8)
m = cv2.morphologyEx(bin_mask, cv2.MORPH_OPEN, kernel, iterations=1)
m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, kernel, iterations=1)
contours, _hier = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
polys = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area_px:
continue
approx = simplify_contour(cnt, epsilon_px=epsilon_px)
if len(approx) < 3:
continue
pts = approx.reshape(-1, 2).astype(np.float32)
polys.append(pts)
return polys
def polygon_px_to_yolo(poly_px, w, h):
"""(N,2) px -> lista [x1,y1,x2,y2,...] normalizada 0..1"""
xs = np.clip(poly_px[:, 0] / float(w), 0.0, 1.0)
ys = np.clip(poly_px[:, 1] / float(h), 0.0, 1.0)
coords = []
for x, y in zip(xs, ys):
coords.append(float(x))
coords.append(float(y))
return coords
def draw_polygons_on_preview(preview_path: Path, polygons_by_class, out_path: Path):
"""Cria overlay (PIL) com polígonos extraídos por cima do preview."""
img = Image.open(preview_path).convert("RGB")
w, h = img.size
draw = ImageDraw.Draw(img, "RGBA")
for cls, polys in polygons_by_class.items():
color = OVERLAY_RGBA.get(cls, (255, 255, 255, 90))
outline = color[:3] + (255,)
for poly in polys:
pts = [(float(x), float(y)) for x, y in poly]
if len(pts) >= 3:
draw.polygon(pts, fill=color, outline=outline)
img.save(out_path)
def make_triview(preview_path: Path, mask_path: Path, overlay_path: Path, out_path: Path, title: str = ""):
"""Salva uma imagem com 3 colunas: preview | mask | overlay."""
prev = Image.open(preview_path).convert("RGB")
msk = Image.open(mask_path).convert("RGB")
ovl = Image.open(overlay_path).convert("RGB")
fig = plt.figure(figsize=(16, 6))
fig.suptitle(title, fontsize=12)
ax1 = fig.add_subplot(1, 3, 1)
ax1.imshow(prev)
ax1.set_title("Preview")
ax1.axis("off")
ax2 = fig.add_subplot(1, 3, 2)
ax2.imshow(msk)
ax2.set_title("Mask (manual)")
ax2.axis("off")
ax3 = fig.add_subplot(1, 3, 3)
ax3.imshow(ovl)
ax3.set_title("Overlay (polígonos extraídos)")
ax3.axis("off")
plt.tight_layout()
fig.savefig(out_path, dpi=140)
plt.close(fig)
def find_matching_preview(previews_dir: Path, stem: str):
"""Procura preview com mesmo stem em extensões comuns."""
for ext in [".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"]:
p = previews_dir / f"{stem}{ext}"
if p.exists():
return p
return None
# =========================
# PIPELINE PRINCIPAL
# =========================
def process_dataset(root_dir: Path, out_labels_dir: Path, out_vis_dir: Path,
class_ids, class_colors, rgb_tol=10,
min_area_px=50, epsilon_px=2.0):
previews_dir = root_dir / "previews"
masks_dir = root_dir / "masks"
if not previews_dir.exists() or not masks_dir.exists():
raise FileNotFoundError(f"Esperado encontrar previews/ e masks/ dentro de {root_dir}")
out_labels_dir.mkdir(parents=True, exist_ok=True)
out_vis_dir.mkdir(parents=True, exist_ok=True)
mask_files = sorted(list(masks_dir.glob("*.png")) + list(masks_dir.glob("*.jpg")) + list(masks_dir.glob("*.jpeg")))
if not mask_files:
print(f"[WARN] Nenhuma máscara encontrada em: {masks_dir}")
return
total = 0
for mask_path in mask_files:
stem = mask_path.stem
preview_path = find_matching_preview(previews_dir, stem)
if preview_path is None:
print(f"[WARN] Sem preview para máscara: {mask_path.name}")
continue
# tamanhos
prev_img = Image.open(preview_path)
w, h = prev_img.size
mask_type, mask_data = load_mask(mask_path)
polygons_by_class = {}
yolo_lines = []
for cls in sorted(CLASS_NAMES.keys()):
if cls not in class_ids or cls not in class_colors:
continue
bin_mask = class_binary_from_mask(mask_type, mask_data, cls, class_ids, class_colors, rgb_tol=rgb_tol)
polys = contours_to_polygons(bin_mask, min_area_px=min_area_px, epsilon_px=epsilon_px)
if not polys:
continue
polygons_by_class[cls] = polys
for poly_px in polys:
coords = polygon_px_to_yolo(poly_px, w, h)
# YOLOv8-seg exige pelo menos 3 pontos (6 nums)
if len(coords) >= 6:
line = str(cls) + " " + " ".join(f"{v:.6f}" for v in coords)
yolo_lines.append(line)
# salva label
label_path = out_labels_dir / f"{stem}.txt"
label_path.write_text("\n".join(yolo_lines) + ("\n" if yolo_lines else ""), encoding="utf-8")
# gera overlay e triview
overlay_path = out_vis_dir / f"{stem}_overlay.png"
triview_path = out_vis_dir / f"{stem}_triview.png"
draw_polygons_on_preview(preview_path, polygons_by_class, overlay_path)
make_triview(preview_path, mask_path, overlay_path, triview_path, title=stem)
total += 1
print(f"[OK] {stem}: polys={sum(len(v) for v in polygons_by_class.values())} -> {label_path.name}")
print(f"\nFeito ✅ Processados: {total} arquivos")
print(f"Labels: {out_labels_dir}")
print(f"Vis: {out_vis_dir}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", type=str, required=True, help="Pasta raiz no formato antigo (contendo previews/ masks/ raws/ metas/)")
ap.add_argument("--rgb_tol", type=int, default=10, help="Tolerância p/ match de cor RGB na máscara")
ap.add_argument("--min_area", type=int, default=50, help="Área mínima (px) pra descartar sujeira")
ap.add_argument("--eps", type=float, default=2.0, help="Epsilon (px) pra simplificar polígonos")
args = ap.parse_args()
root_dir = Path(args.root)
out_labels_dir = Path(f"{args.root}/labels")
out_vis_dir = Path(f"{args.root}/vis")
process_dataset(
root_dir=root_dir,
out_labels_dir=out_labels_dir,
out_vis_dir=out_vis_dir,
class_ids=CLASS_IDS,
class_colors=CLASS_COLORS_RGB,
rgb_tol=args.rgb_tol,
min_area_px=args.min_area,
epsilon_px=args.eps
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,197 @@
from pathlib import Path
from collections import defaultdict
import argparse
import math
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
def is_float(s: str) -> bool:
try:
float(s)
return True
except:
return False
def parse_label_line(line: str):
"""
Suporta:
- YOLO det: cls xc yc w h
- YOLO seg: cls x1 y1 x2 y2 ...
Retorna dict com:
cls (int), kind ("det"|"seg"|None), n_points (int|None)
"""
parts = line.strip().split()
if len(parts) < 2:
return None
if not is_float(parts[0]):
return None
cls = int(float(parts[0]))
nums = []
for p in parts[1:]:
if not is_float(p):
return None
nums.append(float(p))
# det clássico: 4 números
if len(nums) == 4:
return {"cls": cls, "kind": "det", "n_points": None}
# seg: pares (x,y)
if len(nums) >= 6 and (len(nums) % 2 == 0):
return {"cls": cls, "kind": "seg", "n_points": len(nums) // 2}
# caso estranho
return {"cls": cls, "kind": "unknown", "n_points": None}
def analyze_split(labels_dir: Path):
stats = {
"images_total": 0,
"images_bg": 0,
"images_with_obj": 0,
"images_per_class": defaultdict(int), # quantas imagens têm a classe
"instances_per_class": defaultdict(int), # quantas instâncias (linhas) por classe
"kind_counts": defaultdict(int), # det/seg/unknown
"poly_points": [], # lista de n_points (para seg)
"weird_lines": 0,
"empty_label_files": 0,
}
if not labels_dir.exists():
return stats
label_files = sorted(labels_dir.glob("*.txt"))
stats["images_total"] = len(label_files)
for lf in label_files:
text = lf.read_text(encoding="utf-8", errors="ignore").strip()
if not text:
stats["images_bg"] += 1
stats["empty_label_files"] += 1
continue
stats["images_with_obj"] += 1
classes_in_image = set()
for line in text.splitlines():
parsed = parse_label_line(line)
if parsed is None:
stats["weird_lines"] += 1
continue
cls = parsed["cls"]
kind = parsed["kind"]
stats["kind_counts"][kind] += 1
classes_in_image.add(cls)
stats["instances_per_class"][cls] += 1
if kind == "seg" and parsed["n_points"] is not None:
stats["poly_points"].append(parsed["n_points"])
for cls in classes_in_image:
stats["images_per_class"][cls] += 1
return stats
def summarize(stats):
out = []
out.append(f"images_total : {stats['images_total']}")
out.append(f"images_bg : {stats['images_bg']} ({pct(stats['images_bg'], stats['images_total'])})")
out.append(f"images_with_obj : {stats['images_with_obj']} ({pct(stats['images_with_obj'], stats['images_total'])})")
out.append(f"empty_label_files: {stats['empty_label_files']}")
out.append(f"weird_lines : {stats['weird_lines']}")
if stats["kind_counts"]:
out.append("label_kinds : " + ", ".join(f"{k}={v}" for k, v in sorted(stats["kind_counts"].items())))
# imagens por classe
if stats["images_per_class"]:
out.append("images_per_class : " + ", ".join(f"c{c}={n}" for c, n in sorted(stats["images_per_class"].items())))
else:
out.append("images_per_class : (nenhuma)")
# instâncias por classe
if stats["instances_per_class"]:
out.append("inst_per_class : " + ", ".join(f"c{c}={n}" for c, n in sorted(stats["instances_per_class"].items())))
else:
out.append("inst_per_class : (nenhuma)")
# polígonos
if stats["poly_points"]:
pts = stats["poly_points"]
out.append(f"seg_poly_points : count={len(pts)} min={min(pts)} mean={sum(pts)/len(pts):.2f} max={max(pts)}")
else:
out.append("seg_poly_points : (n/a)")
return "\n".join(out)
def pct(a, b):
if b == 0:
return "n/a"
return f"{(100.0*a/b):.1f}%"
def merge_stats(a, b):
"""merge b into a"""
a["images_total"] += b["images_total"]
a["images_bg"] += b["images_bg"]
a["images_with_obj"] += b["images_with_obj"]
a["empty_label_files"] += b["empty_label_files"]
a["weird_lines"] += b["weird_lines"]
for k, v in b["images_per_class"].items():
a["images_per_class"][k] += v
for k, v in b["instances_per_class"].items():
a["instances_per_class"][k] += v
for k, v in b["kind_counts"].items():
a["kind_counts"][k] += v
a["poly_points"].extend(b["poly_points"])
return a
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", type=str, required=True,
help="Pasta base que contém labels/train labels/val labels/test (ou labels direto)")
args = ap.parse_args()
base = Path(args.base)
labels = base / "labels"
splits = []
# padrão YOLO: labels/train, labels/val, labels/test
if (labels / "train").exists() or (labels / "val").exists() or (labels / "test").exists():
splits = ["train", "val", "test"]
split_dirs = {s: labels / s for s in splits}
else:
# fallback: base/labels direto
split_dirs = {"all": labels}
global_stats = {
"images_total": 0,
"images_bg": 0,
"images_with_obj": 0,
"images_per_class": defaultdict(int),
"instances_per_class": defaultdict(int),
"kind_counts": defaultdict(int),
"poly_points": [],
"weird_lines": 0,
"empty_label_files": 0,
}
for name, d in split_dirs.items():
st = analyze_split(d)
print("===================================")
print(f"SPLIT: {name} ({d})")
print(summarize(st))
print("===================================")
merge_stats(global_stats, st)
print("\n========== GLOBAL ==========")
print(summarize(global_stats))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,48 @@
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv8-seg instance segmentation model. For Usage examples see https://docs.ultralytics.com/tasks/segment
# Parameters
nc: 2 # number of classes
ch: 4 # number of channels
scales: # model compound scaling constants, i.e. 'model=yolov8n-seg.yaml' will call yolov8-seg.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.67, 0.75, 768]
#n: [0.33, 0.25, 1024]
#s: [0.33, 0.50, 1024]
#m: [0.67, 0.75, 768]
#l: [1.00, 1.00, 512]
#x: [1.00, 1.25, 512]
# YOLOv8.0n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f, [512]] # 12
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 15 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f, [512]] # 18 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f, [1024]] # 21 (P5/32-large)
- [[15, 18, 21], 1, Segment, [nc, 32, 256]] # Segment(P3, P4, P5)

View File

@ -0,0 +1,48 @@
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv8-seg instance segmentation model. For Usage examples see https://docs.ultralytics.com/tasks/segment
# Parameters
nc: 2 # number of classes
ch: 5 # number of channels
scales: # model compound scaling constants, i.e. 'model=yolov8n-seg.yaml' will call yolov8-seg.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.33, 0.50, 1024]
#n: [0.33, 0.25, 1024]
#s: [0.33, 0.50, 1024]
#m: [0.67, 0.75, 768]
#l: [1.00, 1.00, 512]
#x: [1.00, 1.25, 512]
# YOLOv8.0n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f, [512]] # 12
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 15 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f, [512]] # 18 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f, [1024]] # 21 (P5/32-large)
- [[15, 18, 21], 1, Segment, [nc, 32, 256]] # Segment(P3, P4, P5)

Binary file not shown.

View File

@ -0,0 +1,46 @@
import os
from PIL import Image
# 🟢 CONFIGURA AQUI
PASTA_IMAGENS = r"images/convert" # troque pelo caminho da pasta
QUALIDADE_JPEG = 85 # 8090 costuma ser um bom equilíbrio
def converter_png_para_jpeg(pasta):
for nome_arquivo in os.listdir(pasta):
if not nome_arquivo.lower().endswith(".png"):
continue
caminho_png = os.path.join(pasta, nome_arquivo)
nome_base, _ = os.path.splitext(nome_arquivo)
caminho_jpeg = os.path.join(pasta, nome_base + ".jpg")
print(f"Convertendo: {caminho_png} -> {caminho_jpeg}")
with Image.open(caminho_png) as img:
# Garante que está em RGB (JPEG não suporta transparência)
if img.mode in ("RGBA", "LA"):
# Fundo branco; mude para (0, 0, 0) se quiser fundo preto
fundo = Image.new("RGB", img.size, (255, 255, 255))
fundo.paste(img, mask=img.split()[-1]) # usa o canal alpha como máscara
img = fundo
else:
img = img.convert("RGB")
# Salva como JPEG
img.save(
caminho_jpeg,
"JPEG",
quality=QUALIDADE_JPEG,
optimize=True,
progressive=True,
)
# ⚠️ Se quiser apagar o PNG depois de conferir que ficou ok, descomente:
# os.remove(caminho_png)
if __name__ == "__main__":
converter_png_para_jpeg(PASTA_IMAGENS)
print("Finalizado!")

View File

@ -0,0 +1,107 @@
from pathlib import Path
import math
EPS_DIST = 1e-4 # distância mínima entre pontos
MIN_AREA = 1e-6 # área mínima do polígono
def clamp(v: float) -> float:
# remove -0.0 e garante [0,1]
v = 0.0 if abs(v) < 1e-12 else v
return max(0.0, min(1.0, v))
def polygon_area(pts):
# fórmula do polígono (shoelace)
area = 0.0
for i in range(len(pts)):
x1, y1 = pts[i]
x2, y2 = pts[(i + 1) % len(pts)]
area += x1 * y2 - x2 * y1
return abs(area) * 0.5
def dist(a, b):
return math.hypot(a[0] - b[0], a[1] - b[1])
def dedupe_and_simplify(pts):
# remove pontos duplicados / muito próximos
clean = []
for p in pts:
if not clean or dist(p, clean[-1]) > EPS_DIST:
clean.append(p)
# fecha polígono se necessário
if len(clean) > 2 and dist(clean[0], clean[-1]) < EPS_DIST:
clean.pop()
return clean
def convert_label_file(path: Path):
text = path.read_text().strip()
if not text:
print(f"[WARN] {path.name} vazio, pulando.")
return
new_lines = []
for line in text.splitlines():
parts = line.strip().split()
if len(parts) < 7:
continue # menos que 3 pontos
cls_id = int(float(parts[0]))
nums = list(map(float, parts[1:]))
if len(nums) % 2 != 0:
continue
pts = []
for x, y in zip(nums[0::2], nums[1::2]):
pts.append((clamp(x), clamp(y)))
pts = dedupe_and_simplify(pts)
if len(pts) < 3:
continue
area = polygon_area(pts)
if area < MIN_AREA:
continue
flat = []
for x, y in pts:
flat.append(f"{x:.6f}")
flat.append(f"{y:.6f}")
new_lines.append(" ".join([str(cls_id)] + flat))
if not new_lines:
print(f"[WARN] {path.name} ficou sem polígonos válidos.")
return
# backup
backup = path.with_suffix(path.suffix + ".bak")
if not backup.exists():
backup.write_text(text)
path.write_text("\n".join(new_lines) + "\n")
print(f"[OK] Corrigido: {path.name}")
def main():
root = Path(".").resolve()
for sub in ["labels/train", "labels/val"]:
d = root / sub
if not d.exists():
continue
for txt in sorted(d.glob("*.txt")):
convert_label_file(txt)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,159 @@
# custom_channel_dataset.py
from pathlib import Path
import cv2
import numpy as np
from ultralytics.data.dataset import YOLODataset
from ultralytics.data.augment import Compose, LetterBox, Format
# 🔧 AJUSTE AQUI PRO SEU RAW REAL:
RAW_ALREADY_01 = True
RAW_DTYPE = np.float32 # ou np.uint16, conforme seu arquivo .raw
RAW_MAX_VAL = 1.0 if RAW_ALREADY_01 else 65535.0
class CustomChannelSegDataset(YOLODataset):
"""
Dataset de segmentação para imagens 4-canais (R, G, IR, B).
Convenção:
- O train.txt / val.txt continuam apontando para as IMAGENS RGB normais,
ex: data/images/train/20260126_121341_900.jpg
- Ao lado de cada imagem, você terá um .raw
ex: data/images/train/20260126_121341_900.raw
Esse .raw deve ser (H, W, 4) float32 em [0,1],
com os canais [R, G, IR, B] na ordem que combinamos.
"""
def __init__(self, img_path, data, canais, imgsz, augment, hyp, **kwargs):
self.canais = canais
data = dict(data)
data["channels"] = canais
super().__init__(
img_path=img_path,
data=data,
imgsz=imgsz,
augment=augment,
hyp=hyp,
task="segment",
**kwargs,
)
if len(self.im_files):
p0 = self.im_files[0]
preview0 = cv2.imread(p0, cv2.IMREAD_UNCHANGED)
if preview0 is None:
raise FileNotFoundError(f"Não consegui ler preview: {p0}")
self._fixed_hw = preview0.shape[:2] # (h0, w0)
else:
self._fixed_hw = None
def load_image(self, i):
"""
a imagem a partir do .raw e retorna:
im : np.ndarray uint8 com shape (H, W, C) onde C = 4 ou 5
shapes: (h0, w0) tamanho original
"""
im_path = self.im_files[i] # caminho listado no train/val.txt
# 1) Usa o JPG/PNG só pra descobrir H, W
#preview = cv2.imread(im_path, cv2.IMREAD_UNCHANGED)
#if preview is None:
# raise FileNotFoundError(f"Não consegui ler preview: {im_path}")
#h0, w0 = preview.shape[:2]
h0, w0 = self._fixed_hw
# 2) Caminho do RAW correspondente
p = Path(im_path)
raw_path = p.with_suffix(".raw") # troca extensão por .raw
if not raw_path.exists():
raise FileNotFoundError(f"RAW 4ch não encontrado: {raw_path}")
# 3) Carrega o RAW como CHW (4, H, W)
# IMPORTANTE: o arquivo físico sempre tem 4 canais (R, G, IR, B)
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"Tamanho inesperado no RAW {raw_path}: "
f"esperado {expected_size}, veio {arr_flat.size}"
)
raw_chw = arr_flat.reshape(num_raw_channels, h0, w0) # (4, H, W)
# 4) Converte para HWC float32 (H, W, 4)
arr = np.transpose(raw_chw, (1, 2, 0)).astype(np.float32) # (H, W, 4)
# 5) Normaliza para 0..1 de forma consistente
arr = arr.astype(np.float32, copy=False)
if RAW_ALREADY_01:
arr_norm = arr
else:
# Normalização por escala fixa (ex.: 65535 p/ uint16)
arr_norm = arr / float(RAW_MAX_VAL)
# Segurança: clamp
arr_norm = np.clip(arr_norm, 0.0, 1.0)
# 6) Se self.canais == 5, calculamos NDVI e empilhamos como 5º canal
# Ordem final: [R, G, IR, B, NDVI]
if self.canais == 5:
# índices assumidos: 0=R, 1=G, 2=IR, 3=B
R = arr_norm[..., 0]
IR = arr_norm[..., 2]
# NDVI bruto
eps = 1e-6
ndvi = (IR - R) / (IR + R + eps) # faixa ~[-1, 1]
# clampa e reescala pra [0, 1] (fica alinhado com outros canais)
ndvi = np.clip(ndvi, -1.0, 1.0)
ndvi01 = (ndvi + 1.0) / 2.0
# empilha como 5º canal
arr_norm = np.concatenate(
[arr_norm, ndvi01[..., None]],
axis=-1
) # (H, W, 5)
elif self.canais == 4:
# usa só os 4 canais originais
pass
else:
raise ValueError(f"self.canais deve ser 4 ou 5, veio {self.canais}.")
# 7) Converte de 0..1 para 0..255 uint8 pro YOLO
im = np.clip(arr_norm * 255.0, 0, 255).astype(np.uint8)
#if i == 0:
# print("im dtype:", im.dtype, "min/max:", im.min(), im.max(), "shape:", im.shape)
h, w = im.shape[:2]
return im, (h0, w0), (h, w)
def build_targets(self, batch):
# Garante que as masks e labels aceitem o formato custom
return super().build_targets(batch)
def build_transforms_disabled(self, hyp=None):
# Pega o tamanho da imagem (imgsz)
new_shape = getattr(self, 'imgsz', 800)
return Compose([
# 1. Redimensiona a imagem 4-canais e as masks
LetterBox(new_shape=new_shape),
# 2. Converte tudo para o formato que o Segment Head entende
Format(
bbox_format="xywh", # Formato padrão do YOLO (centro_x, centro_y, largura, altura)
normalize=True, # Normaliza bboxes para 0-1
return_mask=True, # ESSENCIAL para segmentação
batch_idx=True, # Necessário para o DataLoader do Trainer
mask_ratio=4, # Padrão do YOLOv8-seg
mask_overlap=True, # Padrão do YOLOv8-seg
)
])

View File

@ -0,0 +1,13 @@
raw_channels: 4
channels: 5
names:
0: erva
1: cana
colors:
0: (0,128,0)
1: (0,0,128)
size: 864
scale: "s"
path: C:/ZendionInc/agrobot_base/Python/yolov8-seg
train: images/train
val: images/val

378
Python/yolov8-seg/infer.py Normal file
View File

@ -0,0 +1,378 @@
# 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()

View File

@ -0,0 +1,51 @@
from pathlib import Path
# ==============================
# CONFIGURE AQUI
# ==============================
IMAGES_DIR = Path("images/chao") # <-- ALTERAR
OVERWRITE = False # True para sobrescrever labels existentes
# ==============================
def main():
if not IMAGES_DIR.exists():
print(f"[ERRO] Pasta não encontrada: {IMAGES_DIR}")
return
labels_dir = IMAGES_DIR / "labels"
labels_dir.mkdir(exist_ok=True)
image_extensions = {".jpg", ".jpeg", ".png"}
images = [
f for f in IMAGES_DIR.iterdir()
if f.suffix.lower() in image_extensions
]
if not images:
print("[INFO] Nenhuma imagem encontrada.")
return
criados = 0
ignorados = 0
for img in images:
label_path = labels_dir / (img.stem + ".txt")
if label_path.exists() and not OVERWRITE:
ignorados += 1
continue
label_path.write_text("") # cria arquivo vazio
criados += 1
print("===================================")
print(f"Imagens encontradas: {len(images)}")
print(f"Labels criados: {criados}")
print(f"Labels ignorados: {ignorados}")
print("Pasta labels:", labels_dir)
print("===================================")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,48 @@
# patch_model_xch.py
import torch
import torch.nn as nn
def patch_yolov8_first_conv_to_xch(model_or_seq, canais: int = 5):
"""
Aceita:
- SegmentationModel (tem .model)
- nn.Sequential ( é o .model interno)
"""
seq = model_or_seq.model if hasattr(model_or_seq, "model") else model_or_seq
# seq é tipo nn.Sequential; primeiro bloco costuma ser Conv(...)
first = seq[0]
# ultralytics Conv wrapper: first.conv é nn.Conv2d
conv = first.conv
if conv.in_channels == canais:
return # já patchado
old_w = conv.weight.data # [out, in, k, k]
old_in = conv.in_channels
out_ch, _, k1, k2 = old_w.shape
new_conv = nn.Conv2d(
in_channels=canais,
out_channels=conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
dilation=conv.dilation,
groups=conv.groups,
bias=(conv.bias is not None),
padding_mode=conv.padding_mode,
).to(conv.weight.device).type(conv.weight.dtype)
with torch.no_grad():
# copia os canais existentes
new_conv.weight[:, :old_in, :, :] = old_w
# inicializa canais extras com a média dos 3 canais (boa heurística)
if canais > old_in:
mean = old_w.mean(dim=1, keepdim=True) # [out,1,k,k]
new_conv.weight[:, old_in:, :, :] = mean.repeat(1, canais - old_in, 1, 1)
if conv.bias is not None:
new_conv.bias.copy_(conv.bias.data)
# troca a conv dentro do wrapper Ultralytics
first.conv = new_conv

Binary file not shown.

245
Python/yolov8-seg/split.py Normal file
View File

@ -0,0 +1,245 @@
from pathlib import Path
import random
import shutil
from collections import defaultdict
# =========================
# CONFIG
# =========================
BASE_DIR = Path(r".") # <- altere
IMAGES_IN = BASE_DIR / "images" / "original"
LABELS_IN = BASE_DIR / "labels" / "original"
# percentuais (0..1). Tem que somar 1.0
TRAIN_P = 0.70
VAL_P = 0.30
TEST_P = 0.00 # se não quiser test, deixa 0
SEED = 42
COPY_MODE = True # True=copia, False=move
# extensões de imagem aceitas
IMG_EXTS = [".jpg", ".jpeg", ".png"]
# se você quiser limitar a split por classes específicas, informe aqui (opcional)
# ex: {0, 1} para erva/cana. Se None, pega todas que aparecerem.
ALLOWED_CLASSES = None
# =========================
def ensure_dirs(*dirs: Path):
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
def parse_classes_from_label(txt_path: Path):
"""
Retorna um set de classes presentes no arquivo label YOLO-seg.
Se vazio/sem arquivo -> set() (background).
"""
if not txt_path.exists():
return set()
content = txt_path.read_text(encoding="utf-8", errors="ignore").strip()
if not content:
return set()
classes = set()
for line in content.splitlines():
parts = line.strip().split()
if not parts:
continue
try:
cls = int(float(parts[0]))
classes.add(cls)
except:
continue
return classes
def key_from_classes(classes: set):
"""
Cria chave de estrato por combinação de classes.
background => "BG"
ex: {0} => "C0"
{1} => "C1"
{0,1} => "C0_C1"
"""
if not classes:
return "BG"
return "_".join([f"C{c}" for c in sorted(classes)])
def proportional_counts(n, p_train, p_val, p_test):
"""
Converte n e percentuais em contagens inteiras que somam n.
Faz arredondamento e ajusta no final.
"""
t = int(round(n * p_train))
v = int(round(n * p_val))
s = int(round(n * p_test))
# Ajusta para somar n
total = t + v + s
while total != n:
if total > n:
# tira de quem tem mais
if t >= v and t >= s and t > 0:
t -= 1
elif v >= t and v >= s and v > 0:
v -= 1
elif s > 0:
s -= 1
else:
# adiciona em quem tem menos
if t <= v and t <= s:
t += 1
elif v <= t and v <= s:
v += 1
else:
s += 1
total = t + v + s
return t, v, s
def copy_or_move(src: Path, dst: Path, copy_mode=True):
if not src.exists():
return
if copy_mode:
shutil.copy2(src, dst)
else:
shutil.move(src, dst)
def main():
# valida percentuais
psum = TRAIN_P + VAL_P + TEST_P
if abs(psum - 1.0) > 1e-6:
raise ValueError(f"TRAIN_P+VAL_P+TEST_P precisa somar 1.0. Atual: {psum}")
if not IMAGES_IN.exists():
raise FileNotFoundError(f"Pasta não encontrada: {IMAGES_IN}")
if not LABELS_IN.exists():
raise FileNotFoundError(f"Pasta não encontrada: {LABELS_IN}")
# destinos
images_train = BASE_DIR / "images" / "train"
images_val = BASE_DIR / "images" / "val"
images_test = BASE_DIR / "images" / "test"
labels_train = BASE_DIR / "labels" / "train"
labels_val = BASE_DIR / "labels" / "val"
labels_test = BASE_DIR / "labels" / "test"
ensure_dirs(images_train, images_val, images_test, labels_train, labels_val, labels_test)
# lista imagens (baseadas em IMAGES_IN)
imgs = []
for ext in IMG_EXTS:
imgs.extend(IMAGES_IN.glob(f"*{ext}"))
imgs = sorted(imgs)
if not imgs:
print("[INFO] Nenhuma imagem encontrada em:", IMAGES_IN)
return
# monta itens com (stem, img_path, raw_path, label_path, classes, stratum)
items = []
all_classes = set()
for img_path in imgs:
stem = img_path.stem
raw_path = IMAGES_IN / f"{stem}.raw"
label_path = LABELS_IN / f"{stem}.txt"
classes = parse_classes_from_label(label_path)
if ALLOWED_CLASSES is not None:
classes = set(c for c in classes if c in ALLOWED_CLASSES)
all_classes |= classes
stratum = key_from_classes(classes)
items.append({
"stem": stem,
"img": img_path,
"raw": raw_path if raw_path.exists() else None,
"label": label_path if label_path.exists() else None,
"classes": classes,
"stratum": stratum
})
# agrupa por estrato
groups = defaultdict(list)
for it in items:
groups[it["stratum"]].append(it)
random.seed(SEED)
train_set, val_set, test_set = [], [], []
# split estratificado por estrato
for stratum, group_items in groups.items():
random.shuffle(group_items)
n = len(group_items)
nt, nv, ns = proportional_counts(n, TRAIN_P, VAL_P, TEST_P)
train_set.extend(group_items[:nt])
val_set.extend(group_items[nt:nt+nv])
test_set.extend(group_items[nt+nv:nt+nv+ns])
# embaralha cada split (opcional)
random.shuffle(train_set)
random.shuffle(val_set)
random.shuffle(test_set)
def report(split_name, split_items):
counts = defaultdict(int)
for it in split_items:
if not it["classes"]:
counts["BG"] += 1
else:
for c in it["classes"]:
counts[f"C{c}"] += 1
return counts
# Copia/move arquivos
def place(items_list, img_out: Path, lbl_out: Path):
for it in items_list:
# imagem
copy_or_move(it["img"], img_out / it["img"].name, COPY_MODE)
# raw (se existir)
if it["raw"] is not None:
copy_or_move(it["raw"], img_out / it["raw"].name, COPY_MODE)
# label: se não existir, cria vazio (background)
out_label = lbl_out / f"{it['stem']}.txt"
if it["label"] is not None:
copy_or_move(it["label"], out_label, COPY_MODE)
else:
out_label.write_text("", encoding="utf-8")
place(train_set, images_train, labels_train)
place(val_set, images_val, labels_val)
place(test_set, images_test, labels_test)
# relatório
print("========================================")
print("Split concluído ✅")
print(f"Total imagens: {len(items)}")
print(f"Train: {len(train_set)} | Val: {len(val_set)} | Test: {len(test_set)}")
print("----------------------------------------")
print("Classes detectadas no dataset:", sorted(all_classes) if all_classes else "Nenhuma (só BG)")
print("----------------------------------------")
print("Distribuição (contagem de imagens que contém a classe):")
print("Train:", dict(report("train", train_set)))
print("Val :", dict(report("val", val_set)))
print("Test :", dict(report("test", test_set)))
print("----------------------------------------")
print("Pastas destino:")
print(" -", images_train)
print(" -", images_val)
print(" -", images_test)
print(" -", labels_train)
print(" -", labels_val)
print(" -", labels_test)
print("========================================")
if __name__ == "__main__":
main()

170
Python/yolov8-seg/test.py Normal file
View File

@ -0,0 +1,170 @@
import torch
print("version:", torch.__version__)
print("cuda available:", torch.cuda.is_available())
print("device count:", torch.cuda.device_count())
if torch.cuda.is_available():
print("device 0:", torch.cuda.get_device_name(0))
from pathlib import Path
# Ajusta se o caminho base for outro
root = Path(r"C:\ZendionInc\agrobot_base\Python\yolov8-seg")
train_img_dir = root / "images" / "train"
train_lbl_dir = root / "labels" / "train"
print("=== DEBUG TRAIN ===")
print("Pasta imagens:", train_img_dir)
print("Pasta labels :", train_lbl_dir)
# extensões que vamos considerar como imagem
img_exts = {".jpg", ".jpeg", ".png", ".bmp"}
imgs = sorted([p for p in train_img_dir.iterdir() if p.suffix.lower() in img_exts])
print(f"Total de imagens em train: {len(imgs)}")
missing_lbl = []
empty_lbl = []
ok_lbl = []
for img in imgs:
lbl = train_lbl_dir / f"{img.stem}.txt"
if not lbl.exists():
missing_lbl.append(img.name)
else:
content = lbl.read_text(encoding="utf-8").strip()
if not content:
empty_lbl.append(img.name)
else:
ok_lbl.append(img.name)
print("\nImagens COM label não vazio:", len(ok_lbl))
print("Imagens SEM arquivo de label:", len(missing_lbl))
print("Imagens COM label VAZIO:", len(empty_lbl))
if ok_lbl:
exemplo = ok_lbl[0]
exemplo_lbl = train_lbl_dir / f"{Path(exemplo).stem}.txt"
print("\nExemplo de label não vazio (primeira linha):")
print(exemplo_lbl.read_text(encoding="utf-8").splitlines()[0])
else:
print("\nNenhum label não vazio encontrado.")
# Repetir o mesmo para VAL se quiser:
val_img_dir = root / "images" / "val"
val_lbl_dir = root / "labels" / "val"
print("\n=== DEBUG VAL ===")
print("Pasta imagens:", val_img_dir)
print("Pasta labels :", val_lbl_dir)
imgs_val = sorted([p for p in val_img_dir.iterdir() if p.suffix.lower() in img_exts])
print(f"Total de imagens em val: {len(imgs_val)}")
missing_lbl_val = []
empty_lbl_val = []
ok_lbl_val = []
for img in imgs_val:
lbl = val_lbl_dir / f"{img.stem}.txt"
if not lbl.exists():
missing_lbl_val.append(img.name)
else:
content = lbl.read_text(encoding="utf-8").strip()
if not content:
empty_lbl_val.append(img.name)
else:
ok_lbl_val.append(img.name)
print("\nImagens COM label não vazio:", len(ok_lbl_val))
print("Imagens SEM arquivo de label:", len(missing_lbl_val))
print("Imagens COM label VAZIO:", len(empty_lbl_val))
from pathlib import Path
root = Path(r"C:\ZendionInc\agrobot_base\Python\yolov8-seg")
names = {0: "erva", 1: "cana"} # bate com seu data.yaml
nc = len(names)
def checar_pasta(subdir: str):
lbl_dir = root / "labels" / subdir
txts = sorted(lbl_dir.glob("*.txt"))
print(f"\n=== CHECANDO {subdir.upper()} ===")
print(f"Total de arquivos de label: {len(txts)}")
total_linhas = 0
total_objs_validos = 0
total_objs_invalidos = 0
for p in txts:
linhas = p.read_text(encoding="utf-8").strip().splitlines()
if not linhas:
print(f"[VAZIO] {p.name}")
continue
for idx, line in enumerate(linhas):
total_linhas += 1
parts = line.strip().split()
erro = None
# 1) min fields: class + pelo menos 3 pontos (6 coords)
if len(parts) < 1 + 6:
erro = "menos de 3 pontos (precisa >= 7 valores)"
else:
# class
try:
cls = int(parts[0])
except ValueError:
erro = "class não é inteiro"
cls = None
if erro is None and not (0 <= cls < nc):
erro = f"class fora do range [0,{nc-1}]"
# coords
coords = parts[1:]
if erro is None and len(coords) % 2 != 0:
erro = f"nº de coords ímpar ({len(coords)})"
# checar faixa [0,1]
if erro is None:
try:
vals = list(map(float, coords))
except ValueError:
erro = "coordenadas não numéricas"
vals = []
if not erro:
for v in vals:
if not (0.0 <= v <= 1.0):
erro = f"coord fora de [0,1]: {v}"
break
if erro:
total_objs_invalidos += 1
print(f"[ERRO] {p.name} (linha {idx+1}): {erro}")
print(" ->", line[:120], "...")
else:
total_objs_validos += 1
print(f"\nResumo {subdir}:")
print(f" Linhas totais : {total_linhas}")
print(f" Objetos válidos : {total_objs_validos}")
print(f" Objetos inválidos : {total_objs_invalidos}")
checar_pasta("train")
checar_pasta("val")
from ultralytics.data.utils import visualize_image_annotations
img = "images/train/20260126_121341_900.jpg"
lbl = "labels/train/20260126_121341_900.txt"
label_map = {0: "erva", 1: "cana"}
visualize_image_annotations(img, lbl, label_map)

View File

@ -0,0 +1,78 @@
# train.py
from pathlib import Path
from ultralytics.models.yolo.segment.train import SegmentationTrainer
from custom_channel_dataset import CustomChannelSegDataset
from patch_model_xch import patch_yolov8_first_conv_to_xch
import yaml
with Path("data.yaml").open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
CHANNELS = int(cfg.get("channels", 4))
SCALE = cfg.get("scale", "m")
IMG_SZ = int(cfg.get("size", 800))
class CustomChannelSegTrainer(SegmentationTrainer):
def build_dataset(self, img_path, mode="train", batch=None):
# img_path é train.txt ou val.txt vindo do data.yaml
return CustomChannelSegDataset(
img_path=img_path,
data=self.data,
rect=False,
canais=CHANNELS,
imgsz=self.args.imgsz,
augment=(mode == "train"), # 👈 importante
hyp=self.args, # 👈 importante: passa os hypers
)
def get_model(self, cfg=None, weights=None, verbose=True):
model = super().get_model(cfg=cfg, weights=weights, verbose=verbose)
patch_yolov8_first_conv_to_xch(model, canais=CHANNELS)
# debug pra garantir
seq = model.model if hasattr(model, "model") else model
print("First conv in_channels:", seq[0].conv.in_channels)
return model
def main():
args = {
"project": f"C:/ZendionInc/agrobot_base/Python/yolov8-seg/runs/segment/ch_{CHANNELS}/sc_{SCALE}/sz_{IMG_SZ}",
"name": "weed_detector_segformer",
"model": f"backbones/yolov8-seg-{CHANNELS}ch.yaml",
#"model": f"backbones/yolov8{SCALE}-seg.pt",
"pretrained": False,
"data": "data.yaml",
"epochs": 1000,
"patience": 200,
"imgsz": IMG_SZ,
"device": 0,
"batch": 4,
"workers": 4,
# zerando todas as augmentações “perigosas” pro debug
"mosaic": 0.0,
"copy_paste": 0.0,
"mixup": 0.0,
"erasing": 0.0,
"auto_augment": "none",
"hsv_h": 0.0,
"hsv_s": 0.0,
"hsv_v": 0.0,
# augmentacoes geometricas
"fliplr": 0.5,
"flipud": 0.0,
"translate": 0.05,
"scale": 0.2,
"degrees": 10.0,
"shear": 0.0,
"perspective": 0.0
}
trainer = CustomChannelSegTrainer(overrides=args)
trainer.train()
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

37
Python/yolov8-seg/view.py Normal file
View File

@ -0,0 +1,37 @@
from PIL import Image, ImageDraw
img = "20260126_121759_474"
img_path = f"images/train/{img}.png"
label_path = f"labels/train/{img}.txt"
img = Image.open(img_path)
w, h = img.size
with open(label_path, "r") as f:
lines = [ln.strip() for ln in f.readlines() if ln.strip()]
draw = ImageDraw.Draw(img, "RGBA")
# Cor semântica por classe (ajuste se quiser)
colors = {
0: ( 0, 128, 0, 90), # classe 0 (erva) = verde
1: ( 0, 0, 128, 90), # classe 1 (cana) = azul
}
for line in lines:
parts = line.split()
cls = int(parts[0])
coords = list(map(float, parts[1:]))
# pares (x, y) normalizados [0,1]
pts_px = []
for x, y in zip(coords[0::2], coords[1::2]):
# só pra garantir que não passa um pouquinho de 0..1
x = max(0.0, min(1.0, x))
y = max(0.0, min(1.0, y))
pts_px.append((x * w, y * h))
color = colors.get(cls, (255, 255, 255, 90))
draw.polygon(pts_px, fill=color, outline=color[:3] + (255,))
img.show() # ou img.save("overlay.png")

Binary file not shown.