Implementacao das cameras e segformer
This commit is contained in:
parent
5e9155c3ed
commit
ffeeafcca7
|
|
@ -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/
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 ?? "");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
},
|
||||
]
|
||||
|
|
@ -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, só 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)
|
||||
|
||||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
Lê 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:
|
||||
"""
|
||||
Lê 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 já montado
|
||||
(RGB + segmentação) e só 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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::
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# label:color_rgb:parts:actions
|
||||
naopulverizar:128,0,0::
|
||||
pulverizar:0,128,0::
|
||||
ignore:255,255,255::
|
||||
|
|
@ -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):
|
||||
"""
|
||||
Lê 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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
|
@ -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 # 80–90 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!")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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],
|
||||
já 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):
|
||||
"""
|
||||
Lê 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
|
||||
)
|
||||
])
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
"""
|
||||
Lê RAW (4 canais físicos R,G,IR,B) como CHW no arquivo, normaliza fixo e
|
||||
retorna HWC uint8 com C=4 ou C=5 (com NDVI em 0..255).
|
||||
"""
|
||||
# usa o preview só pra pegar H,W (igual treino)
|
||||
preview = cv2.imread(str(img_path), cv2.IMREAD_UNCHANGED)
|
||||
if preview is None:
|
||||
raise FileNotFoundError(f"Não consegui ler preview: {img_path}")
|
||||
h0, w0 = preview.shape[:2]
|
||||
|
||||
raw_path = img_path.with_suffix(".raw")
|
||||
if not raw_path.exists():
|
||||
raise FileNotFoundError(f"RAW não encontrado: {raw_path}")
|
||||
|
||||
num_raw_channels = 4
|
||||
arr_flat = np.fromfile(str(raw_path), dtype=RAW_DTYPE)
|
||||
expected_size = num_raw_channels * h0 * w0
|
||||
if arr_flat.size != expected_size:
|
||||
raise ValueError(f"RAW size errado {raw_path}: esperado {expected_size}, veio {arr_flat.size}")
|
||||
|
||||
raw_chw = arr_flat.reshape(num_raw_channels, h0, w0) # (4,H,W)
|
||||
arr = np.transpose(raw_chw, (1, 2, 0)).astype(np.float32, copy=False) # (H,W,4)
|
||||
|
||||
# normalização fixa
|
||||
if RAW_ALREADY_01:
|
||||
arr_norm = arr
|
||||
else:
|
||||
arr_norm = arr / float(RAW_MAX_VAL)
|
||||
arr_norm = np.clip(arr_norm, 0.0, 1.0)
|
||||
|
||||
# NDVI opcional (5ch)
|
||||
if canais == 5:
|
||||
R = arr_norm[..., 0]
|
||||
IR = arr_norm[..., 2]
|
||||
eps = 1e-6
|
||||
ndvi = (IR - R) / (IR + R + eps) # [-1,1]
|
||||
ndvi = np.clip(ndvi, -1.0, 1.0)
|
||||
ndvi01 = (ndvi + 1.0) / 2.0 # [0,1]
|
||||
arr_norm = np.concatenate([arr_norm, ndvi01[..., None]], axis=-1) # (H,W,5)
|
||||
elif canais != 4:
|
||||
raise ValueError(f"canais deve ser 4 ou 5, veio {canais}")
|
||||
|
||||
im_uint8 = (arr_norm * 255.0).round().clip(0, 255).astype(np.uint8)
|
||||
return im_uint8 # HWC uint8
|
||||
|
||||
def make_bgr_preview_from_raw_uint8(raw_hwc_uint8: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Preview BGR (OpenCV) a partir de HWC uint8:
|
||||
assume ordem [R,G,IR,B,(NDVI)].
|
||||
Usa B=canal 3, G=canal 1, R=canal 0.
|
||||
"""
|
||||
R = raw_hwc_uint8[..., 0]
|
||||
G = raw_hwc_uint8[..., 1]
|
||||
B = raw_hwc_uint8[..., 3]
|
||||
return np.dstack([B, G, R])
|
||||
|
||||
# ============================
|
||||
# Drawing
|
||||
# ============================
|
||||
def draw_result(vis_bgr: np.ndarray, boxes_xyxy: np.ndarray, cls_ids: np.ndarray, confs: np.ndarray, masks: np.ndarray | None):
|
||||
out = vis_bgr.copy()
|
||||
|
||||
# máscaras (se houver)
|
||||
if masks is not None:
|
||||
for i in range(masks.shape[0]):
|
||||
cls = int(cls_ids[i])
|
||||
color = CLASS_COLORS[cls] if cls < len(CLASS_COLORS) else (0, 255, 0)
|
||||
m = masks[i].astype(bool)
|
||||
overlay = np.array(color, dtype=np.float32)
|
||||
out[m] = (out[m] * 0.4 + overlay * 0.6).astype(np.uint8)
|
||||
|
||||
# boxes + labels
|
||||
for i in range(len(boxes_xyxy)):
|
||||
x1, y1, x2, y2 = boxes_xyxy[i].astype(int)
|
||||
cls = int(cls_ids[i])
|
||||
conf = float(confs[i])
|
||||
color = CLASS_COLORS[cls] if cls < len(CLASS_COLORS) else (0, 255, 0)
|
||||
name = CLASS_NAMES[cls] if cls < len(CLASS_NAMES) else str(cls)
|
||||
|
||||
cv2.rectangle(out, (x1, y1), (x2, y2), color, 2)
|
||||
txt = f"{name} {conf:.2f}"
|
||||
cv2.putText(out, txt, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, cv2.LINE_AA)
|
||||
|
||||
return out
|
||||
|
||||
# ============================
|
||||
# Inference core
|
||||
# ============================
|
||||
@torch.no_grad()
|
||||
def infer_one(model: YOLO, raw_uint8_hwc: np.ndarray, conf_thres=0.25, iou_thres=0.7, max_det=300):
|
||||
"""
|
||||
raw_uint8_hwc: HWC uint8 com C=4 ou 5
|
||||
Retorna boxes/masks já no tamanho original (h0,w0)
|
||||
"""
|
||||
device = next(model.model.parameters()).device
|
||||
|
||||
h0, w0 = raw_uint8_hwc.shape[:2]
|
||||
|
||||
# 1) LetterBox igual treino (mantém proporção)
|
||||
lb_img, ratio, pad, new_unpad_wh, pads_tblr = letterbox_multi(raw_uint8_hwc, new_shape=IMG_SZ, color=114)
|
||||
|
||||
# 2) Tensor (B,C,H,W) float32 0..1
|
||||
x = torch.from_numpy(lb_img).to(device) # (H,W,C) uint8
|
||||
x = x.permute(2, 0, 1).contiguous().float() / 255.0 # (C,H,W) float
|
||||
x = x.unsqueeze(0) # (1,C,H,W)
|
||||
|
||||
# 3) Forward direto
|
||||
out = model.model(x)
|
||||
|
||||
# 4) separa preds/proto
|
||||
if isinstance(out, (list, tuple)):
|
||||
preds_raw = out[0]
|
||||
proto = out[1] if len(out) > 1 else None
|
||||
else:
|
||||
preds_raw = out
|
||||
proto = None
|
||||
|
||||
if preds_raw.ndim == 2:
|
||||
preds_raw = preds_raw.unsqueeze(0)
|
||||
|
||||
nc = len(model.names) if hasattr(model, "names") else len(CLASS_NAMES)
|
||||
det_list = ops.non_max_suppression(
|
||||
preds_raw,
|
||||
conf_thres=conf_thres,
|
||||
iou_thres=iou_thres,
|
||||
classes=None,
|
||||
agnostic=False,
|
||||
max_det=max_det,
|
||||
nc=nc
|
||||
)
|
||||
det = det_list[0]
|
||||
if det is None or len(det) == 0:
|
||||
return None
|
||||
|
||||
# det: [x1,y1,x2,y2,conf,cls, ...mask coeffs]
|
||||
boxes_lb = det[:, :4]
|
||||
confs = det[:, 4]
|
||||
cls_ids = det[:, 5].to(torch.int64)
|
||||
|
||||
# 5) Máscaras no espaço letterboxed
|
||||
masks_orig = None
|
||||
if proto is not None and det.shape[1] > 6:
|
||||
# proto pode vir com batch
|
||||
proto_t = proto
|
||||
while isinstance(proto_t, (list, tuple)):
|
||||
proto_t = proto_t[0] if len(proto_t) else None
|
||||
if proto_t is None:
|
||||
break
|
||||
|
||||
if isinstance(proto_t, torch.Tensor):
|
||||
if proto_t.ndim == 4:
|
||||
proto_t = proto_t[0] # (C,Hm,Wm)
|
||||
proto_t = proto_t.to(det.device)
|
||||
|
||||
mask_coeffs = det[:, 6:].to(det.device)
|
||||
|
||||
# garante C_proto == C_mask
|
||||
c_proto = proto_t.shape[0]
|
||||
c_mask = mask_coeffs.shape[1]
|
||||
if c_proto != c_mask:
|
||||
if c_proto > c_mask:
|
||||
proto_t = proto_t[:c_mask]
|
||||
else:
|
||||
mask_coeffs = mask_coeffs[:, :c_proto]
|
||||
|
||||
# gera máscaras no tamanho do input (letterboxed)
|
||||
masks_lb = ops.process_mask(proto_t, mask_coeffs, boxes_lb, x.shape[2:], upsample=True) # (N,Hlb,Wlb) bool
|
||||
# desfaz letterbox -> original
|
||||
masks_orig_t = unletterbox_mask_to_original(masks_lb, (h0, w0), new_unpad_wh, pads_tblr)
|
||||
masks_orig = masks_orig_t.detach().cpu().numpy().astype(np.uint8) # 0/1
|
||||
|
||||
# 6) Boxes: letterbox -> original (usa ratio_pad)
|
||||
# ratio_pad esperado: (ratio, pad) onde pad é (dw, dh)
|
||||
ratio_pad = (ratio, pad)
|
||||
boxes_scaled = ops.scale_boxes(x.shape[2:], boxes_lb.clone(), (h0, w0), ratio_pad=ratio_pad)
|
||||
boxes_xyxy = boxes_scaled.detach().cpu().numpy()
|
||||
|
||||
return {
|
||||
"boxes": boxes_xyxy,
|
||||
"confs": confs.detach().cpu().numpy(),
|
||||
"cls": cls_ids.detach().cpu().numpy(),
|
||||
"masks": masks_orig
|
||||
}
|
||||
|
||||
# ============================
|
||||
# Main loop viewer
|
||||
# ============================
|
||||
def main():
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
print(f"Usando dispositivo: {device}")
|
||||
|
||||
model = YOLO(WEIGHTS_PATH)
|
||||
model.to(device)
|
||||
model.fuse()
|
||||
|
||||
# sanity check do patch
|
||||
first_conv = model.model.model[0].conv
|
||||
print("First conv weight shape:", tuple(first_conv.weight.shape)) # deve ser [48, C, 3, 3]
|
||||
|
||||
img_paths = sorted([p for p in TEST_DIR.iterdir() if p.suffix.lower() in [".jpg", ".jpeg", ".png"]])
|
||||
if not img_paths:
|
||||
print(f"Nenhuma imagem encontrada em {TEST_DIR}")
|
||||
return
|
||||
|
||||
idx = 0
|
||||
n = len(img_paths)
|
||||
|
||||
CONF = 0.45
|
||||
IOU = 0.7
|
||||
|
||||
cv2.namedWindow(f"YOLO {CHANNELS}ch Viewer", cv2.WINDOW_NORMAL)
|
||||
|
||||
while True:
|
||||
img_path = img_paths[idx]
|
||||
print("\n======================================")
|
||||
print(f"Processando: {img_path}")
|
||||
|
||||
try:
|
||||
raw_uint8 = load_raw_as_uint8_hwc(img_path, canais=CHANNELS) # HWC uint8
|
||||
preview_bgr = make_bgr_preview_from_raw_uint8(raw_uint8)
|
||||
|
||||
res = infer_one(model, raw_uint8, conf_thres=CONF, iou_thres=IOU)
|
||||
if res is None:
|
||||
vis = preview_bgr.copy()
|
||||
cv2.putText(vis, "SEM DETECCOES", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,0,255), 2, cv2.LINE_AA)
|
||||
else:
|
||||
vis = draw_result(
|
||||
preview_bgr,
|
||||
boxes_xyxy=res["boxes"],
|
||||
cls_ids=res["cls"],
|
||||
confs=res["confs"],
|
||||
masks=res["masks"]
|
||||
)
|
||||
|
||||
txt = f"[{idx+1}/{n}] {img_path.name} | A/D navega | +/- conf {CONF:.2f} | Q/ESC sai"
|
||||
cv2.putText(vis, txt, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 2, cv2.LINE_AA)
|
||||
cv2.imshow(f"YOLO {CHANNELS}ch Viewer", vis)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao processar {img_path}: {e}")
|
||||
blank = np.zeros((480, 900, 3), dtype=np.uint8)
|
||||
cv2.putText(blank, f"Erro em {img_path.name}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2)
|
||||
cv2.putText(blank, str(e)[:120], (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
|
||||
cv2.imshow(f"YOLO {CHANNELS}ch Viewer", blank)
|
||||
|
||||
k = cv2.waitKey(0) & 0xFF
|
||||
if k in (ord("q"), 27): # q ou ESC
|
||||
break
|
||||
elif k == ord("a"):
|
||||
idx = (idx - 1) % n
|
||||
elif k == ord("d"):
|
||||
idx = (idx + 1) % n
|
||||
elif k in (ord("+"), ord("=")):
|
||||
CONF = min(0.99, CONF + 0.05)
|
||||
elif k in (ord("-"), ord("_")):
|
||||
CONF = max(0.01, CONF - 0.05)
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 (já é 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.
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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.
Loading…
Reference in New Issue