ajustes gerais pre teste

This commit is contained in:
Diego Freitas 2025-09-15 07:23:07 -03:00
parent 35202470c3
commit 9611f564b5
104 changed files with 6246 additions and 906 deletions

Binary file not shown.

View File

@ -79,7 +79,7 @@
this.lblD_Sen_sCOR_24V = new System.Windows.Forms.Label();
this.lblD_Atu_sPRS = new System.Windows.Forms.Label();
this.lblD_Atu_sBMB = new System.Windows.Forms.Label();
this.lblD_Sen_sLRA = new System.Windows.Forms.Label();
this.lblD_Lra = new System.Windows.Forms.Label();
this.lblD_Atu_sMAS = new System.Windows.Forms.Label();
this.lblD_Atu_sFLX = new System.Windows.Forms.Label();
this.lblD_Atu_sBIC_B01 = new System.Windows.Forms.Label();
@ -669,16 +669,16 @@
this.lblD_Atu_sBMB.Text = "BOMBA";
this.lblD_Atu_sBMB.Click += new System.EventHandler(this.lblModulo_Click);
//
// lblD_Sen_sLRA
// lblD_Lra
//
this.lblD_Sen_sLRA.AutoSize = true;
this.lblD_Sen_sLRA.Cursor = System.Windows.Forms.Cursors.Hand;
this.lblD_Sen_sLRA.Location = new System.Drawing.Point(213, 57);
this.lblD_Sen_sLRA.Name = "lblD_Sen_sLRA";
this.lblD_Sen_sLRA.Size = new System.Drawing.Size(46, 24);
this.lblD_Sen_sLRA.TabIndex = 63;
this.lblD_Sen_sLRA.Text = "LRA";
this.lblD_Sen_sLRA.Click += new System.EventHandler(this.lblModulo_Click);
this.lblD_Lra.AutoSize = true;
this.lblD_Lra.Cursor = System.Windows.Forms.Cursors.Hand;
this.lblD_Lra.Location = new System.Drawing.Point(213, 57);
this.lblD_Lra.Name = "lblD_Lra";
this.lblD_Lra.Size = new System.Drawing.Size(46, 24);
this.lblD_Lra.TabIndex = 63;
this.lblD_Lra.Text = "LRA";
this.lblD_Lra.Click += new System.EventHandler(this.lblModulo_Click);
//
// lblD_Atu_sMAS
//
@ -764,7 +764,7 @@
this.Controls.Add(this.lblD_Atu_sBIC_B01);
this.Controls.Add(this.lblD_Atu_sFLX);
this.Controls.Add(this.lblD_Atu_sMAS);
this.Controls.Add(this.lblD_Sen_sLRA);
this.Controls.Add(this.lblD_Lra);
this.Controls.Add(this.lblD_Atu_sBMB);
this.Controls.Add(this.lblD_Atu_sPRS);
this.Controls.Add(this.lblD_Sen_sCOR_24V);
@ -887,7 +887,7 @@
private System.Windows.Forms.Label lblD_Sen_sCOR_24V;
private System.Windows.Forms.Label lblD_Atu_sPRS;
private System.Windows.Forms.Label lblD_Atu_sBMB;
private System.Windows.Forms.Label lblD_Sen_sLRA;
private System.Windows.Forms.Label lblD_Lra;
private System.Windows.Forms.Label lblD_Atu_sMAS;
private System.Windows.Forms.Label lblD_Atu_sFLX;
private System.Windows.Forms.Label lblD_Atu_sBIC_B01;

View File

@ -6,6 +6,7 @@ using AgroBase.Models.Operadores;
using AgroBase.Properties;
using AgroBase.Services;
using AgroBase.Services.Operadores;
using OpenHardwareMonitor.Hardware;
using System;
using System.Collections.Generic;
using System.Data;
@ -203,6 +204,19 @@ namespace AgroBase.Forms.IHM
: saudeImu.status == StatusModulo.Operante ? Color.DarkGreen
: Color.Black;
}
else if (Dispositivo == T_Code.Lra)
{
var Modulo = SerialService.DispositivosMapeados.FirstOrDefault(x => x.Dispositivo == T_Code.Snr);
bool modDesconectado = Modulo == null || Modulo.Status == StatusModulo.Desconectado;
var saudeLra = HealthWorkerService.ModulosSaude.FirstOrDefault(x => x.modulo == Dispositivo);
var lora = Variaveis.LoraService;
lbl.ForeColor =
lora == null || !(Variaveis.OperacaoEmAndamento.DispSen?.Dados?.Conectado ?? false)
? Color.Black
: saudeLra.status == StatusModulo.Alerta ? Color.Gold
: saudeLra.status == StatusModulo.Operante ? Color.DarkGreen
: Color.DarkRed;
}
else if (Dispositivo == T_Code.Sen || Dispositivo == T_Code.Atu)
{
var Modulo = SerialService.DispositivosMapeados.FirstOrDefault(x => x.Dispositivo == Dispositivo);
@ -221,18 +235,7 @@ namespace AgroBase.Forms.IHM
{
S_Code Sensor = string.IsNullOrEmpty(Mod_ID) ? S_Code.sVZO : (S_Code)Enum.Parse(typeof(S_Code), Mod_ID);
string modId = lbl.Name.Split('_').Length == 4 ? lbl.Name.Split('_')[3] : "";
if (Sensor == S_Code.sLRA)
{
var lora = Variaveis.LoraService;
lbl.ForeColor = modDesconectado || lora == null
? Color.Black
: lora.Conectado && !lora.Configurado
? Color.Gold
: lora.Configurado
? Color.DarkGreen
: Color.DarkRed;
}
else if (Sensor == S_Code.sFRO)
if (Sensor == S_Code.sFRO)
{
var freio = Variaveis.OperacaoEmAndamento.DispSen.Dados.Servos.FirstOrDefault(x => x.Componente == Sensor && x.ID.Contains(modId));
lbl.ForeColor = modDesconectado || freio == null
@ -365,14 +368,7 @@ namespace AgroBase.Forms.IHM
{
S_Code Sensor = string.IsNullOrEmpty(Mod_ID) ? S_Code.sVZO : (S_Code)Enum.Parse(typeof(S_Code), Mod_ID);
string modId = lbl.Name.Split('_').Length == 4 ? lbl.Name.Split('_')[3] : "";
if (Sensor == S_Code.sLRA)
{
saudeIndividual = new ManagerWorkerMessageResponseModulosPendentesSaudeModel()
{
status = Variaveis.LoraService.Configurado ? StatusModulo.Operante : Variaveis.LoraService.Conectado ? StatusModulo.Alerta : StatusModulo.Falha,
};
}
else if (Sensor == S_Code.sFRO)
if (Sensor == S_Code.sFRO)
{
var freio = Variaveis.OperacaoEmAndamento.DispSen.Dados.Servos.FirstOrDefault(x => x.Componente == Sensor && x.ID.Contains(modId));
saudeIndividual = saudeGeral.saude_individual.FirstOrDefault(x => x.id == freio.ID_Num.ToString());
@ -408,9 +404,9 @@ namespace AgroBase.Forms.IHM
}
}
StatusModulo status = Modulo?.Status ?? StatusModulo.Desconectado;
double saude = Modulo?.Saude ?? 0;
string erros = Modulo?.Erros ?? "desconectado";
StatusModulo status = Modulo?.Status ?? saudeGeral?.status ?? StatusModulo.Desconectado;
double saude = Modulo?.Saude ?? saudeGeral?.saude ?? 0;
string erros = Modulo?.Erros ?? string.Join(", ", (saudeGeral?.motivos ?? new List<string>() { "desconectado" })) ?? "desconectado";
if (saudeIndividual != null)
{

View File

@ -18,6 +18,7 @@ namespace AgroBase.Models
public double Longitude { get; set; }
public DateTime DataHora { get; set; }
public double Altitude { get; set; }
public double AltitudeElipsoidal { get; set; }
public double Velocidade { get; set; }
public double Distancia { get; set; }
public double OrientacaoMovimento { get; set; }
@ -33,6 +34,7 @@ namespace AgroBase.Models
public double PDOP { get; set; }
public double VDOP { get; set; }
public TiposDimensaoCorrecaoGPS FixDimensao { get; set; }
public string BaseID { get; set; }
public double PrecisaoCm
{
get
@ -67,6 +69,7 @@ namespace AgroBase.Models
Ntrip_ativado = Ntrip_ativado,
NumeroSatelites = NumeroSatelites,
Altitude = Altitude,
AltitudeElipsoidal = AltitudeElipsoidal,
DataHora = DataHora,
Latitude = Latitude,
Longitude = Longitude,
@ -88,7 +91,8 @@ namespace AgroBase.Models
IdadeCorrecao = IdadeCorrecao,
FixDimensao = FixDimensao,
PDOP = PDOP,
VDOP = VDOP
VDOP = VDOP,
BaseID = BaseID,
};
}
}

View File

@ -10,6 +10,7 @@ using static AgroBase.Services.LoRaEspService;
using System.Windows.Forms;
using AgroBase.Services.Operadores;
using AgroBase.Models.Operadores;
using System.Diagnostics;
namespace AgroBase.Models.Modules
{
@ -2915,6 +2916,7 @@ namespace AgroBase.Models.Modules
loraService.ParametrosGet.channel = channel;
loraService.ParametrosGet.worCycle = worCycle;
loraService.ParametrosGet.tranMode = tranMode;
loraService.ParametrosGet.UltimaLeitura = DateTime.Now;
break;
}
default:
@ -2992,10 +2994,10 @@ namespace AgroBase.Models.Modules
break;
}
}
Variaveis.LoraService.UltimoRxDados = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
break;
}
}
loraService.ParametrosGet.UltimaLeitura = DateTime.Now;
break;
}
case S_Code.sTOD:

View File

@ -1273,7 +1273,7 @@ namespace AgroBase.Models
Variaveis.OperacaoEmAndamento.idxLog++;
if (Variaveis.OperacaoEmAndamento.idxLog % 10 == 0)
if (Variaveis.OperacaoEmAndamento.idxLog % (Variaveis.LoraService.TempoEnvioDadosBase / 1000.0) == 0)
{
Variaveis.LoraService.EnviarDadosParaBase();
}

View File

@ -50,6 +50,7 @@ namespace AgroBase.Services
public static void IniciarRotinas()
{
Task.Run(async () => await tmrMonitoramento_Tick());
tmrMonitoramento?.Dispose();
tmrMonitoramento = new AsyncTaskTimerModel("tmrMonitoramento", tmrMonitoramento_Tick, 10000);
tmrMonitoramento.Start();

View File

@ -9,8 +9,10 @@ using System.IO.Ports;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static AgroBase.Models.Enums;
using static AgroBase.Services.GPSService;
namespace AgroBase.Services
{
@ -94,9 +96,26 @@ namespace AgroBase.Services
private static async Task ConfigurarModulo()
{
Console.WriteLine("Iniciando configuração do módulo GPS...");
if (Variaveis.IsAgroMonitor)
{
await ConfigurarModuloBase(tempo_fixacao: 600);
bool sucesso = await BaseFixService.FixarBaseViaNtripAsync(
startNtrip: async () =>
{
Console.WriteLine("Iniciando correção NTRIP...");
CorrecaoRTK_Ntrip = true;
Task.Run(async () => await AplicarCorrecaoRTK_Ntrip());
},
stopNtrip: async () =>
{
Console.WriteLine("Parando correção NTRIP...");
CorrecaoRTK_Ntrip = false;
}
);
if (!sucesso)
{
await ConfigurarModuloBase(tempo_fixacao: 600);
}
}
else
{
@ -174,7 +193,7 @@ namespace AgroBase.Services
// (Opcional) ativar mais constelações:
$"RTCM1094 {porta_saida} 1\r\n", // Galileo MSM4
$"RTCM1084 {porta_saida} 1\r\n", // GLONASS MSM4
$"RTCM1230 {porta_saida} 10\r\n",// Bias GLONASS (OBRIGATÓRIO se 1084 estiver ativo)
//$"RTCM1230 {porta_saida} 10\r\n",// Bias GLONASS (OBRIGATÓRIO se 1084 estiver ativo)
// NMEA mínimo para debug na USB
$"gngga {porta_usb} 1\r\n",
@ -193,6 +212,7 @@ namespace AgroBase.Services
}
private static StringBuilder _buffer = new StringBuilder();
private static void PortaGPS_DataReceived(object sender, SerialDataReceivedEventArgs e)
@ -346,50 +366,64 @@ namespace AgroBase.Services
private static void ProcessarGNGGA(string sentenca)
{
var ci = CultureInfo.InvariantCulture;
var campos = sentenca.Split(',');
string horaUTC = campos[1].Replace(".", ",");
string latitudeRaw = campos[2].Replace(".", ",");
string hemisferioLat = campos[3].Replace(".", ",");
string longitudeRaw = campos[4].Replace(".", ",");
string hemisferioLon = campos[5].Replace(".", ",");
string qualidade = campos[6].Replace(".", ",");
string satelitesUsados = campos[7].Replace(".", ",");
string hdop = campos[8].Replace(".", ",");
string altitudeRaw = campos[9].Replace(".", ",");
string idadeCorrecaoRaw = campos[13];
string base_id = campos[14];
string horaUTC = campos.Length > 1 ? campos[1] : "";
string latitudeRaw = campos.Length > 2 ? campos[2] : "";
string hemisferioLat = campos.Length > 3 ? campos[3] : "";
string longitudeRaw = campos.Length > 4 ? campos[4] : "";
string hemisferioLon = campos.Length > 5 ? campos[5] : "";
string qualidade = campos.Length > 6 ? campos[6] : "0";
string satelitesUsados = campos.Length > 7 ? campos[7] : "0";
string hdop = campos.Length > 8 ? campos[8] : "99.9";
string altitudeRaw = campos.Length > 9 ? campos[9] : "0";
string geoidSepRaw = campos.Length > 11 ? campos[11] : "0";
string idadeCorrecaoRaw = campos.Length > 13 ? campos[13] : "";
string base_id = campos.Length > 14 ? campos[14] : "";
// Conversão de Latitude
// Conversão de Latitude (ddmm.mmmm)
double latitude = 0;
if (!string.IsNullOrEmpty(latitudeRaw))
{
double latitudeGraus = double.Parse(latitudeRaw.Substring(0, 2));
double latitudeMinutos = double.Parse(latitudeRaw.Substring(2)) / 60.0;
latitude = latitudeGraus + latitudeMinutos;
if (hemisferioLat == "S") latitude *= -1;
// lat tem 2 dígitos de graus
var deg = double.Parse(latitudeRaw.Substring(0, 2), ci);
var min = double.Parse(latitudeRaw.Substring(2), ci);
latitude = deg + (min / 60.0);
if (hemisferioLat.Equals("S", StringComparison.OrdinalIgnoreCase)) latitude *= -1;
}
// Conversão de Longitude
// Conversão de Longitude (dddmm.mmmm)
double longitude = 0;
if (!string.IsNullOrEmpty(longitudeRaw))
{
double longitudeGraus = double.Parse(longitudeRaw.Substring(0, 3));
double longitudeMinutos = double.Parse(longitudeRaw.Substring(3)) / 60.0;
longitude = longitudeGraus + longitudeMinutos;
if (hemisferioLon == "W") longitude *= -1;
// lon tem 3 dígitos de graus
var deg = double.Parse(longitudeRaw.Substring(0, 3), ci);
var min = double.Parse(longitudeRaw.Substring(3), ci);
longitude = deg + (min / 60.0);
if (hemisferioLon.Equals("W", StringComparison.OrdinalIgnoreCase)) longitude *= -1;
}
double.TryParse(altitudeRaw, out double altitude);
// Altitude MSL (campo 9)
double altMSL = 0;
double.TryParse(altitudeRaw, NumberStyles.Float, ci, out altMSL);
double.TryParse(hdop, out double precisao);
// Geoid separation (campo 11)
double geoidSep = 0;
double.TryParse(geoidSepRaw, NumberStyles.Float, ci, out geoidSep);
// Altura elipsoidal = MSL + geoid separation
double altElipsoidal = altMSL + geoidSep;
// HDOP (adimensional)
double.TryParse(hdop, NumberStyles.Float, ci, out double hdopVal);
int.TryParse(satelitesUsados, out int nsatelites);
int.TryParse(qualidade, out int fix);
double.TryParse(idadeCorrecaoRaw.Replace(".", ","), out double idadeCorrecao);
int.TryParse(qualidade, out int fixCode);
double idadeCorrecao = -1;
if (!string.IsNullOrWhiteSpace(idadeCorrecaoRaw))
double.TryParse(idadeCorrecaoRaw, NumberStyles.Float, ci, out idadeCorrecao);
//Console.WriteLine($"GNGGA: Hora={horaUTC}, Latitude={latitude}, Longitude={longitude}, Qualidade={qualidade}, Satélites={satelitesUsados}, HDOP={hdop}, Altitude={altitude}");
@ -399,28 +433,38 @@ namespace AgroBase.Services
PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
PenultimaLeitura.Altitude = UltimaLeitura.Altitude;
PenultimaLeitura.AltitudeElipsoidal = UltimaLeitura.AltitudeElipsoidal;
PenultimaLeitura.PrecisaoHorizontal = UltimaLeitura.PrecisaoHorizontal;
PenultimaLeitura.NumeroSatelites = UltimaLeitura.NumeroSatelites;
PenultimaLeitura.QualidadeFix = UltimaLeitura.QualidadeFix;
PenultimaLeitura.DataHora = UltimaLeitura.DataHora;
PenultimaLeitura.IdadeCorrecao = UltimaLeitura.IdadeCorrecao;
PenultimaLeitura.BaseID = UltimaLeitura.BaseID;
// Armazenar os valores na última leitura
UltimaLeitura.TimestampPos.valor = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
UltimaLeitura.Momento = DateTime.Now;
UltimaLeitura.Latitude = latitude;
UltimaLeitura.Longitude = longitude;
UltimaLeitura.Altitude = altitude;
UltimaLeitura.PrecisaoHorizontal = precisao;
UltimaLeitura.Altitude = altMSL;
UltimaLeitura.AltitudeElipsoidal = altElipsoidal;
UltimaLeitura.PrecisaoHorizontal = hdopVal;
UltimaLeitura.NumeroSatelites = nsatelites;
UltimaLeitura.QualidadeFix = (TiposCorrecaoGPS)fix;
UltimaLeitura.IdadeCorrecao = string.IsNullOrEmpty(idadeCorrecaoRaw) ? -1 : idadeCorrecao;
UltimaLeitura.QualidadeFix = (TiposCorrecaoGPS)fixCode;
UltimaLeitura.IdadeCorrecao = idadeCorrecao;
UltimaLeitura.BaseID = base_id;
// Parse a hora do formato HHmmss.ss
if (!string.IsNullOrEmpty(horaUTC) && TimeSpan.TryParseExact(horaUTC.Substring(0, 6), "hhmmss", CultureInfo.InvariantCulture, out TimeSpan timeOfDay))
// Hora UTC no formato HHmmss.ss (fração opcional)
if (!string.IsNullOrEmpty(horaUTC) && horaUTC.Length >= 6)
{
DateTime currentDate = DateTime.UtcNow.Date;
UltimaLeitura.DataHora = currentDate.Add(timeOfDay).ToLocalTime();
// Pega HHmmss e, se houver, fração:
var hh = int.Parse(horaUTC.Substring(0, 2), ci);
var mm = int.Parse(horaUTC.Substring(2, 2), ci);
var ssStr = horaUTC.Substring(4); // "ss" ou "ss.ss"
double ss = double.Parse(ssStr, ci);
var ts = new TimeSpan(0, hh, mm, (int)Math.Floor(ss), (int)Math.Round((ss - Math.Floor(ss)) * 1000.0));
var currentDateUtc = DateTime.UtcNow.Date;
UltimaLeitura.DataHora = currentDateUtc.Add(ts).ToLocalTime();
}
}
@ -891,7 +935,10 @@ namespace AgroBase.Services
public static void AtualizarCoordenadasGPS()
{
PenultimaLeitura.Ntrip_ativado = UltimaLeitura.Ntrip_ativado;
PenultimaLeitura.Heartbeat = UltimaLeitura.Heartbeat;
UltimaLeitura.Ntrip_ativado = CorrecaoRTK_Ntrip;
UltimaLeitura.Heartbeat = (UltimaLeitura.Heartbeat + 1) % 10;
if (Variaveis.IsAgroMonitor)
{
@ -899,9 +946,6 @@ namespace AgroBase.Services
return;
}
PenultimaLeitura.Heartbeat = UltimaLeitura.Heartbeat;
UltimaLeitura.Heartbeat = (UltimaLeitura.Heartbeat + 1) % 10;
DefinirAnguloCarroGPS();
AtualizaDadosRedis();
@ -1247,4 +1291,350 @@ namespace AgroBase.Services
}
}
public class GgaFix
{
public DateTime TsUtc { get; }
public double LatDeg { get; }
public double LonDeg { get; }
public double AltElipsoidalM { get; }
public TiposCorrecaoGPS FixQuality { get; }
public GgaFix(DateTime tsUtc, double latDeg, double lonDeg, double altElipsoidalM, TiposCorrecaoGPS fixQuality)
{
TsUtc = tsUtc;
LatDeg = latDeg;
LonDeg = lonDeg;
AltElipsoidalM = altElipsoidalM;
FixQuality = fixQuality;
}
}
public static class BaseFixService
{
private static int _lastReadHeartbeat = -1;
public static List<GgaFix> amostras_pos = new List<GgaFix>(1000);
private static DateTime? inicioProcesso = null;
private static DateTime? inicioFix = null;
private static DateTime? fimProcesso = null;
private static int segundosFixEstavel = 120;
private static int maxJanelaSegundos = 120;
public static double Progresso
{
get
{
double progresso = inicioFix is null ? 0 : (DateTime.UtcNow - inicioFix.Value).TotalSeconds / segundosFixEstavel * 100.0;
return progresso;
}
}
public static double ProgressoGeral
{
get
{
double progresso = inicioProcesso is null ? 0 : (DateTime.UtcNow - inicioProcesso.Value).TotalSeconds / maxJanelaSegundos * 100.0;
return progresso;
}
}
public static string ProgressoStr
{
get
{
string progresso = "";
if (CorrecaoEmAndamento)
{
progresso = $"Recebendo correção RTK via Ntrip. Progresso geral: {ProgressoGeral.ToString("0.00")}%, Progresso correção: {Progresso.ToString("0.00")}%";
}
else if (CorrecaoAbsoluta && inicioProcesso.HasValue && fimProcesso.HasValue)
{
progresso = $"Correção absoluta concluída com {amostras_pos.Count} amostras em {(fimProcesso.Value - inicioProcesso.Value).TotalSeconds.ToString("0.00")} segundos";
}
else if (inicioProcesso.HasValue && fimProcesso.HasValue)
{
progresso = $"Correção absoluta falhou com {amostras_pos.Count} amostras em {(fimProcesso.Value - inicioProcesso.Value).TotalSeconds.ToString("0.00")} segundos";
}
else
{
progresso = $"Correção absoluta não realizada";
}
return progresso;
}
}
public static bool CorrecaoAbsoluta = false;
public static bool CorrecaoEmAndamento = false;
// ===== 1) Função principal =====
public static async Task<bool> FixarBaseViaNtripAsync(string portaUsb = "com3", string portaEntrada = "com2", string portaSaida = "com2", string baseId = "957", int segsFixEstavel = 120, int maxJanelaSegs = 600, double madK = 3.5, Func<Task> startNtrip = null, Func<Task> stopNtrip = null)
{
if (CorrecaoEmAndamento)
return false;
CorrecaoEmAndamento = true;
fimProcesso = null;
segundosFixEstavel = segsFixEstavel;
maxJanelaSegundos = maxJanelaSegs;
// 1.1 Config temporária como rover parado + NMEA
await ConfigurarComoRoverParadoAsync(portaUsb, portaEntrada);
// 1.2 Ligar NTRIP (injeta RTCM na portaEntrada)
if (startNtrip != null) await startNtrip();
try
{
// 2) Esperar FIX sustentado e coletar GNGGA
var amostras = await EsperarFixEAmostrarAsync();
if (amostras.Count < 10)
{
Console.WriteLine("Poucas amostras de RTK FIX coletadas. Tente aumentar o tempo ou verificar sinais.");
CorrecaoAbsoluta = false;
return CorrecaoAbsoluta;
}
// 3) Filtro robusto (MAD) + média final
var (lat, lon, h, nAmostras) = FiltrarEAgrupar(amostras, madK);
// 4) Alternar para base FIX + perfil RTCM
await AplicarBaseFixAsync(portaUsb, portaSaida, baseId, lat, lon, h);
Console.WriteLine($"[BASE/FIX] Coordenadas aplicadas (n={nAmostras}):");
Console.WriteLine($" lat = {lat:0.000000000}, lon = {lon:0.000000000}, h = {h:0.000}");
CorrecaoAbsoluta = true;
return CorrecaoAbsoluta;
}
finally
{
if (stopNtrip != null) await stopNtrip();
fimProcesso = DateTime.UtcNow;
CorrecaoEmAndamento = false;
}
}
// ===== 1.1 Rover parado + NMEA + limpar logs =====
private static async Task ConfigurarComoRoverParadoAsync(string portaUsb, string portaEntrada)
{
Console.WriteLine("Configurando base como modo rover parado...");
string freq = "1.0";
string[] cmds = {
// Ajuste de bauds
$"config {portaUsb} 115200\r\n",
$"config {portaEntrada} 115200\r\n",
// Limpa logs
$"unlog com1\r\n",
$"unlog com2\r\n",
$"unlog com3\r\n",
// Rover parado (vamos usar NTRIP p/ obter FIX)
$"mode rover uav\r\n",
// NMEA na USB
$"gngga {portaUsb} {freq}\r\n",
$"gpths {portaUsb} {freq}\r\n",
$"saveconfig\r\n"
};
await Task.Delay(1000);
foreach (var c in cmds)
{
var b = Encoding.ASCII.GetBytes(c);
PortaGPS.Write(b, 0, b.Length);
await Task.Delay(250);
}
}
// ===== 2) Coleta GNGGA com FIX sustentado =====
private static async Task<List<GgaFix>> EsperarFixEAmostrarAsync()
{
Console.WriteLine("Inciando coleta de dados...");
amostras_pos = new List<GgaFix>(1000);
inicioProcesso = DateTime.UtcNow;
inicioFix = null;
// Você já deve ter um leitor da COM que devolve linhas NMEA.
// Abaixo, vamos supor um método async que lê GGA parseado.
while (ProgressoGeral < 100)
{
// Lê próxima sentença (bloqueante/assíncrono)
var gga = await LerProximoGgaAsync(); // implemente no seu stack
if (gga is null) continue;
// Considera "RTK FIX" como qualidade válida
if (!new List<TiposCorrecaoGPS>() { TiposCorrecaoGPS.RTKFixo }.Contains(gga.FixQuality))
{
inicioFix = null; // reset
continue;
}
// Marca início da janela de FIX estável
if (inicioFix is null)
{
Console.WriteLine("RTK Fixo definido! Iniciando coleta de dados com precisão...");
inicioFix = DateTime.UtcNow;
}
amostras_pos.Add(gga);
Console.WriteLine("Nova coordenada registrada!");
// Verifica se já temos FIX estável pelo período necessário
if (Progresso >= 100)
break;
}
return amostras_pos;
}
// ===== 2.1) Ajuste a assinatura se quiser passar timeout e CT de fora
private static async Task<GgaFix> LerProximoGgaAsync(int timeoutMs = 5000, CancellationToken ct = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
int startHb = System.Threading.Volatile.Read(ref _lastReadHeartbeat);
// 1) Espera um novo heartbeat
while (!ct.IsCancellationRequested)
{
int currentHb = UltimaLeitura.Heartbeat; // <- leitura normal da propriedade
if (currentHb != startHb) break;
if (sw.ElapsedMilliseconds >= timeoutMs)
//throw new TimeoutException("Timeout aguardando nova leitura GGA.");
return new GgaFix(DateTime.UtcNow, 0, 0, 0, TiposCorrecaoGPS.SemCorrecao);
await Task.Delay(75, ct).ConfigureAwait(false);
}
ct.ThrowIfCancellationRequested();
// 2) Snapshot consistente
while (true)
{
int hbBefore = UltimaLeitura.Heartbeat;
// Captura TODOS os campos que você precisa em variáveis locais
DateTime tsUtc = UltimaLeitura.DataHora.ToUniversalTime();
double lat = UltimaLeitura.Latitude;
double lon = UltimaLeitura.Longitude;
double altElips = UltimaLeitura.AltitudeElipsoidal; // garanta que já é elipsoidal no parser
var fixQual = UltimaLeitura.QualidadeFix; // enum? ok.
int hbAfter = UltimaLeitura.Heartbeat;
// Se o heartbeat não mudou durante o snapshot, temos dados coerentes
if (hbBefore == hbAfter)
{
// marca como lido
System.Threading.Volatile.Write(ref _lastReadHeartbeat, hbAfter);
// monta o DTO
return new GgaFix(
tsUtc: tsUtc,
latDeg: lat,
lonDeg: lon,
altElipsoidalM: altElips,
fixQuality: fixQual
);
}
// caso contrário, alguém atualizou no meio — tenta de novo rápido
await Task.Yield();
}
}
// ===== 3) Filtro robusto (MAD) + média =====
private static (double lat, double lon, double h, int n) FiltrarEAgrupar(List<GgaFix> amostras, double madK = 3.5)
{
Console.WriteLine("Filtrando dados aferidos...");
// Medianas
var lats = amostras.Select(a => a.LatDeg).OrderBy(x => x).ToArray();
var lons = amostras.Select(a => a.LonDeg).OrderBy(x => x).ToArray();
var hs = amostras.Select(a => a.AltElipsoidalM).OrderBy(x => x).ToArray();
double medLat = Mediana(lats);
double medLon = Mediana(lons);
double medH = Mediana(hs);
// Desvios absolutos da mediana (MAD)
var dLat = amostras.Select(a => Math.Abs(a.LatDeg - medLat)).OrderBy(x => x).ToArray();
var dLon = amostras.Select(a => Math.Abs(a.LonDeg - medLon)).OrderBy(x => x).ToArray();
var dH = amostras.Select(a => Math.Abs(a.AltElipsoidalM - medH)).OrderBy(x => x).ToArray();
double madLat = Mediana(dLat) + 1e-12;
double madLon = Mediana(dLon) + 1e-12;
double madHgt = Mediana(dH) + 1e-12;
// Filtra outliers (|x - med| / MAD <= madK)
var filtradas = amostras.Where(a =>
(Math.Abs(a.LatDeg - medLat) / madLat) <= madK &&
(Math.Abs(a.LonDeg - medLon) / madLon) <= madK &&
(Math.Abs(a.AltElipsoidalM - medH) / madHgt) <= madK
).ToList();
// Média final
double lat = filtradas.Average(a => a.LatDeg);
double lon = filtradas.Average(a => a.LonDeg);
double h = filtradas.Average(a => a.AltElipsoidalM);
return (lat, lon, h, filtradas.Count);
double Mediana(double[] arr)
{
int n = arr.Length;
if (n == 0) return double.NaN;
return (n % 2 == 1) ? arr[n / 2] : 0.5 * (arr[n / 2 - 1] + arr[n / 2]);
}
}
// ===== 4) Aplicar base FIX + RTCM + save =====
private static async Task AplicarBaseFixAsync(string portaUsb, string portaSaida, string baseId, double latDeg, double lonDeg, double hEllipsM)
{
Console.WriteLine("Aplicando dados de correção...");
var ci = System.Globalization.CultureInfo.InvariantCulture;
// Desliga logs antes de trocar modo
string[] pre = {
$"unlog com1\r\n",
$"unlog com2\r\n",
$"unlog com3\r\n"
};
foreach (var c in pre) { PortaGPS.Write(Encoding.ASCII.GetBytes(c), 0, c.Length); await Task.Delay(150); }
var latStr = latDeg.ToString("0.000000000", ci);
var lonStr = lonDeg.ToString("0.000000000", ci);
var hStr = hEllipsM.ToString("0.000", ci);
var fix = Encoding.ASCII.GetBytes($"mode base {baseId} {latStr} {lonStr} {hStr}\r\n");
PortaGPS.Write(fix, 0, fix.Length);
await Task.Delay(250);
// Reativar RTCM no canal de saída para o LoRa
string[] rtcmCmds = {
// RTCM perfil (comece leve; ative mais constelações se o LoRa aguentar)
$"RTCM1006 {portaSaida} 10\r\n",
$"RTCM1033 {portaSaida} 30\r\n",
$"RTCM1074 {portaSaida} 1\r\n", // GPS MSM4
$"RTCM1124 {portaSaida} 1\r\n", // BeiDou MSM4
// (Opcional) ativar mais constelações:
$"RTCM1094 {portaSaida} 1\r\n", // Galileo MSM4
$"RTCM1084 {portaSaida} 1\r\n", // GLONASS MSM4
//$"RTCM1230 {portaSaida} 10\r\n",// Bias GLONASS (OBRIGATÓRIO se 1084 estiver ativo)
};
foreach (var c in rtcmCmds) { var b = Encoding.ASCII.GetBytes(c); PortaGPS.Write(b, 0, b.Length); await Task.Delay(200); }
// NMEA mínimo na USB p/ debug
var nmea = $"gngga {portaUsb} 1\r\n";
PortaGPS.Write(Encoding.ASCII.GetBytes(nmea), 0, nmea.Length);
await Task.Delay(150);
// Persistir
var save = "saveconfig\r\n";
PortaGPS.Write(Encoding.ASCII.GetBytes(save), 0, save.Length);
}
}
}

View File

@ -303,6 +303,7 @@ namespace AgroBase.Services
if (_filaTx.TryDequeue(out var buffer))
{
EnviarComando(buffer);
await Task.Delay(100);
}
}
@ -373,6 +374,8 @@ namespace AgroBase.Services
CanMessagePosicaoDados posicao = (CanMessagePosicaoDados)dados[0];
byte idNum = dados[1];
Console.WriteLine($"Dados LoRa recebidos do rover {posicao.ToString()} ({((DadosLoRaParse)posicao).ToString()}) " + string.Join(" ", dados));
LoRaProtocoloTransmissaoModel equipamento = LeituraDadosOperacao.FirstOrDefault(x => x.EnderecoCarro == remetente);
if (equipamento == null)
{

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System;
using System.Threading.Tasks;
using System.Diagnostics;
namespace AgroBase.Services
{
@ -22,6 +23,16 @@ namespace AgroBase.Services
return Conectado && Configurado;
}
}
public double FrequenciaEnvioDadosBase { get; set; } = 0.1;
public double TempoEnvioDadosBase
{
get
{
return 1.0 / FrequenciaEnvioDadosBase * 1000.0;
}
}
public double UltimoRxDados { get; set; } = 0;
public double UltimoTxDados { get; set; } = 0;
public LoRaParametrosModel ParametrosSet { get; set; } = new LoRaParametrosModel();
public LoRaParametrosModel ParametrosGet { get; set; } = new LoRaParametrosModel();
public List<FuncoesPinout> Funcoes { get; set; }
@ -93,6 +104,7 @@ namespace AgroBase.Services
byte idNum = payload[1];
Variaveis.OperacaoEmAndamento.DispSen?.AdicionarMensagemFila(idNum, posicao, posicao, payload.Skip(2).ToArray(), false);
UltimoTxDados = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
return true;
}
@ -102,6 +114,12 @@ namespace AgroBase.Services
{
var _Sensoriamento = Variaveis.OperacaoEmAndamento.Sensoriamento;
List<byte[]> dadosGps = LoRaSerializer.SerializeGPS(_Sensoriamento.Gps);
foreach (var dado in dadosGps)
{
Variaveis.LoraService?.EnviarDadosLoRaViaCAN(dado);
}
List<byte[]> dadosOperacao = LoRaSerializer.SerializeOperacao(_Sensoriamento);
foreach (var dado in dadosOperacao)
{
@ -114,11 +132,7 @@ namespace AgroBase.Services
Variaveis.LoraService?.EnviarDadosLoRaViaCAN(dado);
}
List<byte[]> dadosGps = LoRaSerializer.SerializeGPS(_Sensoriamento.Gps);
foreach (var dado in dadosGps)
{
Variaveis.LoraService?.EnviarDadosLoRaViaCAN(dado);
}
Variaveis.LoraService?.EnviarDadosLoRaViaCAN(LoRaSerializer.SerializeControleAtual(_Sensoriamento.Controle));
}

View File

@ -267,9 +267,9 @@ namespace AgroBase.Services
return new List<byte[]>()
{
_fix.ToArray(),
_lat.ToArray(),
_long.ToArray(),
_fix.ToArray(),
_gerais.ToArray(),
};
}

View File

@ -544,6 +544,17 @@ namespace AgroBase.Services.Operadores
}
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Atu), bombasAtualizados.ToArray());
var DadosLra = Variaveis.LoraService;
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Lra),
("timestamp", agora),
("conectado", DadosLra.Conectado),
("configurado", DadosLra.Configurado),
("tempo_base_tx", DadosLra.TempoEnvioDadosBase / 1000.0),
("tempo_base_rx", VariaveisEquipamento.TempoEntrePingsConexao / 1000.0),
("last_tx", DadosLra.UltimoTxDados),
("last_rx", DadosLra.UltimoRxDados)
);
}

View File

@ -29,5 +29,5 @@
"top_topics_and_observing_domains": [ ]
} ],
"hex_encoded_hmac_key": "40F346D3248C3AFDF2BEE1FE496DBD32F7CED6E5AE98B881ABC421AA7E7B5642",
"next_scheduled_calculation_time": "13402424980889204"
"next_scheduled_calculation_time": "13402424980889277"
}

View File

@ -1,3 +1,3 @@
2025/09/08-16:31:17.388 11ec Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/MANIFEST-000001
2025/09/08-16:31:17.394 11ec Recovering log #3
2025/09/08-16:31:17.398 11ec Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/000003.log
2025/09/09-15:46:04.235 680c Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/MANIFEST-000001
2025/09/09-15:46:04.241 680c Recovering log #3
2025/09/09-15:46:04.244 680c Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/000003.log

View File

@ -1,3 +1,3 @@
2025/09/08-16:13:12.809 9cc Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/MANIFEST-000001
2025/09/08-16:13:12.816 9cc Recovering log #3
2025/09/08-16:13:12.819 9cc Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/000003.log
2025/09/09-15:09:11.525 3c58 Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/MANIFEST-000001
2025/09/09-15:09:11.532 3c58 Recovering log #3
2025/09/09-15:09:11.536 3c58 Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Local Storage\leveldb/000003.log

View File

@ -1 +1 @@
{"net":{"http_server_properties":{"servers":[{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13401919905758592","port":443,"protocol_str":"quic"}],"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false,0],"network_stats":{"srtt":12274},"server":"https://tile.openstreetmap.org","supports_spdy":true}],"supports_quic":{"address":"2804:d78:6a8:5d00:3:b137:22ef:9d9c","used_quic":true},"version":5},"network_qualities":{"CAASABiAgICA+P////8B":"4G","CAESABiAgICA+P////8B":"4G","CAISABiAgICA+P////8B":"4G","CAYSABiAgICA+P////8B":"Offline"}}}
{"net":{"http_server_properties":{"servers":[{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13402003564945866","port":443,"protocol_str":"quic"}],"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false,0],"network_stats":{"srtt":37908},"server":"https://tile.openstreetmap.org","supports_spdy":true}],"supports_quic":{"address":"2804:d78:6a8:5d00:593a:511a:74dd:807b","used_quic":true},"version":5},"network_qualities":{"CAASABiAgICA+P////8B":"4G","CAESABiAgICA+P////8B":"4G","CAISABiAgICA+P////8B":"4G","CAYSABiAgICA+P////8B":"Offline"}}}

View File

@ -1 +1 @@
{"sts":[{"expiry":1788891588.105686,"host":"bWGAftl61rqoc0YzqPncsLvQQh/iC2Bdp3ejUeGC83w=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1757355588.105689}],"version":2}
{"sts":[{"expiry":1788979564.946284,"host":"bWGAftl61rqoc0YzqPncsLvQQh/iC2Bdp3ejUeGC83w=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1757443564.946287}],"version":2}

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
2025/09/08-17:01:10.553 11ec Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/MANIFEST-000001
2025/09/08-17:01:10.555 11ec Recovering log #3
2025/09/08-17:01:10.558 11ec Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/000003.log
2025/09/09-15:58:01.007 680c Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/MANIFEST-000001
2025/09/09-15:58:01.008 680c Recovering log #3
2025/09/09-15:58:01.011 680c Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/000003.log

View File

@ -1,3 +1,3 @@
2025/09/08-16:14:02.023 9cc Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/MANIFEST-000001
2025/09/08-16:14:02.024 9cc Recovering log #3
2025/09/08-16:14:02.026 9cc Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/000003.log
2025/09/09-15:44:08.465 3c58 Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/MANIFEST-000001
2025/09/09-15:44:08.467 3c58 Recovering log #3
2025/09/09-15:44:08.470 3c58 Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Session Storage/000003.log

View File

@ -1,3 +1,3 @@
2025/09/08-16:31:17.316 42c4 Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/MANIFEST-000001
2025/09/08-16:31:17.318 42c4 Recovering log #7
2025/09/08-16:31:17.318 42c4 Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/000007.log
2025/09/09-15:46:04.142 58d8 Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/MANIFEST-000001
2025/09/09-15:46:04.143 58d8 Recovering log #7
2025/09/09-15:46:04.144 58d8 Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/000007.log

View File

@ -1,3 +1,3 @@
2025/09/08-16:13:12.733 2a9c Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/MANIFEST-000001
2025/09/08-16:13:12.736 2a9c Recovering log #7
2025/09/08-16:13:12.736 2a9c Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/000007.log
2025/09/09-15:09:11.448 1b60 Reusing MANIFEST C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/MANIFEST-000001
2025/09/09-15:09:11.449 1b60 Recovering log #7
2025/09/09-15:09:11.450 1b60 Reusing old log C:\ZendionInc\agrobot_base\AgroBase\AgroBase\bin\x64\Debug\AgroBase.exe.WebView2\EBWebView\Default\Site Characteristics Database/000007.log

View File

@ -1 +1 @@
139.0.3405.125
140.0.3485.54

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"hashes":{"00AF3F07B5ABB71F6D30337E1EEF62FA280F06EF19485C0CF6B72171F92CCC0A":{"appid":"kpfehajjjbbcifeehjgfgnabifknmdad","fp":"1.00AF3F07B5ABB71F6D30337E1EEF62FA280F06EF19485C0CF6B72171F92CCC0A"},"452064dcff76e03e0e81b4bb3c48ab1c432e040094f7546c7261a90d4a1c1bbf":{"appid":"fgbafbciocncjfbbonhocjaohoknlaco","fp":""},"4B81B4DF3AD971287E1AE02C449344FCFA5D20431DE17B2BA8854CC9CDCE4089":{"appid":"ahmaebgpfccdhgidjaidaoojjcijckba","fp":"1.4B81B4DF3AD971287E1AE02C449344FCFA5D20431DE17B2BA8854CC9CDCE4089"},"89f43cb3df807293de2772d5f01ac2fc1482b38ccc8fdaee859b80642b7a0487":{"appid":"pghocgajpebopihickglahgebcmkcekh","fp":""},"95FD9D48E4FC245A3F3A99A3A16ECD1355050BA3F4AFC555F19A97C7F9B49677":{"appid":"ohckeflnhegojcjlcpbfpciadgikcohk","fp":"1.95FD9D48E4FC245A3F3A99A3A16ECD1355050BA3F4AFC555F19A97C7F9B49677"},"A81D1959892AE4180554347DF1B97834ABBA2E1A5E6B9AEBA000ECEA26EABECC":{"appid":"fppmbhmldokgmleojlplaaodlkibgikh","fp":"1.A81D1959892AE4180554347DF1B97834ABBA2E1A5E6B9AEBA000ECEA26EABECC"},"A99D66CFCE8CA170740CE0403956F4DFAF4683829A89F4B7AD9C95303871E284":{"appid":"eeobbhfgfagbclfofmgbdfoicabjdbkn","fp":"1.A99D66CFCE8CA170740CE0403956F4DFAF4683829A89F4B7AD9C95303871E284"},"b987bdc4f2ad2a409b964921d4a5db1cdbe07f9c98f868293b4cc32acdc42cec":{"appid":"alpjnmnfbgfkmmpcfpejmmoebdndedno","fp":""},"bcb93cba8636743d1fbd32be3bd8ce8ff602323895bf4d136c26b9bc64b0a6a4":{"appid":"ndikpojcjlepofdkaaldkinkjbeeebkl","fp":""},"fa29a6d5775ff340900a65b39b02fc8b4b77d403c048d8debf22f2f5d09c61a0":{"appid":"oankkpibpaokgecfckkdkgaoafllipag","fp":""}}}
{"hashes":{"00AF3F07B5ABB71F6D30337E1EEF62FA280F06EF19485C0CF6B72171F92CCC0A":{"appid":"kpfehajjjbbcifeehjgfgnabifknmdad","fp":"1.00AF3F07B5ABB71F6D30337E1EEF62FA280F06EF19485C0CF6B72171F92CCC0A"},"452064dcff76e03e0e81b4bb3c48ab1c432e040094f7546c7261a90d4a1c1bbf":{"appid":"fgbafbciocncjfbbonhocjaohoknlaco","fp":""},"4B81B4DF3AD971287E1AE02C449344FCFA5D20431DE17B2BA8854CC9CDCE4089":{"appid":"ahmaebgpfccdhgidjaidaoojjcijckba","fp":"1.4B81B4DF3AD971287E1AE02C449344FCFA5D20431DE17B2BA8854CC9CDCE4089"},"8482d8cbe30fdc7c561f12ce566b64102191a99798dae98132c35714020e1aad":{"appid":"jbfaflocpnkhbgcijpkiafdpbjkedane","fp":""},"89f43cb3df807293de2772d5f01ac2fc1482b38ccc8fdaee859b80642b7a0487":{"appid":"pghocgajpebopihickglahgebcmkcekh","fp":""},"95FD9D48E4FC245A3F3A99A3A16ECD1355050BA3F4AFC555F19A97C7F9B49677":{"appid":"ohckeflnhegojcjlcpbfpciadgikcohk","fp":"1.95FD9D48E4FC245A3F3A99A3A16ECD1355050BA3F4AFC555F19A97C7F9B49677"},"A81D1959892AE4180554347DF1B97834ABBA2E1A5E6B9AEBA000ECEA26EABECC":{"appid":"fppmbhmldokgmleojlplaaodlkibgikh","fp":"1.A81D1959892AE4180554347DF1B97834ABBA2E1A5E6B9AEBA000ECEA26EABECC"},"A99D66CFCE8CA170740CE0403956F4DFAF4683829A89F4B7AD9C95303871E284":{"appid":"eeobbhfgfagbclfofmgbdfoicabjdbkn","fp":"1.A99D66CFCE8CA170740CE0403956F4DFAF4683829A89F4B7AD9C95303871E284"},"b987bdc4f2ad2a409b964921d4a5db1cdbe07f9c98f868293b4cc32acdc42cec":{"appid":"alpjnmnfbgfkmmpcfpejmmoebdndedno","fp":""},"bcb93cba8636743d1fbd32be3bd8ce8ff602323895bf4d136c26b9bc64b0a6a4":{"appid":"ndikpojcjlepofdkaaldkinkjbeeebkl","fp":""},"fa29a6d5775ff340900a65b39b02fc8b4b77d403c048d8debf22f2f5d09c61a0":{"appid":"oankkpibpaokgecfckkdkgaoafllipag","fp":""}}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@
<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
#map_28e5199720417a91208a7dff1794e441 {
#map_0d47d5844cb03a1b59409c5f2e45d89e {
position: relative;
width: 100.0%;
height: 100.0%;
@ -54,14 +54,14 @@
<body>
<div class="folium-map" id="map_28e5199720417a91208a7dff1794e441" ></div>
<div class="folium-map" id="map_0d47d5844cb03a1b59409c5f2e45d89e" ></div>
</body>
<script>
var map_28e5199720417a91208a7dff1794e441 = L.map(
"map_28e5199720417a91208a7dff1794e441",
var map_0d47d5844cb03a1b59409c5f2e45d89e = L.map(
"map_0d47d5844cb03a1b59409c5f2e45d89e",
{
center: [0.0, 0.0],
crs: L.CRS.EPSG3857,
@ -77,6 +77,26 @@
var tile_layer_833f84bdc2b417e2650cc33bb5c7c348 = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
"minZoom": 0,
"maxZoom": 19,
"maxNativeZoom": 19,
"noWrap": false,
"attribution": "\u0026copy; \u003ca href=\"https://www.openstreetmap.org/copyright\"\u003eOpenStreetMap\u003c/a\u003e contributors",
"subdomains": "abc",
"detectRetina": false,
"tms": false,
"opacity": 1,
}
);
tile_layer_833f84bdc2b417e2650cc33bb5c7c348.addTo(map_0d47d5844cb03a1b59409c5f2e45d89e);
</script>
<script>
@ -96,7 +116,7 @@
}
trajeto_json_add({"features": []});
trajeto_json.addTo(map_28e5199720417a91208a7dff1794e441);
trajeto_json.addTo(map_0d47d5844cb03a1b59409c5f2e45d89e);
function adicionarGeometria(novaGeometria) {
trajeto_json.addData(novaGeometria);
@ -159,9 +179,9 @@
var marcadorEquipamento = L.marker([0, 0], {
icon: customIcon
}).addTo(map_28e5199720417a91208a7dff1794e441);
}).addTo(map_0d47d5844cb03a1b59409c5f2e45d89e);
var marcadorBase = L.marker([0, 0], {}).addTo(map_28e5199720417a91208a7dff1794e441);
var marcadorBase = L.marker([0, 0], {}).addTo(map_0d47d5844cb03a1b59409c5f2e45d89e);
var icon = L.AwesomeMarkers.icon(
{"extraClasses": "fa-rotate-0", "icon": "info-sign", "iconColor": "white", "markerColor": "red", "prefix": "glyphicon"}
);
@ -226,7 +246,7 @@
}
if (foco) {
map_28e5199720417a91208a7dff1794e441.setView(novaPosicao, map_28e5199720417a91208a7dff1794e441.getZoom());
map_0d47d5844cb03a1b59409c5f2e45d89e.setView(novaPosicao, map_0d47d5844cb03a1b59409c5f2e45d89e.getZoom());
}
}
@ -248,7 +268,7 @@
marcadorDinamico.setRotationAngle(angulo);
adicionarCoordenada("Tj", [novaLongitude, novaLatitude]);
map_28e5199720417a91208a7dff1794e441.setView(novaPosicao, map_28e5199720417a91208a7dff1794e441.getZoom());*/
map_0d47d5844cb03a1b59409c5f2e45d89e.setView(novaPosicao, map_0d47d5844cb03a1b59409c5f2e45d89e.getZoom());*/
});
function calcularOrientacao(P1latitude, P1longitude, P2latitude, P2longitude) {

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,7 @@ def main():
from health_worker.modulos.movimentacao import ModuloMovimentacao
from health_worker.modulos.sensoriamento import ModuloSensoriamento
from health_worker.modulos.atuador import ModuloAtuador
from health_worker.modulos.lora import ModuloLoRa
from health_worker.modulos.imu import IMUCamera
from health_worker.config import mostrar_log
from shared.contexto_global_redis import ContextoGlobalRedis, CmdKey, CtxKey
@ -22,7 +23,8 @@ def main():
T_Code.Mov: ModuloMovimentacao(),
T_Code.Sen: ModuloSensoriamento(),
T_Code.Atu: ModuloAtuador(),
T_Code.Imu: IMUCamera()
T_Code.Lra: ModuloLoRa(),
T_Code.Imu: IMUCamera(),
}
def loop_ativo():

View File

@ -38,8 +38,9 @@ class IMUCamera(ModuloDiagnosticoBase):
self.mostrar_log("Task iniciada")
def parar(self):
self.ativo = False
self.mostrar_log("Task parada")
if self.ativo:
self.ativo = False
self.mostrar_log("Task parada")
def imu_task_loop(self):
t0 = time.time()

View File

@ -0,0 +1,100 @@
import time
from shared.contexto_global_redis import ContextoGlobalRedis
from shared.enums import StatusModulo, T_Code
from health_worker.modulos.base import ModuloDiagnosticoBase
class ModuloLoRa(ModuloDiagnosticoBase):
def __init__(self):
self.t_code = T_Code.Lra
self.nome = "E220"
self.timeout = 5
def atualizar_saude(self):
try:
now = time.perf_counter()
m = ContextoGlobalRedis.get_modulo(self.t_code) or {}
_operacao = ContextoGlobalRedis.get_operacao()
SAUDE_MIN_ALERTA = 80
conectado = bool(m.get("conectado", False))
configurado = bool(m.get("configurado", False))
operacao_iniciada = bool(_operacao.get("iniciado", False))
# períodos-alvo (segundos) — se vier 0/None, usa fallback prudente
tb_tx = float(m.get("tempo_base_tx", 1.0)) or 1.0
tb_rx = float(m.get("tempo_base_rx", 1.0)) or 1.0
# timestamps monotônicos (segundos)
last_tx = float(m.get("last_tx", 0.0))
last_rx = float(m.get("last_rx", 0.0))
age_tx = max(0.0, now - last_tx)
age_rx = max(0.0, now - last_rx)
# tempo-limite pra considerar "morto": 3 períodos ou 3s (o que for maior)
timeout_rx = max(30.0, 3.0 * tb_rx)
timeout_tx = max(30.0, 3.0 * tb_tx) # informativo (RX é vital)
saude = 100
motivos = []
if not conectado:
saude = 0
motivos.append("desconectado")
else:
if not configurado:
saude -= 50
motivos.append("não configurado")
if operacao_iniciada:
# Penalização por atraso relativo ao período-alvo
# Ex.: se tb_rx=1s e age_rx=1.5s → atraso_rel=0.5 → penaliza 0.5*70=35%
atraso_rel_rx = max(0.0, (age_rx / tb_rx) - 1.0)
atraso_rel_tx = max(0.0, (age_tx / tb_tx) - 1.0)
p_rx = int(min(round(atraso_rel_rx * 70), 70)) # RX pesa mais (até 70%)
p_tx = int(min(round(atraso_rel_tx * 40), 40)) # TX pesa menos (até 40%)
if p_rx > 0:
saude -= p_rx
motivos.append(f"RX atrasado {age_rx:.2f}s (-{p_rx}%)")
if p_tx > 0:
saude -= p_tx
motivos.append(f"TX atrasado {age_tx:.2f}s (-{p_tx}%)")
# Se ainda conectado e com RX dentro do timeout, classifica alerta/operante
saude = max(saude, 0)
status = StatusModulo.OPERANTE
if not conectado:
status = StatusModulo.DESCONECTADO
elif saude <= 0:
status = StatusModulo.FALHA
elif saude < SAUDE_MIN_ALERTA:
status = StatusModulo.ALERTA
payload = {
"conectado": conectado,
"status": status.value,
"saude": saude,
"motivos": motivos,
"saude_individual": [],
"condicoes_operacionais": [],
"detalhes": {
"age_rx_s": age_rx,
"age_tx_s": age_tx,
"timeout_rx_s": timeout_rx,
"timeout_tx_s": timeout_tx,
"periodo_alvo_rx_s": tb_rx,
"periodo_alvo_tx_s": tb_tx,
},
}
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.ModKey(self.t_code),
saude=payload
)
except Exception as e:
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")

View File

@ -5,7 +5,7 @@ import redis
import json
from enum import Enum
from shared.enums import ManagerWorkerCommandType, ModoOperacao, StatusModulo, StatusOperacao, T_Code, TiposControladorDirecional, WeedWorkerCommandType
from shared.enums import ManagerWorkerCommandType, ModoOperacao, ParametrosOperacao, StatusModulo, StatusOperacao, T_Code, TiposControladorDirecional, WeedWorkerCommandType
class CtxKey(str, Enum):
DadosCameras = "ctx:dados_cameras_"
@ -247,7 +247,9 @@ class ContextoGlobalRedis:
"saude": saude,
"motivos": motivos,
"saude_individual": saude_individual,
"condicoes_operacionais": condicoes_operacionais
"condicoes_operacionais": condicoes_operacionais,
"operante": mod_operante,
"tem_condicao_critica": tem_condicao_critica
})
# 🔹 Apenas registrar motivo se for módulo obrigatório e estiver ruim
@ -274,9 +276,59 @@ class ContextoGlobalRedis:
tem_mandatorio_config = len(modulos_mandatorios) > 0
ao_menos_um_opcional_presente = len(modulos_opcionais & modulos_presentes) > 0 if len(modulos_opcionais) > 0 else False
# 🔹 Parâmetros mandatórios (mantive sua lógica)
parametros = _operacao.get("parametros_mandatorios", [])
parametros_ok = True # ajuste aqui se tiver validação
# ——— checagem dos PARÂMETROS mandatórios ———
parametros = _operacao.get("parametros_mandatorios", []) or []
motivos_parametros = []
parametros_pendentes = []
def _ok_parametro(p):
try:
tc = cls._deparaparametros(p)
if tc == T_Code.Vzo:
return True
md_i = next((i for i, m in enumerate(status_modulos) if m.get("modulo") == tc.value), -1)
md = status_modulos[md_i]
if not md:
parametros_pendentes.append(int(tc.value))
motivos_parametros.append(f"{p.name if hasattr(p,'name') else str(p)}{tc.name}: módulo ausente/dados indisponíveis")
return False
if not md["operante"]:
status_parametro = StatusModulo(md['status'])
motivos_parametro = md['motivos']
if status_parametro not in [StatusModulo.OPERANTE, StatusModulo.ALERTA]:
motivo_txt = "Desconectado" if status_parametro == StatusModulo.DESCONECTADO else "; ".join(motivos_parametro)
motivos_parametros.append(f"{tc.name}: {motivo_txt}")
return False
if md["tem_condicao_critica"]:
# agregue descrições das críticas, se houver
descs = [
c.get("descricao", "Condição crítica")
for c in (md["condicoes_operacionais"] or [])
if c.get("severidade") == 100
]
if descs:
motivos_parametros.append(f"{tc.name}: " + "; ".join(descs))
else:
motivos_parametros.append(f"{tc.name}: condição crítica ativa")
return False
return True
except Exception as _:
motivos_parametros.append(f"{p.name if hasattr(p,'name') else str(p)}: erro ao avaliar")
return False
if parametros:
params_ok_bools = [ _ok_parametro(p) for p in set(parametros) ]
parametros_ok = all(params_ok_bools)
else:
parametros_ok = True # sem parâmetros mandatórios, considerar ok
# se falhar parâmetro, agregue motivos à lista geral (mantendo tua UX)
if not parametros_ok and motivos_parametros:
motivos_dos_modulos_mandatorios.extend(motivos_parametros)
# 🔹 Debug mode
debug_mode = _operacao.get("debug_mode", False)
@ -312,6 +364,17 @@ class ContextoGlobalRedis:
modulos_opcionais_configurados=list(modulos_opcionais)
)
@classmethod
def _deparaparametros(cls, parametro: ParametrosOperacao):
if parametro == ParametrosOperacao.CameraSolo:
return T_Code.Cam
elif parametro == ParametrosOperacao.Sonar:
return T_Code.Snr
elif parametro == ParametrosOperacao.LoRa:
return T_Code.Lra
else:
return T_Code.Vzo
@classmethod
def _atualiza_status_operacao(cls):
agora = time.time()

View File

@ -70,6 +70,13 @@ class T_Code(IntEnum):
Mod = 115
Snr = 117
class ParametrosOperacao(IntEnum):
CameraSolo = 1
Mapa = 2
Joystick = 3
Sonar = 4
LoRa = 5
class S_Code(IntEnum):
sVZO = -1
sRPM = 1

View File

@ -1417,6 +1417,8 @@ class CameraManager:
if "near_is_bottom" in fuse:
near_is_bottom = bool(fuse["near_is_bottom"])
rgb_frame = cv2.resize(rgb_frame, (1280, 720))
Hf, Wf = rgb_frame.shape[:2]
H, W = custo_f.shape

View File

@ -72,7 +72,7 @@ def load_seg_config(force_reload=False):
# "kernel_morf": 3
# }
_CONFIG_CACHE = {
"debug_visual": False,
"debug_visual": True,
"ia_roi_begin": 0.0,
"ia_roi_size": 1.0,
"ia_resolution": [512,288],
@ -88,7 +88,7 @@ def reload_seg_config():
def load_det_config():
_CONFIG_DET = {
"debug_visual": False,
"debug_visual": True,
"ia_roi_begin": 0.0,
"ia_roi_size": 1.0,
"ia_resolution": [300,300],

View File

@ -411,6 +411,9 @@ class CostmapFuser:
if (d_ref is not None) and np.isfinite(d_ref) and (d_ref > margem_parada):
v_max_sug = float(np.sqrt(max(0.0, 2.0 * a_max_freio * (d_ref - margem_parada))))
if d_for_stop <= dist_necessaria:
stop_now = True
# histerese
st = self._blk_state
if stop_now:

View File

@ -19,6 +19,7 @@ namespace AgroMonitor
RedisService.Iniciar();
RedisService.LimparDadosIniciais();
APIService.IniciarRotinas();
FuncoesGlobais.DefinirEventosPicBtn(picAjustes, "Erro ao definir ajustes!", async () =>
{

View File

@ -34,6 +34,8 @@
this.txtLatitude = new System.Windows.Forms.TextBox();
this.txtLongitude = new System.Windows.Forms.TextBox();
this.pnlDados = new System.Windows.Forms.Panel();
this.txtFix = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.txtCarroConectado = new System.Windows.Forms.TextBox();
this.lblCarroConectado = new System.Windows.Forms.Label();
this.chbFoco = new System.Windows.Forms.CheckBox();
@ -92,8 +94,13 @@
this.tvwFalhas = new System.Windows.Forms.TreeView();
this.lblMapa = new System.Windows.Forms.Label();
this.btnCarregar = new System.Windows.Forms.Button();
this.txtFix = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.txtPrecisao = new System.Windows.Forms.TextBox();
this.label2 = new System.Windows.Forms.Label();
this.txtNSatelites = new System.Windows.Forms.TextBox();
this.label3 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.txtDistanciaEntreLeituras = new System.Windows.Forms.TextBox();
this.txtStatusFix = new System.Windows.Forms.TextBox();
this.pnlDados.SuspendLayout();
this.pnlControle.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.tkbAnguloMP)).BeginInit();
@ -115,7 +122,7 @@
//
this.lblLatitude.AutoSize = true;
this.lblLatitude.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblLatitude.Location = new System.Drawing.Point(12, 159);
this.lblLatitude.Location = new System.Drawing.Point(12, 202);
this.lblLatitude.Name = "lblLatitude";
this.lblLatitude.Size = new System.Drawing.Size(59, 17);
this.lblLatitude.TabIndex = 2;
@ -125,7 +132,7 @@
//
this.lblLongitude.AutoSize = true;
this.lblLongitude.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblLongitude.Location = new System.Drawing.Point(117, 159);
this.lblLongitude.Location = new System.Drawing.Point(117, 202);
this.lblLongitude.Name = "lblLongitude";
this.lblLongitude.Size = new System.Drawing.Size(71, 17);
this.lblLongitude.TabIndex = 3;
@ -133,7 +140,7 @@
//
// txtLatitude
//
this.txtLatitude.Location = new System.Drawing.Point(11, 179);
this.txtLatitude.Location = new System.Drawing.Point(11, 222);
this.txtLatitude.Name = "txtLatitude";
this.txtLatitude.ReadOnly = true;
this.txtLatitude.Size = new System.Drawing.Size(100, 20);
@ -142,7 +149,7 @@
//
// txtLongitude
//
this.txtLongitude.Location = new System.Drawing.Point(119, 179);
this.txtLongitude.Location = new System.Drawing.Point(119, 222);
this.txtLongitude.Name = "txtLongitude";
this.txtLongitude.ReadOnly = true;
this.txtLongitude.Size = new System.Drawing.Size(100, 20);
@ -152,6 +159,13 @@
// pnlDados
//
this.pnlDados.AutoScroll = true;
this.pnlDados.Controls.Add(this.txtStatusFix);
this.pnlDados.Controls.Add(this.txtPrecisao);
this.pnlDados.Controls.Add(this.label2);
this.pnlDados.Controls.Add(this.txtNSatelites);
this.pnlDados.Controls.Add(this.label3);
this.pnlDados.Controls.Add(this.label4);
this.pnlDados.Controls.Add(this.txtDistanciaEntreLeituras);
this.pnlDados.Controls.Add(this.txtFix);
this.pnlDados.Controls.Add(this.label1);
this.pnlDados.Controls.Add(this.txtCarroConectado);
@ -211,6 +225,25 @@
this.pnlDados.Size = new System.Drawing.Size(274, 594);
this.pnlDados.TabIndex = 6;
//
// txtFix
//
this.txtFix.Location = new System.Drawing.Point(159, 136);
this.txtFix.Name = "txtFix";
this.txtFix.ReadOnly = true;
this.txtFix.Size = new System.Drawing.Size(60, 20);
this.txtFix.TabIndex = 56;
this.txtFix.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.label1.Location = new System.Drawing.Point(157, 116);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(66, 17);
this.label1.TabIndex = 55;
this.label1.Text = "Correção";
//
// txtCarroConectado
//
this.txtCarroConectado.Location = new System.Drawing.Point(113, 75);
@ -279,7 +312,7 @@
this.pnlControle.Controls.Add(this.cmbTipoMovimento);
this.pnlControle.Controls.Add(this.btnParar);
this.pnlControle.Controls.Add(this.btnControle);
this.pnlControle.Location = new System.Drawing.Point(11, 669);
this.pnlControle.Location = new System.Drawing.Point(11, 727);
this.pnlControle.Name = "pnlControle";
this.pnlControle.Size = new System.Drawing.Size(233, 292);
this.pnlControle.TabIndex = 49;
@ -365,7 +398,7 @@
//
this.lblControle.AutoSize = true;
this.lblControle.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblControle.Location = new System.Drawing.Point(12, 649);
this.lblControle.Location = new System.Drawing.Point(12, 703);
this.lblControle.Name = "lblControle";
this.lblControle.Size = new System.Drawing.Size(61, 17);
this.lblControle.TabIndex = 48;
@ -373,7 +406,7 @@
//
// txtTempoEstimado
//
this.txtTempoEstimado.Location = new System.Drawing.Point(119, 265);
this.txtTempoEstimado.Location = new System.Drawing.Point(119, 335);
this.txtTempoEstimado.Name = "txtTempoEstimado";
this.txtTempoEstimado.ReadOnly = true;
this.txtTempoEstimado.Size = new System.Drawing.Size(100, 20);
@ -384,7 +417,7 @@
//
this.lblTempoEstimado.AutoSize = true;
this.lblTempoEstimado.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblTempoEstimado.Location = new System.Drawing.Point(117, 245);
this.lblTempoEstimado.Location = new System.Drawing.Point(117, 315);
this.lblTempoEstimado.Name = "lblTempoEstimado";
this.lblTempoEstimado.Size = new System.Drawing.Size(66, 17);
this.lblTempoEstimado.TabIndex = 45;
@ -394,7 +427,7 @@
//
this.lblTempoDecorrido.AutoSize = true;
this.lblTempoDecorrido.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblTempoDecorrido.Location = new System.Drawing.Point(12, 245);
this.lblTempoDecorrido.Location = new System.Drawing.Point(12, 315);
this.lblTempoDecorrido.Name = "lblTempoDecorrido";
this.lblTempoDecorrido.Size = new System.Drawing.Size(70, 17);
this.lblTempoDecorrido.TabIndex = 44;
@ -402,7 +435,7 @@
//
// txtTempoDecorrido
//
this.txtTempoDecorrido.Location = new System.Drawing.Point(11, 265);
this.txtTempoDecorrido.Location = new System.Drawing.Point(11, 335);
this.txtTempoDecorrido.Name = "txtTempoDecorrido";
this.txtTempoDecorrido.ReadOnly = true;
this.txtTempoDecorrido.Size = new System.Drawing.Size(100, 20);
@ -413,7 +446,7 @@
//
this.lblProgressoRua.AutoSize = true;
this.lblProgressoRua.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblProgressoRua.Location = new System.Drawing.Point(118, 288);
this.lblProgressoRua.Location = new System.Drawing.Point(118, 358);
this.lblProgressoRua.Name = "lblProgressoRua";
this.lblProgressoRua.Size = new System.Drawing.Size(50, 17);
this.lblProgressoRua.TabIndex = 42;
@ -421,7 +454,7 @@
//
// pgbProgressoRua
//
this.pgbProgressoRua.Location = new System.Drawing.Point(117, 308);
this.pgbProgressoRua.Location = new System.Drawing.Point(117, 378);
this.pgbProgressoRua.Name = "pgbProgressoRua";
this.pgbProgressoRua.Size = new System.Drawing.Size(100, 20);
this.pgbProgressoRua.TabIndex = 41;
@ -430,7 +463,7 @@
//
this.lblProgressoOperacao.AutoSize = true;
this.lblProgressoOperacao.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblProgressoOperacao.Location = new System.Drawing.Point(12, 288);
this.lblProgressoOperacao.Location = new System.Drawing.Point(12, 358);
this.lblProgressoOperacao.Name = "lblProgressoOperacao";
this.lblProgressoOperacao.Size = new System.Drawing.Size(87, 17);
this.lblProgressoOperacao.TabIndex = 40;
@ -438,14 +471,14 @@
//
// pgbProgressoOperacao
//
this.pgbProgressoOperacao.Location = new System.Drawing.Point(11, 308);
this.pgbProgressoOperacao.Location = new System.Drawing.Point(11, 378);
this.pgbProgressoOperacao.Name = "pgbProgressoOperacao";
this.pgbProgressoOperacao.Size = new System.Drawing.Size(100, 20);
this.pgbProgressoOperacao.TabIndex = 39;
//
// txtStatusCarro
//
this.txtStatusCarro.Location = new System.Drawing.Point(119, 222);
this.txtStatusCarro.Location = new System.Drawing.Point(119, 292);
this.txtStatusCarro.Name = "txtStatusCarro";
this.txtStatusCarro.ReadOnly = true;
this.txtStatusCarro.Size = new System.Drawing.Size(100, 20);
@ -456,7 +489,7 @@
//
this.lblStatusCarro.AutoSize = true;
this.lblStatusCarro.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblStatusCarro.Location = new System.Drawing.Point(117, 202);
this.lblStatusCarro.Location = new System.Drawing.Point(117, 272);
this.lblStatusCarro.Name = "lblStatusCarro";
this.lblStatusCarro.Size = new System.Drawing.Size(43, 17);
this.lblStatusCarro.TabIndex = 37;
@ -466,7 +499,7 @@
//
this.lblStatusOperacao.AutoSize = true;
this.lblStatusOperacao.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblStatusOperacao.Location = new System.Drawing.Point(12, 202);
this.lblStatusOperacao.Location = new System.Drawing.Point(12, 272);
this.lblStatusOperacao.Name = "lblStatusOperacao";
this.lblStatusOperacao.Size = new System.Drawing.Size(71, 17);
this.lblStatusOperacao.TabIndex = 36;
@ -474,7 +507,7 @@
//
// txtStatusOperacao
//
this.txtStatusOperacao.Location = new System.Drawing.Point(11, 222);
this.txtStatusOperacao.Location = new System.Drawing.Point(11, 292);
this.txtStatusOperacao.Name = "txtStatusOperacao";
this.txtStatusOperacao.ReadOnly = true;
this.txtStatusOperacao.Size = new System.Drawing.Size(100, 20);
@ -485,7 +518,7 @@
//
this.lblErvasIdentificadas.AutoSize = true;
this.lblErvasIdentificadas.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblErvasIdentificadas.Location = new System.Drawing.Point(158, 427);
this.lblErvasIdentificadas.Location = new System.Drawing.Point(158, 497);
this.lblErvasIdentificadas.Name = "lblErvasIdentificadas";
this.lblErvasIdentificadas.Size = new System.Drawing.Size(54, 13);
this.lblErvasIdentificadas.TabIndex = 34;
@ -495,7 +528,7 @@
//
this.lblVelocidadeDados.AutoSize = true;
this.lblVelocidadeDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblVelocidadeDados.Location = new System.Drawing.Point(117, 612);
this.lblVelocidadeDados.Location = new System.Drawing.Point(117, 682);
this.lblVelocidadeDados.Name = "lblVelocidadeDados";
this.lblVelocidadeDados.Size = new System.Drawing.Size(82, 13);
this.lblVelocidadeDados.TabIndex = 33;
@ -505,7 +538,7 @@
//
this.lblVelocidade.AutoSize = true;
this.lblVelocidade.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblVelocidade.Location = new System.Drawing.Point(12, 589);
this.lblVelocidade.Location = new System.Drawing.Point(12, 659);
this.lblVelocidade.Name = "lblVelocidade";
this.lblVelocidade.Size = new System.Drawing.Size(78, 17);
this.lblVelocidade.TabIndex = 32;
@ -513,7 +546,7 @@
//
// pgbVelocidade
//
this.pgbVelocidade.Location = new System.Drawing.Point(11, 609);
this.pgbVelocidade.Location = new System.Drawing.Point(11, 679);
this.pgbVelocidade.Maximum = 20;
this.pgbVelocidade.Name = "pgbVelocidade";
this.pgbVelocidade.Size = new System.Drawing.Size(100, 20);
@ -523,7 +556,7 @@
//
this.lblHerbicidaDados.AutoSize = true;
this.lblHerbicidaDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblHerbicidaDados.Location = new System.Drawing.Point(117, 440);
this.lblHerbicidaDados.Location = new System.Drawing.Point(117, 510);
this.lblHerbicidaDados.Name = "lblHerbicidaDados";
this.lblHerbicidaDados.Size = new System.Drawing.Size(100, 13);
this.lblHerbicidaDados.TabIndex = 30;
@ -533,7 +566,7 @@
//
this.lblHerbicida.AutoSize = true;
this.lblHerbicida.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblHerbicida.Location = new System.Drawing.Point(12, 417);
this.lblHerbicida.Location = new System.Drawing.Point(12, 487);
this.lblHerbicida.Name = "lblHerbicida";
this.lblHerbicida.Size = new System.Drawing.Size(126, 17);
this.lblHerbicida.TabIndex = 29;
@ -541,7 +574,7 @@
//
// pgbHerbicidaAplicado
//
this.pgbHerbicidaAplicado.Location = new System.Drawing.Point(11, 437);
this.pgbHerbicidaAplicado.Location = new System.Drawing.Point(11, 507);
this.pgbHerbicidaAplicado.Maximum = 35;
this.pgbHerbicidaAplicado.Name = "pgbHerbicidaAplicado";
this.pgbHerbicidaAplicado.Size = new System.Drawing.Size(100, 20);
@ -551,7 +584,7 @@
//
this.lblPressaoDados.AutoSize = true;
this.lblPressaoDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblPressaoDados.Location = new System.Drawing.Point(117, 483);
this.lblPressaoDados.Location = new System.Drawing.Point(117, 553);
this.lblPressaoDados.Name = "lblPressaoDados";
this.lblPressaoDados.Size = new System.Drawing.Size(44, 13);
this.lblPressaoDados.TabIndex = 27;
@ -561,7 +594,7 @@
//
this.lblPressao.AutoSize = true;
this.lblPressao.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblPressao.Location = new System.Drawing.Point(12, 460);
this.lblPressao.Location = new System.Drawing.Point(12, 530);
this.lblPressao.Name = "lblPressao";
this.lblPressao.Size = new System.Drawing.Size(119, 17);
this.lblPressao.TabIndex = 26;
@ -569,7 +602,7 @@
//
// pgbPressao
//
this.pgbPressao.Location = new System.Drawing.Point(11, 480);
this.pgbPressao.Location = new System.Drawing.Point(11, 550);
this.pgbPressao.Maximum = 150;
this.pgbPressao.Name = "pgbPressao";
this.pgbPressao.Size = new System.Drawing.Size(100, 20);
@ -579,7 +612,7 @@
//
this.lblTemperaturaCampoDados.AutoSize = true;
this.lblTemperaturaCampoDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblTemperaturaCampoDados.Location = new System.Drawing.Point(117, 569);
this.lblTemperaturaCampoDados.Location = new System.Drawing.Point(117, 639);
this.lblTemperaturaCampoDados.Name = "lblTemperaturaCampoDados";
this.lblTemperaturaCampoDados.Size = new System.Drawing.Size(42, 13);
this.lblTemperaturaCampoDados.TabIndex = 24;
@ -589,7 +622,7 @@
//
this.lblTemperaturaCampo.AutoSize = true;
this.lblTemperaturaCampo.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblTemperaturaCampo.Location = new System.Drawing.Point(8, 546);
this.lblTemperaturaCampo.Location = new System.Drawing.Point(8, 616);
this.lblTemperaturaCampo.Name = "lblTemperaturaCampo";
this.lblTemperaturaCampo.Size = new System.Drawing.Size(138, 17);
this.lblTemperaturaCampo.TabIndex = 23;
@ -597,7 +630,7 @@
//
// pgbTemperaturaCampo
//
this.pgbTemperaturaCampo.Location = new System.Drawing.Point(11, 566);
this.pgbTemperaturaCampo.Location = new System.Drawing.Point(11, 636);
this.pgbTemperaturaCampo.Name = "pgbTemperaturaCampo";
this.pgbTemperaturaCampo.Size = new System.Drawing.Size(100, 20);
this.pgbTemperaturaCampo.TabIndex = 22;
@ -606,7 +639,7 @@
//
this.lblTemperaturaMotoresDados.AutoSize = true;
this.lblTemperaturaMotoresDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblTemperaturaMotoresDados.Location = new System.Drawing.Point(117, 526);
this.lblTemperaturaMotoresDados.Location = new System.Drawing.Point(117, 596);
this.lblTemperaturaMotoresDados.Name = "lblTemperaturaMotoresDados";
this.lblTemperaturaMotoresDados.Size = new System.Drawing.Size(42, 13);
this.lblTemperaturaMotoresDados.TabIndex = 21;
@ -616,7 +649,7 @@
//
this.lblTemperaturaMotores.AutoSize = true;
this.lblTemperaturaMotores.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblTemperaturaMotores.Location = new System.Drawing.Point(12, 503);
this.lblTemperaturaMotores.Location = new System.Drawing.Point(12, 573);
this.lblTemperaturaMotores.Name = "lblTemperaturaMotores";
this.lblTemperaturaMotores.Size = new System.Drawing.Size(145, 17);
this.lblTemperaturaMotores.TabIndex = 20;
@ -624,7 +657,7 @@
//
// pgbTemperaturaMotores
//
this.pgbTemperaturaMotores.Location = new System.Drawing.Point(11, 523);
this.pgbTemperaturaMotores.Location = new System.Drawing.Point(11, 593);
this.pgbTemperaturaMotores.Maximum = 200;
this.pgbTemperaturaMotores.Name = "pgbTemperaturaMotores";
this.pgbTemperaturaMotores.Size = new System.Drawing.Size(100, 20);
@ -634,7 +667,7 @@
//
this.lblReservatorioDados.AutoSize = true;
this.lblReservatorioDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblReservatorioDados.Location = new System.Drawing.Point(117, 397);
this.lblReservatorioDados.Location = new System.Drawing.Point(117, 467);
this.lblReservatorioDados.Name = "lblReservatorioDados";
this.lblReservatorioDados.Size = new System.Drawing.Size(75, 13);
this.lblReservatorioDados.TabIndex = 15;
@ -644,7 +677,7 @@
//
this.lblBateriaDados.AutoSize = true;
this.lblBateriaDados.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
this.lblBateriaDados.Location = new System.Drawing.Point(117, 354);
this.lblBateriaDados.Location = new System.Drawing.Point(117, 424);
this.lblBateriaDados.Name = "lblBateriaDados";
this.lblBateriaDados.Size = new System.Drawing.Size(98, 13);
this.lblBateriaDados.TabIndex = 14;
@ -654,7 +687,7 @@
//
this.lblReservatorio.AutoSize = true;
this.lblReservatorio.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblReservatorio.Location = new System.Drawing.Point(12, 374);
this.lblReservatorio.Location = new System.Drawing.Point(12, 444);
this.lblReservatorio.Name = "lblReservatorio";
this.lblReservatorio.Size = new System.Drawing.Size(153, 17);
this.lblReservatorio.TabIndex = 13;
@ -662,7 +695,7 @@
//
// pgbReservatorio
//
this.pgbReservatorio.Location = new System.Drawing.Point(11, 394);
this.pgbReservatorio.Location = new System.Drawing.Point(11, 464);
this.pgbReservatorio.Maximum = 35;
this.pgbReservatorio.Name = "pgbReservatorio";
this.pgbReservatorio.Size = new System.Drawing.Size(100, 20);
@ -672,7 +705,7 @@
//
this.lblBateria.AutoSize = true;
this.lblBateria.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblBateria.Location = new System.Drawing.Point(12, 331);
this.lblBateria.Location = new System.Drawing.Point(12, 401);
this.lblBateria.Name = "lblBateria";
this.lblBateria.Size = new System.Drawing.Size(53, 17);
this.lblBateria.TabIndex = 11;
@ -680,14 +713,14 @@
//
// pgbBateria
//
this.pgbBateria.Location = new System.Drawing.Point(11, 351);
this.pgbBateria.Location = new System.Drawing.Point(11, 421);
this.pgbBateria.Name = "pgbBateria";
this.pgbBateria.Size = new System.Drawing.Size(100, 20);
this.pgbBateria.TabIndex = 10;
//
// txtOrientacao
//
this.txtOrientacao.Location = new System.Drawing.Point(93, 136);
this.txtOrientacao.Location = new System.Drawing.Point(77, 136);
this.txtOrientacao.Name = "txtOrientacao";
this.txtOrientacao.ReadOnly = true;
this.txtOrientacao.Size = new System.Drawing.Size(76, 20);
@ -698,7 +731,7 @@
//
this.lblOrientacao.AutoSize = true;
this.lblOrientacao.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.lblOrientacao.Location = new System.Drawing.Point(91, 116);
this.lblOrientacao.Location = new System.Drawing.Point(75, 116);
this.lblOrientacao.Name = "lblOrientacao";
this.lblOrientacao.Size = new System.Drawing.Size(78, 17);
this.lblOrientacao.TabIndex = 8;
@ -719,7 +752,7 @@
this.txtLeitura.Location = new System.Drawing.Point(11, 136);
this.txtLeitura.Name = "txtLeitura";
this.txtLeitura.ReadOnly = true;
this.txtLeitura.Size = new System.Drawing.Size(76, 20);
this.txtLeitura.Size = new System.Drawing.Size(60, 20);
this.txtLeitura.TabIndex = 6;
this.txtLeitura.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
@ -768,24 +801,71 @@
this.btnCarregar.UseVisualStyleBackColor = true;
this.btnCarregar.Click += new System.EventHandler(this.btnCarregar_Click);
//
// txtFix
// txtPrecisao
//
this.txtFix.Location = new System.Drawing.Point(175, 136);
this.txtFix.Name = "txtFix";
this.txtFix.ReadOnly = true;
this.txtFix.Size = new System.Drawing.Size(44, 20);
this.txtFix.TabIndex = 56;
this.txtFix.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.txtPrecisao.Location = new System.Drawing.Point(159, 179);
this.txtPrecisao.Name = "txtPrecisao";
this.txtPrecisao.ReadOnly = true;
this.txtPrecisao.Size = new System.Drawing.Size(60, 20);
this.txtPrecisao.TabIndex = 62;
this.txtPrecisao.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// label1
// label2
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.label1.Location = new System.Drawing.Point(173, 116);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(25, 17);
this.label1.TabIndex = 55;
this.label1.Text = "Fix";
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.label2.Location = new System.Drawing.Point(157, 159);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(63, 17);
this.label2.TabIndex = 61;
this.label2.Text = "Precisão";
//
// txtNSatelites
//
this.txtNSatelites.Location = new System.Drawing.Point(78, 179);
this.txtNSatelites.Name = "txtNSatelites";
this.txtNSatelites.ReadOnly = true;
this.txtNSatelites.Size = new System.Drawing.Size(75, 20);
this.txtNSatelites.TabIndex = 60;
this.txtNSatelites.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// label3
//
this.label3.AutoSize = true;
this.label3.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.label3.Location = new System.Drawing.Point(76, 159);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(62, 17);
this.label3.TabIndex = 59;
this.label3.Text = "Satelites";
//
// label4
//
this.label4.AutoSize = true;
this.label4.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.25F);
this.label4.Location = new System.Drawing.Point(12, 159);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(66, 17);
this.label4.TabIndex = 58;
this.label4.Text = "Distancia";
//
// txtDistanciaEntreLeituras
//
this.txtDistanciaEntreLeituras.Location = new System.Drawing.Point(11, 179);
this.txtDistanciaEntreLeituras.Name = "txtDistanciaEntreLeituras";
this.txtDistanciaEntreLeituras.ReadOnly = true;
this.txtDistanciaEntreLeituras.Size = new System.Drawing.Size(60, 20);
this.txtDistanciaEntreLeituras.TabIndex = 57;
this.txtDistanciaEntreLeituras.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// txtStatusFix
//
this.txtStatusFix.Location = new System.Drawing.Point(11, 248);
this.txtStatusFix.Name = "txtStatusFix";
this.txtStatusFix.ReadOnly = true;
this.txtStatusFix.Size = new System.Drawing.Size(208, 20);
this.txtStatusFix.TabIndex = 63;
this.txtStatusFix.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// frmMonitoramento
//
@ -879,5 +959,12 @@
private System.Windows.Forms.Label lblCarroConectado;
private System.Windows.Forms.TextBox txtFix;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox txtPrecisao;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox txtNSatelites;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TextBox txtDistanciaEntreLeituras;
private System.Windows.Forms.TextBox txtStatusFix;
}
}

View File

@ -125,11 +125,15 @@ namespace AgroMonitor.Forms
{
MomentoLog = DateTime.Now;
txtLeitura.Text = MomentoLog.ToString("ddd HH:mm:ss");
txtLeitura.Text = MomentoLog.ToString("HH:mm:ss");
txtOrientacao.Text = GPSService.UltimaLeitura.OrientacaoReal.ToString("0.00");
txtLatitude.Text = GPSService.UltimaLeitura.Latitude.ToString();
txtLongitude.Text = GPSService.UltimaLeitura.Longitude.ToString();
txtFix.Text = GPSService.UltimaLeitura.QualidadeFix.ToString();
txtDistanciaEntreLeituras.Text = (GPSService.UltimaLeitura.Distancia * 100.0).ToString("0.00") + " cm";
txtNSatelites.Text = GPSService.UltimaLeitura.NumeroSatelites.ToString();
txtPrecisao.Text = GPSService.UltimaLeitura.PrecisaoCm.ToString("0.00") + " cm";
txtStatusFix.Text = BaseFixService.ProgressoStr;
txtCodigoFalha.Text = "";
txtStatusOperacao.Text = "";

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,13 @@ std::vector<SensorFluxo*> listaSensoresFluxo;
std::vector<SensorMassa*> listaSensoresMassa;
std::vector<SensorPressao*> listaSensoresPressao;
void MostrarLog(String mensagem) {
bool _debugMode = false;
if (_debugMode) {
PrintTela(String("[ATU] ") + mensagem);
}
}
void setup() {
delay(5000);
@ -100,7 +107,7 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) {
return;
}
PrintTela("[ATU] Atualizando dados dos sensores...");
MostrarLog("Atualizando dados dos sensores...");
if (todosIDs && enviarStatus) {
EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, Conectado, VERSION));
@ -125,7 +132,7 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) {
EnviarDadosCAN(canService.MontarFrameReqDadosFim(latenciaLoop));
}
PrintTela("[ATU] Ciclo concluido, latencia de loop: " + String(latenciaLoop));
MostrarLog("Ciclo concluido, latencia de loop: " + String(latenciaLoop));
}
}
@ -186,7 +193,7 @@ void ProcessarCfg(std::vector<uint8_t> data) {
Conectado = conectar;
_chkRx = canService.MontarFrameReqStatusMod(D_Code, Conectado, VERSION);
PrintTela("[ATU] Configuracao do modulo concluida");
MostrarLog("Configuracao do modulo concluida");
break;
}
}
@ -243,7 +250,7 @@ void LimparListasComponentes() {
for (auto* bico : listaBicos) {
if (bico) {
bico->Desligar();
PrintTela("[ATU] Bico " + bico->_ID + " desligado.");
MostrarLog("Bico " + bico->_ID + " desligado.");
delete bico;
}
}
@ -252,7 +259,7 @@ void LimparListasComponentes() {
for (auto* bomba : listaBombas) {
if (bomba) {
bomba->Desligar();
PrintTela("[ATU] Bomba Pressurizadora " + bomba->_ID + " desligada.");
MostrarLog("Bomba Pressurizadora " + bomba->_ID + " desligada.");
delete bomba;
}
}
@ -261,7 +268,7 @@ void LimparListasComponentes() {
for (auto* sensor : listaSensoresFluxo) {
if (sensor) {
sensor->Desligar();
PrintTela("[ATU] Sensor de Fluxo " + sensor->_ID + " desligado.");
MostrarLog("Sensor de Fluxo " + sensor->_ID + " desligado.");
delete sensor;
}
}
@ -270,7 +277,7 @@ void LimparListasComponentes() {
for (auto* sensor : listaSensoresMassa) {
if (sensor) {
sensor->Desligar();
PrintTela("[ATU] Sensor de Massa " + sensor->_ID + " desligado.");
MostrarLog("Sensor de Massa " + sensor->_ID + " desligado.");
delete sensor;
}
}
@ -279,7 +286,7 @@ void LimparListasComponentes() {
for (auto* sensor : listaSensoresPressao) {
if (sensor) {
sensor->Desligar();
PrintTela("[ATU] Sensor de Pressao " + sensor->_ID + " desligado.");
MostrarLog("Sensor de Pressao " + sensor->_ID + " desligado.");
delete sensor;
}
}

View File

@ -374,8 +374,8 @@ public:
}
void HealthTask(void* pvParameters) {
const uint32_t RX_TIMEOUT_MS = 3000; // ajuste conforme seu tráfego esperado
const uint32_t TX_STALL_MS = 2000; // quanto tempo uma chave pode ficar sem sair
const uint32_t RX_TIMEOUT_MS = 5000; // ajuste conforme seu tráfego esperado
const uint32_t TX_STALL_MS = 3000; // quanto tempo uma chave pode ficar sem sair
const uint8_t MAX_REC_FAILS = 3; // depois disso, reboot
static uint8_t consecutive_rec_fails = 0;

View File

@ -398,7 +398,7 @@ class I2CService {
};
bool I2CService::DebugMode = true;
bool I2CService::DebugMode = false;
int I2CService::_pinoSDA = 1;
int I2CService::_pinoSCL = 2;
unsigned long I2CService::LimiteTempoI2C = 2000;

View File

@ -57,7 +57,7 @@ public:
// Inicializa GPIOs
pinMode(_pinM0, OUTPUT);
pinMode(_pinM1, OUTPUT);
pinMode(_pinAUX, INPUT_PULLUP);
pinMode(_pinAUX, INPUT);
_serialLoRa = &Serial2;
if (Conectado) {
@ -224,6 +224,9 @@ public:
Configurado = configurarModuloLoRa(addr, baud, packet, channel, worCycle);
}
}
else {
MostrarLog("Sem resposta do modulo ao requisitar parametros");
}
}
EnviarDadosCAN(MontarMensagemCAN(CanMessagePosicaoDados::Status));
EnviarDadosCAN(MontarMensagemCAN(CanMessagePosicaoDados::Dados1));
@ -249,10 +252,12 @@ private:
int addrDestinatario = -1;
std::vector<uint8_t> buffer;
unsigned long ultimoByteRecebido = 0;
const unsigned long timeoutRecebimentoMs = 100; // por exemplo, 100ms
const unsigned long timeoutRecebimentoMs = 800; // por exemplo, 100ms
const unsigned long tempoEntreEnvios = 200;
void reiniciarEstado() {
recebendo = false;
PausarTX = false;
bytesEsperados = -1;
addrRemetente = -1;
addrDestinatario = -1;
@ -271,7 +276,7 @@ private:
LoRaService* service = static_cast<LoRaService*>(pvParameters);
while (true) {
if (!Conectado || !Configurado || PausarRX) {
vTaskDelay(1000);
vTaskDelay(500);
continue;
}
if (recebendo && (millis() - ultimoByteRecebido > timeoutRecebimentoMs)) {
@ -288,9 +293,17 @@ private:
reiniciarEstado();
MostrarLog("Iniciou o recebimento dos dados LoRa");
recebendo = true;
PausarTX = true;
}
}
else {
if (byteRecebido == CodigosFuncoes::BeginMsg) {
MostrarLog("Re-sync: novo BeginMsg detectado durante recepcao");
reiniciarEstado();
recebendo = true;
PausarTX = true;
continue;
}
if (bytesEsperados == -1) {
bytesEsperados = byteRecebido;
//MostrarLog("Definindo bytesEsperados = " + String(bytesEsperados));
@ -301,15 +314,45 @@ private:
}
else if (addrDestinatario == -1) {
addrDestinatario = byteRecebido;
//MostrarLog("Definindo addrDestinatario = " + String(addrDestinatario));
if (addrDestinatario != parametrosAtual.address) {
MostrarLog("Mensagem para outro ID, descartando");
MostrarLog("Mensagem para outro ID, descartando ate EndMsg");
// drenar ate checksum + 0x55 com base em 'bytesEsperados'
size_t toDrain = (bytesEsperados >= 0 ? bytesEsperados + 2 : 0);
for (size_t i = 0; i < toDrain; ++i) {
uint8_t dump;
if (!service->_serialLoRa->available()) break;
dump = service->_serialLoRa->read();
}
reiniciarEstado();
continue;
}
}
else {
buffer.push_back(byteRecebido);
if (bytesEsperados >= 0 && buffer.size() == (size_t)bytesEsperados) {
// tentar ler checksum e EndMsg com timeouts curtinhos
uint32_t t0 = millis();
while (service->_serialLoRa->available() < 2 && (millis() - t0) < 200) {
vTaskDelay(1);
}
if (service->_serialLoRa->available() >= 2) {
uint8_t cks = service->_serialLoRa->read();
uint8_t endb = service->_serialLoRa->read();
buffer.push_back(cks);
buffer.push_back(endb);
}
else {
MostrarLog("Cauda perdida, adicionando manualmente...");
uint8_t soma = 0;
for (size_t i = 0; i < buffer.size(); ++i) {
soma += buffer[i];
}
buffer.push_back(soma);
buffer.push_back(CodigosFuncoes::EndMsg);
}
}
bool protocoloCompleto = addrRemetente != -1 && addrDestinatario != -1 && buffer.size() == bytesEsperados + 2; // +2 = checksum + end
MostrarLog("protocoloCompleto = " + String(protocoloCompleto) + ", bufferSize = " + buffer.size() + ", bytesEsperados = " + String(bytesEsperados));
@ -362,9 +405,10 @@ private:
std::vector<uint8_t> msg;
while (true) {
if (!Conectado || !Configurado || PausarTX) {
vTaskDelay(1000);
vTaskDelay(500);
continue;
}
if (xQueueReceive(service->lraQueueTx, &msg, portMAX_DELAY) == pdTRUE) {
service->EnviarDadosLoRa(msg);
}
@ -442,60 +486,46 @@ private:
bool setLoRaMode(LoRaMode mode) {
if (mode == currentMode) return true;
// 0) Garante direção dos pinos e pull-up no AUX
pinMode(_pinM0, OUTPUT);
pinMode(_pinM1, OUTPUT);
pinMode(_pinAUX, INPUT_PULLUP); // evita flutuação
// 1) Pausa tarefas que podem forçar TX/RX enquanto troca
pauseLoRaTasks();
// 2) Aguarda o rádio estar ocioso antes de mexer em M0/M1
if (!waitAUXHigh(2000)) {
MostrarLog("Erro: AUX não ficou HIGH antes da troca de modo.");
resumeLoRaTasks();
return false;
}
// 3) Para tráfego de UART e limpa buffers
drainUart();
// 4) Comuta M0/M1
// Ajusta os pinos M0 e M1 conforme o modo desejado
switch (mode) {
case NORMAL: digitalWrite(_pinM0, LOW); digitalWrite(_pinM1, LOW); break;
case WAKE_UP: digitalWrite(_pinM0, HIGH); digitalWrite(_pinM1, LOW); break;
case POWER_SAVING: digitalWrite(_pinM0, LOW); digitalWrite(_pinM1, HIGH); break;
case CONFIG: digitalWrite(_pinM0, HIGH); digitalWrite(_pinM1, HIGH); break;
}
// 5) Esperas mínimas do datasheet
vTaskDelay(5); // >2ms após M0/M1
// 6) Espera o módulo sinalizar pronto no novo modo
if (!waitAUXHigh(2000)) {
MostrarLog("Erro: Timeout aguardando AUX após mudança de modo.");
resumeLoRaTasks();
return false;
}
// 7) Ajustes específicos por modo
if (mode == CONFIG) {
// A UART do módulo em CONFIG é 9600 8N1 SEMPRE.
if (_serialLoRa) _serialLoRa->begin(9600, SERIAL_8N1, _pinRXD, _pinTXD);
drainUart();
}
if (mode == NORMAL) {
// voltar à baud configurada do módulo (ex.: 9600/19200/etc)
if (_serialLoRa) _serialLoRa->begin(_baudRate, SERIAL_8N1, _pinRXD, _pinTXD);
drainUart();
case NORMAL:
digitalWrite(_pinM0, LOW);
digitalWrite(_pinM1, LOW);
break;
case WAKE_UP:
digitalWrite(_pinM0, HIGH);
digitalWrite(_pinM1, LOW);
break;
case POWER_SAVING:
digitalWrite(_pinM0, LOW);
digitalWrite(_pinM1, HIGH);
break;
case CONFIG:
digitalWrite(_pinM0, HIGH);
digitalWrite(_pinM1, HIGH);
break;
}
MostrarLog("Modo alterado de " + String(currentMode) + " para " + String(mode));
currentMode = mode;
MostrarLog("Modo alterado OK: " + String(mode));
// 8) Retoma tarefas só quando NORMAL (evita competição em CONFIG)
if (mode == NORMAL) resumeLoRaTasks();
vTaskDelay(100); // Aguarda sinalização inicial de troca
// ESPERA o AUX ir para HIGH, indicando que a troca completou
unsigned long timeout = millis() + commandTimeout;
while (digitalRead(_pinAUX) == LOW) {
if (millis() > timeout) {
MostrarLog("Erro: Timeout aguardando AUX após mudança de modo.");
resumeLoRaTasks();
return false;
}
}
vTaskDelay(100); // Manual pede 2ms após AUX ficar HIGH
resumeLoRaTasks();
return true;
}
@ -669,6 +699,7 @@ private:
}
}
} else if (b == CodigosFuncoes::WrongMode) {
MostrarLog("Resposta WrongMode");
// opcional: logar que o módulo respondeu “modo errado”
// e talvez sair para re-tentar setLoRaMode(CONFIG)
}
@ -745,6 +776,7 @@ private:
MostrarLog("Enviando dados via LoRa para o endereco " + String(AddressBase) + " no canal " + String(parametrosAtual.channel));
std::vector<uint8_t> dadosLora = MontarFrameLoRa(AddressBase, parametrosAtual.channel, dados);
enviarDadosSerial(dadosLora.data(), dadosLora.size(), "Dados LoRa");
vTaskDelay(pdMS_TO_TICKS(tempoEntreEnvios));
return true;
}

View File

@ -58,6 +58,13 @@ std::vector<SensorLuzUV*> listaSensoresLuzUV;
std::vector<SensorQualidadeAr*> listaSensoresQualidadeAr;
std::vector<SensorChuva*> listaSensoresChuva;
void MostrarLog(String mensagem) {
bool _debugMode = false;
if (_debugMode) {
PrintTela(String("[SEN] ") + mensagem);
}
}
void inicializarDependenciasI2C() {
I2CService::IniciarI2C();
I2CService::IniciarMUX();
@ -122,7 +129,7 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) {
return;
}
PrintTela("Atualizando dados dos sensores...");
MostrarLog("Atualizando dados dos sensores...");
if (todosIDs && enviarStatus) {
EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, Conectado, VERSION));
@ -155,7 +162,7 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) {
EnviarDadosCAN(canService.MontarFrameReqDadosFim(latenciaLoop));
}
PrintTela("[SEN] Ciclo concluido, latencia de loop: " + String(latenciaLoop));
MostrarLog("Ciclo concluido, latencia de loop: " + String(latenciaLoop));
}
}
@ -217,7 +224,7 @@ void ProcessarCfg(std::vector<uint8_t> data) {
Conectado = conectar;
_chkRx = canService.MontarFrameReqStatusMod(D_Code, Conectado, VERSION);
PrintTela("[SEN] Configuracao do modulo concluida");
MostrarLog("Configuracao do modulo concluida");
break;
}
}
@ -312,84 +319,84 @@ void ProcessarCmd(std::vector<uint8_t> data) {
void LimparListasComponentes() {
for (auto* rele : listaReles) {
rele->Desligar();
PrintTela("Rele " + rele->_ID + " desligado.");
MostrarLog("Rele " + rele->_ID + " desligado.");
delete rele;
}
listaReles.clear();
for (auto* sinaleiro : listaSinaleiros) {
sinaleiro->Desligar();
PrintTela("Sinaleiro " + sinaleiro->_ID + " desligado.");
MostrarLog("Sinaleiro " + sinaleiro->_ID + " desligado.");
delete sinaleiro;
}
listaSinaleiros.clear();
for (auto* servo : listaServoFreios) {
servo->Desligar();
PrintTela("Servo " + servo->_ID + " desligado.");
MostrarLog("Servo " + servo->_ID + " desligado.");
delete servo;
}
listaServoFreios.clear();
for (auto* sensor : listaSensoresCorrente) {
sensor->Desligar();
PrintTela("Sensor de Corrente " + sensor->_ID + " desligado.");
MostrarLog("Sensor de Corrente " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresCorrente.clear();
for (auto* sensor : listaSensoresTemperaturaNTC) {
sensor->Desligar();
PrintTela("Sensor de Temperatura " + sensor->_ID + " desligado.");
MostrarLog("Sensor de Temperatura " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresTemperaturaNTC.clear();
for (auto* sensor : listaSensoresIMU) {
sensor->Desligar();
PrintTela("Sensor IMU " + sensor->_ID + " desligado.");
MostrarLog("Sensor IMU " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresIMU.clear();
for (auto* sensor : listaSensoresGas) {
sensor->Desligar();
PrintTela("Sensor de gas " + sensor->_ID + " desligado.");
MostrarLog("Sensor de gas " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresGas.clear();
for (auto* sensor : listaSensoresTemperaturaSHT) {
sensor->Desligar();
PrintTela("Sensor de temperatura e umidade " + sensor->_ID + " desligado.");
MostrarLog("Sensor de temperatura e umidade " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresTemperaturaSHT.clear();
for (auto* sensor : listaSensoresLuminosidade) {
sensor->Desligar();
PrintTela("Sensor de luminosidade " + sensor->_ID + " desligado.");
MostrarLog("Sensor de luminosidade " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresLuminosidade.clear();
for (auto* sensor : listaSensoresLuzUV) {
sensor->Desligar();
PrintTela("Sensor de luz UV " + sensor->_ID + " desligado.");
MostrarLog("Sensor de luz UV " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresLuzUV.clear();
for (auto* sensor : listaSensoresQualidadeAr) {
sensor->Desligar();
PrintTela("Sensor de qualidade do ar " + sensor->_ID + " desligado.");
MostrarLog("Sensor de qualidade do ar " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresQualidadeAr.clear();
for (auto* sensor : listaSensoresChuva) {
sensor->Desligar();
PrintTela("Sensor de chuva " + sensor->_ID + " desligado.");
MostrarLog("Sensor de chuva " + sensor->_ID + " desligado.");
delete sensor;
}
listaSensoresChuva.clear();

View File

@ -12,10 +12,20 @@ MODEL_NAME = config["model_name"]
RESOLUCAO = config["resolucao"]
MAIN_CLASS_NAME = config["main_class_name"]
N_SHAVES = config["shaves"]
use_main_class = config["use_main_class"]
model_to_use = config["model_to_use"]
model_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME)
labelmap_path = os.path.join(MODELO, "dataset", "labelmap.txt")
model_name = f"{MODEL_NAME}_best{f'_f1_{MAIN_CLASS_NAME}' if use_main_class else ''}"
model_name = ""
if model_to_use == "geral":
model_name = f"{MODEL_NAME}_best.pth"
elif model_to_use == "main_class":
MAIN_CLASS_NAME = config["main_class_name"]
model_name = f"{MODEL_NAME}_best_f1_{MAIN_CLASS_NAME}.pth"
elif model_to_use == "es":
ES_CLASSES_NAME = config["es_classes"]
model_name = f"{MODEL_NAME}_best_es_{ES_CLASSES_NAME}.pth"
else:
model_name = f"{MODEL_NAME}_best.pth"
dummy_input = torch.randn(1, 3, RESOLUCAO[1], RESOLUCAO[0]) # (batch, channels, height, width)
@ -23,7 +33,7 @@ _, _, classes, _ = carregar_labelmap_completo(labelmap_path)
NUM_CLASSES = len(classes)
base = FastSCNNWithNorm(num_classes=NUM_CLASSES, to_rgb=True) # ajuste num_classes conforme seu labelmap
base.backbone.load_state_dict(torch.load(os.path.join(model_path, f"{MODEL_NAME}_best.pth"), map_location="cpu"))
base.backbone.load_state_dict(torch.load(os.path.join(model_path, model_name), map_location="cpu"))
base.eval()
torch.onnx.export(

View File

@ -12,12 +12,26 @@ with open("config.json", "r") as f:
MODELO = config["camera"]
MODEL_NAME = config["model_name"]
RESOLUCAO = config["resolucao"]
SHAVES = config["shaves"]
ROI_INICIO = 0.0
ROI_TAMANHO = 1.0
MAIN_CLASS_NAME = config["main_class_name"]
use_main_class = config["use_main_class"]
blob_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, f"{MODEL_NAME}_best{f'_f1_{MAIN_CLASS_NAME}' if use_main_class else ''}_openvino_2022.1_6shave.blob")
model_to_use = config["model_to_use"]
labelmap_path = os.path.join(MODELO, "dataset", "labelmap.txt")
model_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME)
model_name = ""
if model_to_use == "geral":
model_name = f"{MODEL_NAME}_best.pth"
elif model_to_use == "main_class":
MAIN_CLASS_NAME = config["main_class_name"]
model_name = f"{MODEL_NAME}_best_f1_{MAIN_CLASS_NAME}.pth"
elif model_to_use == "es":
ES_CLASSES_NAME = config["es_classes"]
model_name = f"{MODEL_NAME}_best_es_{ES_CLASSES_NAME}.pth"
else:
model_name = f"{MODEL_NAME}_best.pth"
blob_path = os.path.join(model_path, f"{model_name.replace(".pth", "")}_openvino_2022.1_{SHAVES}shave.blob")
# Carregar mapa de cores
_, colormap_rgb, classes, ignore_rgb = carregar_labelmap_completo(labelmap_path)

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Conta a porcentagem de pixels por CLASSE (segundo o labelmap) dentro da ROI,
para cada grupo em dataset/split/train/group.
- Usa utils.carregar_labelmap_completo(labelmap_path) para obter:
colormap_rgb, classes (id->nome) e ignore_rgb
- Ignora pixels com o valor de "ignore" do labelmap
- Normaliza a porcentagem SOMENTE sobre classes válidas (sem ignore)
Uso:
python _12_check_percent_class_labelmap.py
"""
import os, json
import numpy as np
from PIL import Image
from utils import carregar_labelmap_completo
# -------------- Config --------------
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config["camera"]
W, H = config["resolucao"][0], config["resolucao"][1]
ROI_INICIO = config["roi_inicio"]
ROI_TAMANHO = config["roi_tamanho"]
pasta_base = os.path.join(MODELO, "dataset")
labelmap_path = os.path.join(pasta_base, "labelmap.txt")
root = os.path.join(pasta_base, "split", "train", "group")
#root = os.path.join(pasta_base, "576x320", "group")
# Limite de amostras por grupo (para rodar rápido). Ajuste se quiser.
MAX_SAMPLES_PER_GROUP = 1000
# -------------- Utils --------------
def infer_ignore_id(ignore_rgb, default_id=255):
"""
Converte o 'ignore' do labelmap (que pode vir como [id] ou (R,G,B) ou int)
para um ID inteiro que devemos ignorar nas máscaras de IDs.
"""
# pode vir como lista/tupla com 1 elemento (id) ou 3 (cor)
if isinstance(ignore_rgb, (list, tuple)):
if len(ignore_rgb) == 1 and isinstance(ignore_rgb[0], (int, np.integer)):
return int(ignore_rgb[0])
if len(ignore_rgb) == 3:
return default_id
# pode vir como inteiro
if isinstance(ignore_rgb, (int, np.integer)):
return int(ignore_rgb)
return default_id
def roi_slice(h):
y_fim = int((1.0 - ROI_TAMANHO) * h)
y_ini = int(ROI_INICIO * h)
if y_ini <= y_fim:
y_fim, y_ini = max(0, h - int(ROI_TAMANHO * h)), h
return slice(y_fim, y_ini)
# -------------- Labelmap --------------
_, colormap_rgb, classes, ignore_rgb = carregar_labelmap_completo(labelmap_path)
ignore_id = infer_ignore_id(ignore_rgb, default_id=255)
# 'classes' esperado como dict: id -> nome
# Ordena por id para imprimir de forma estável
class_ids_sorted = sorted(classes.keys())
class_names_sorted = [classes[cid] for cid in class_ids_sorted]
# -------------- Coleta --------------
if not os.path.isdir(root):
raise SystemExit(f"Nenhum diretório encontrado em {root}")
grupos = [g for g in os.listdir(root) if os.path.isdir(os.path.join(root, g))]
for g in sorted(grupos):
mdir = os.path.join(root, g, "masks")
if not os.path.isdir(mdir):
continue
totals = {cid: 0 for cid in class_ids_sorted}
n = 0
for fname in os.listdir(mdir):
if not fname.lower().endswith(".png"):
continue
m = np.array(Image.open(os.path.join(mdir, fname)).convert("L"))
rs = roi_slice(m.shape[0])
roi = m[rs, :]
# Acumula só das classes válidas do labelmap (ignorando 'ignore' e outros valores)
for cid in class_ids_sorted:
totals[cid] += int((roi == cid).sum())
n += 1
if n >= MAX_SAMPLES_PER_GROUP:
break
s = sum(totals.values())
s = s if s > 0 else 1 # evita div/0
# Monta string dinâmica "nome=xx.xx%"
parts = []
for cid in class_ids_sorted:
name = classes[cid]
perc = totals[cid] / s
parts.append(f"{name}={perc:6.2%}")
print(f"{g:16s} " + " ".join(parts) + f" (amostras={n})")

View File

@ -0,0 +1,206 @@
# copy_pairs.py
import json
import os
import cv2
import csv
import shutil
import argparse
# ⚙️ Configurações (MODELO via config.json, pode sobrescrever na CLI)
with open("config.json", "r") as f:
config = json.load(f)
MODELO = config.get("camera", ".")
# Pastas
PASTA_NEW_IMAGES = os.path.join(MODELO, "dataset", "original", "new_images")
PASTA_NEW_MASKS = os.path.join(MODELO, "dataset", "original", "new_masks")
PASTA_FINAL_IMAGES = os.path.join(MODELO, "dataset", "original", "images")
PASTA_FINAL_MASKS = os.path.join(MODELO, "dataset", "original", "masks")
# Extensões aceitas
EXT_IMAGENS = (".jpg", ".jpeg", ".png")
EXT_MASKS = (".png", ".jpg", ".jpeg") # prioridade será .png quando houver
MANIFESTO = "manifest.csv"
# ===================================================
def garantir_pasta(p):
os.makedirs(p, exist_ok=True)
def nome_disponivel(dest_dir, base_name, ext):
"""
Retorna um caminho disponível em dest_dir, garantindo unicidade com sufixos _001, _002, ...
"""
cand = os.path.join(dest_dir, base_name + ext)
if not os.path.exists(cand):
return cand
i = 1
while True:
cand = os.path.join(dest_dir, f"{base_name}_{i:03d}{ext}")
if not os.path.exists(cand):
return cand
i += 1
def mapear_masks_por_base(pasta_masks):
"""
Cria um dicionário {base: caminho_mask} escolhendo .png com prioridade
quando houver múltiplas máscaras para o mesmo base.
"""
mapa = {}
for nome in os.listdir(pasta_masks):
lower = nome.lower()
if not lower.endswith(EXT_MASKS):
continue
base, ext = os.path.splitext(nome)
caminho = os.path.join(pasta_masks, nome)
# Prioriza PNG se houver mais de uma
if base not in mapa:
mapa[base] = caminho
else:
atual_ext = os.path.splitext(mapa[base])[1].lower()
if atual_ext != ".png" and ext.lower() == ".png":
mapa[base] = caminho
return mapa
def copiar_com_pareamento(caminho_img_src, caminho_mask_src, dest_img_dir, dest_mask_dir):
"""
Copia imagem e máscara mantendo mesmo nome-base. Resolve colisões com sufixo _NNN.
Retorna (dst_img_path, dst_mask_path).
"""
garantir_pasta(dest_img_dir)
garantir_pasta(dest_mask_dir)
base_img_src = os.path.splitext(os.path.basename(caminho_img_src))[0]
img_ext = os.path.splitext(caminho_img_src)[1].lower()
mask_ext = os.path.splitext(caminho_mask_src)[1].lower()
# 1) escolhe nome disponível para a imagem
dst_img_path = nome_disponivel(dest_img_dir, base_img_src, img_ext)
new_base = os.path.splitext(os.path.basename(dst_img_path))[0]
# 2) tenta a máscara com o mesmo base
dst_mask_path = os.path.join(dest_mask_dir, new_base + mask_ext)
# 3) se já existir uma máscara com esse nome, gera um base novo e sincroniza a imagem
if os.path.exists(dst_mask_path):
dst_mask_path = nome_disponivel(dest_mask_dir, new_base, mask_ext)
new_base = os.path.splitext(os.path.basename(dst_mask_path))[0]
# sincroniza imagem com o mesmo base
dst_img_path = os.path.join(dest_img_dir, new_base + img_ext)
if os.path.exists(dst_img_path):
dst_img_path = nome_disponivel(dest_img_dir, new_base, img_ext)
# 4) copia
shutil.copy2(caminho_img_src, dst_img_path)
shutil.copy2(caminho_mask_src, dst_mask_path)
print(f"[COPIADO] {os.path.basename(dst_img_path)} | {os.path.basename(dst_mask_path)}")
return dst_img_path, dst_mask_path
def ler_dim(caminho_img):
img = cv2.imread(caminho_img, cv2.IMREAD_UNCHANGED)
if img is None:
raise RuntimeError(f"Erro ao abrir: {caminho_img}")
h, w = img.shape[:2]
return (h, w)
def processar_copias(so_mov=False, manifesto_csv=None, validar_tamanho=True, estrito=False):
"""
- so_mov=False: copia (mantém em new_*). True: move (remove de new_* após copiar).
- validar_tamanho=True: avisa se (w,h) imagem != (w,h) máscara; estrito=True -> pula nesses casos.
"""
garantir_pasta(PASTA_NEW_IMAGES)
garantir_pasta(PASTA_NEW_MASKS)
garantir_pasta(PASTA_FINAL_IMAGES)
garantir_pasta(PASTA_FINAL_MASKS)
mapa_masks = mapear_masks_por_base(PASTA_NEW_MASKS)
registros = []
total, copiados, pulados, sem_mask, erros, dim_mismatch = 0, 0, 0, 0, 0, 0
for nome in os.listdir(PASTA_NEW_IMAGES):
if not nome.lower().endswith(EXT_IMAGENS):
continue
total += 1
caminho_img = os.path.join(PASTA_NEW_IMAGES, nome)
base, _ = os.path.splitext(nome)
caminho_mask = mapa_masks.get(base)
if not caminho_mask:
sem_mask += 1
print(f"[SKIP] Sem máscara correspondente para: {nome}")
continue
try:
if validar_tamanho:
try:
hi, wi = ler_dim(caminho_img)
hm, wm = ler_dim(caminho_mask)
if (hi, wi) != (hm, wm):
dim_mismatch += 1
msg = f"[AVISO] Dimensões diferentes (img {wi}x{hi} vs mask {wm}x{hm}) em base '{base}'"
if estrito:
print(msg + " → pulando.")
pulados += 1
continue
else:
print(msg + " → copiando mesmo assim.")
except Exception as e_dim:
print(f"[AVISO] Falha ao validar dimensões: {e_dim} → copiando mesmo assim.")
dst_img, dst_mask = copiar_com_pareamento(
caminho_img, caminho_mask, PASTA_FINAL_IMAGES, PASTA_FINAL_MASKS
)
copiados += 1
registros.append([caminho_img, caminho_mask, dst_img, dst_mask])
if so_mov:
try:
os.remove(caminho_img)
except Exception:
pass
try:
os.remove(caminho_mask)
except Exception:
pass
except Exception as e:
erros += 1
print(f"[ERRO] {nome}: {e}")
# Manifesto
if manifesto_csv and registros:
with open(manifesto_csv, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["src_image", "src_mask", "dst_image", "dst_mask"])
w.writerows(registros)
print(f"[MANIFESTO] {manifesto_csv} salvo ({len(registros)} entradas).")
print(f"\nResumo: total_imgs={total} | copiados={copiados} | pulados={pulados} | sem_mask={sem_mask} | "
f"dim_mismatch={dim_mismatch} | erros={erros}")
def build_cli():
ap = argparse.ArgumentParser(
description="Copia (ou move) pares IMG+MASK de new_* para images/masks com nomes únicos e pareados."
)
ap.add_argument("--move", action="store_true", help="Move em vez de copiar (remove de new_* após copiar).")
ap.add_argument("--manifest", default=MANIFESTO, help="CSV de manifesto a gerar ('' para não gerar).")
ap.add_argument("--no-validate", action="store_true", help="Não validar dimensões de IMG e MASK.")
ap.add_argument("--strict", action="store_true", help="Se validar dimensões e forem diferentes, pular o par.")
return ap
if __name__ == "__main__":
ap = build_cli()
args = ap.parse_args()
manifesto_csv = None if (args.manifest.strip() == "") else args.manifest
processar_copias(
so_mov=args.move,
manifesto_csv=manifesto_csv,
validar_tamanho=not args.no_validate,
estrito=args.strict
)

View File

@ -1,109 +0,0 @@
import json, os, cv2
from PIL import Image
import albumentations as A
# ⚙️ Configurações
with open("config.json", "r") as f:
config = json.load(f)
MODELO = config["camera"]
# Pastas
dataset_path = os.path.join(MODELO, "dataset", "original", "images")
masks_path = os.path.join(MODELO, "dataset", "original", "masks")
aug_img_out = os.path.join(MODELO, "dataset", "augmented", "images")
aug_msk_out = os.path.join(MODELO, "dataset", "augmented", "masks")
os.makedirs(aug_img_out, exist_ok=True)
os.makedirs(aug_msk_out, exist_ok=True)
# Pipeline de augmentations
train_tf = A.Compose([
A.HorizontalFlip(p=0.5),
# Geométricas (aplicam em imagem e máscara)
A.ShiftScaleRotate(
shift_limit=0.01,
scale_limit=0.10,
rotate_limit=5,
border_mode=cv2.BORDER_REFLECT_101,
#value=(255,255,255),
#mask_value=(255,255,255),
interpolation=cv2.INTER_LINEAR,
p=0.3
),
# Fotométricas (somente imagem)
A.OneOf([
A.RandomBrightnessContrast(0.2, 0.2, p=1),
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=20, val_shift_limit=15, p=1),
A.RandomGamma(gamma_limit=(90,110), p=1),
], p=0.7),
A.OneOf([
A.MotionBlur(blur_limit=3, p=1),
A.GaussianBlur(blur_limit=3, p=1),
], p=0.20),
A.OneOf([
A.GaussNoise(var_limit=(5.0, 15.0), p=1),
A.ImageCompression(quality_lower=50, quality_upper=85, p=1),
], p=0.20),
A.RandomShadow(p=0.1),
A.RandomSunFlare(p=0.1),
A.ChannelShuffle(p=0.05),
A.CoarseDropout(max_holes=6, max_height=16, max_width=16, p=0.1)
# Resize final (img=LINEAR, mask=NEAREST)
#A.Resize(height=H, width=W, interpolation=cv2.INTER_LINEAR, mask_interpolation=cv2.INTER_NEAREST),
], additional_targets={'mask':'mask'})
def load_rgb(path):
# cv2 lê BGR → converte pra RGB (Albumentations usa RGB por padrão)
im = cv2.imread(path, cv2.IMREAD_COLOR)
if im is None:
raise FileNotFoundError(path)
return cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
def save_rgb(path, arr_rgb):
# Salva em RGB mantendo cores corretas
Image.fromarray(arr_rgb).save(path)
def augment_images_and_masks(n_copies=6):
# Faz pareamento por nome base (sem extensão)
imgs = sorted([f for f in os.listdir(dataset_path) if os.path.isfile(os.path.join(dataset_path,f))])
msks = sorted([f for f in os.listdir(masks_path) if os.path.isfile(os.path.join(masks_path,f))])
# Mapeia máscaras por nome-base
msk_map = {os.path.splitext(m)[0]: m for m in msks}
total = 0
for img_file in imgs:
base, ext = os.path.splitext(img_file)
if base not in msk_map:
print(f"[WARN] Máscara não encontrada para {img_file}, pulando.")
continue
img_path = os.path.join(dataset_path, img_file)
msk_path = os.path.join(masks_path, msk_map[base])
# Carrega RGB (máscara como RGB também — mantemos as cores exatas)
img = load_rgb(img_path)
msk = load_rgb(msk_path)
for i in range(n_copies):
# Aplica aug; máscara recebe só geométricas
aug = train_tf(image=img, mask=msk)
img_aug = aug["image"]
msk_aug = aug["mask"]
# Salva
out_img = os.path.join(aug_img_out, f"{base}_aug_{i:02d}{ext}")
out_msk = os.path.join(aug_msk_out, f"{base}_aug_{i:02d}{os.path.splitext(msk_map[base])[1]}")
save_rgb(out_img, img_aug)
save_rgb(out_msk, msk_aug)
total += 1
print(f"Augmentation completed! {total} pares gerados.")
if __name__ == "__main__":
augment_images_and_masks(n_copies=5)

View File

@ -179,7 +179,6 @@ def processar_novas_imagens(fazer_copia_final=True, manifesto_csv=None):
def build_cli():
ap = argparse.ArgumentParser(description="Gera máscaras sólidas para novas imagens e copia para dataset final com dedup.")
ap.add_argument("--modelo", default=MODELO, help="Nome do modelo (base da árvore de pastas).")
ap.add_argument("--no-copy", action="store_true", help="Não copia para as pastas finais (só cria masks em new_masks).")
ap.add_argument("--manifest", default=MANIFESTO, help="Caminho do CSV de manifesto a gerar (ou vazio para não gerar).")
return ap

View File

@ -1,77 +0,0 @@
import json
import os
import cv2
from utils import carregar_labelmap_completo, converter_mask_rgb_para_ids
# ⚙️ Configurações
with open("config.json", "r") as f:
config = json.load(f)
MODELO = config["camera"]
RESOLUCAO = config["resolucao"]
pasta_base = os.path.join(MODELO, "dataset")
fonte_dados = ["original", "augmented"]
labelmap_path = os.path.join(pasta_base, "labelmap.txt")
RESOLUCOES = {
f"{RESOLUCAO[0]}x{RESOLUCAO[1]}": (RESOLUCAO[0], RESOLUCAO[1]),
}
# === Início do processamento ===
cor_para_id, _, _, ignore_rgb = carregar_labelmap_completo(labelmap_path)
ignore_id = ignore_rgb[0]
# Cria pastas de saída
for nome_res, dim in RESOLUCOES.items():
os.makedirs(os.path.join(pasta_base, nome_res, "images"), exist_ok=True)
os.makedirs(os.path.join(pasta_base, nome_res, "masks"), exist_ok=True)
# Processa cada fonte de dados (original + augmented)
for fonte in fonte_dados:
if (not os.path.exists(os.path.join(pasta_base, fonte))):
continue
pasta_rgb = os.path.join(pasta_base, fonte, "images")
pasta_masks = os.path.join(pasta_base, fonte, "masks")
nomes_arquivos = sorted([f for f in os.listdir(pasta_rgb) if f.endswith(".jpg") or f.endswith(".jpeg")])
total = len(nomes_arquivos)
for i, nome in enumerate(nomes_arquivos, 1):
caminho_rgb = os.path.join(pasta_rgb, nome)
caminho_mask = os.path.join(pasta_masks, nome.replace(".jpg", ".png").replace(".jpeg", ".png"))
img_rgb = cv2.imread(caminho_rgb)
if img_rgb is None:
print(f"[!] Erro ao ler imagem {nome}")
continue
# Tenta carregar a máscara RGB (se existir)
if os.path.exists(caminho_mask):
img_mask_rgb = cv2.cvtColor(cv2.imread(caminho_mask), cv2.COLOR_BGR2RGB)
#img_mask_rgb = cv2.cvtColor(img_mask_rgb, cv2.COLOR_BGR2RGB) # ← CORRIGE isso!
if img_mask_rgb is not None:
mask_ids = converter_mask_rgb_para_ids(img_mask_rgb, cor_para_id, ignore_id)
else:
print(f"[!] Erro ao ler máscara {caminho_mask}, ignorando.")
mask_ids = None
else:
mask_ids = None
for nome_res, dim in RESOLUCOES.items():
# Cria nomes únicos baseados na fonte
nome_saida_img = f"{fonte}_{nome}"
nome_saida_mask = nome_saida_img.replace(".jpg", ".png").replace(".jpeg", ".png")
# Redimensiona e salva imagem
img_resized = cv2.resize(img_rgb, dim, interpolation=cv2.INTER_AREA)
path_img_saida = os.path.join(pasta_base, nome_res, "images", nome_saida_img)
cv2.imwrite(path_img_saida, img_resized)
# Redimensiona e salva máscara (se existir)
if mask_ids is not None:
mask_resized = cv2.resize(mask_ids, dim, interpolation=cv2.INTER_NEAREST)
path_mask_saida = os.path.join(pasta_base, nome_res, "masks", nome_saida_mask)
cv2.imwrite(path_mask_saida, mask_resized)
print(f"[{fonte}] [{i}/{total}] Redimensionado: {nome}")
print("\n✅ Concluído com sucesso! Todas as fontes foram processadas.")

View File

@ -0,0 +1,316 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Agrupa imagens e máscaras novas em subpastas por combinação de classes presentes.
Estrutura lida (via config.json -> MODELO):
MODELO/dataset/original/new_images/
MODELO/dataset/original/new_masks/
Saída:
MODELO/dataset/original/group/<grupo>/images
MODELO/dataset/original/group/<grupo>/masks
Onde <grupo> é os nomes das classes presentes unidos por "_", ex:
chao, erva, cana, chao_erva, erva_cana, chao_erva_cana, etc.
Requer: utils.carregar_labelmap_completo(labelmap_path)
O labelmap define mapeamento de cores/ids/nomes das classes.
"""
import os
import cv2
import csv
import json
import shutil
import argparse
import numpy as np
from utils import carregar_labelmap_completo
# ====================== Configurações base ======================
def carregar_config_e_paths():
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config.get("camera")
pasta_base = os.path.join(MODELO, "dataset")
labelmap_path = os.path.join(pasta_base, "labelmap.txt")
# Pastas origem/destino
PASTA_NEW_IMAGES = os.path.join(pasta_base, "original", "images")
PASTA_NEW_MASKS = os.path.join(pasta_base, "original", "masks")
PASTA_FINAL = os.path.join(pasta_base, "original", "group")
return MODELO, pasta_base, labelmap_path, PASTA_NEW_IMAGES, PASTA_NEW_MASKS, PASTA_FINAL
# Extensões aceitas
EXT_IMAGENS = (".jpg", ".jpeg", ".png")
EXT_MASKS = (".png", ".jpg", ".jpeg") # prioridade será .png quando houver
# Manifesto padrão
MANIFESTO_DEFAULT = "manifest.csv"
# ====================== Utilitários ======================
def garantir_pasta(p):
os.makedirs(p, exist_ok=True)
def nome_disponivel(dest_dir, base_name, ext):
"""Gera nome único em dest_dir com sufixos _001, _002, ... se necessário."""
cand = os.path.join(dest_dir, base_name + ext)
if not os.path.exists(cand):
return cand
i = 1
while True:
cand = os.path.join(dest_dir, f"{base_name}_{i:03d}{ext}")
if not os.path.exists(cand):
return cand
i += 1
def mapear_masks_por_base(pasta_masks):
"""Retorna {base: caminho_mask}, priorizando .png se houver múltiplas por base."""
mapa = {}
for nome in os.listdir(pasta_masks):
lower = nome.lower()
if not lower.endswith(EXT_MASKS):
continue
base, ext = os.path.splitext(nome)
cam = os.path.join(pasta_masks, nome)
if base not in mapa:
mapa[base] = cam
else:
atual_ext = os.path.splitext(mapa[base])[1].lower()
if atual_ext != ".png" and ext.lower() == ".png":
mapa[base] = cam
return mapa
def localizar_imagem_por_base(pasta_imgs, base):
"""Retorna caminho da imagem correspondente ao base se existir."""
for ext in EXT_IMAGENS:
p = os.path.join(pasta_imgs, base + ext)
if os.path.isfile(p):
return p
return None
def inferir_ignore_id(ignore_rgb, cor_para_id):
"""
Tenta inferir o ID da classe ignorada a partir do retorno ignore_rgb e do mapa cor->id.
- Se ignore_rgb for [id] ou (id,), retorna esse id.
- Se ignore_rgb parecer uma cor RGB (len==3), usa cor_para_id[(R,G,B)] se existir.
- Caso não consiga, retorna None.
"""
if ignore_rgb is None:
return None
try:
# caso [id]
if isinstance(ignore_rgb, (list, tuple)) and len(ignore_rgb) == 1:
return int(ignore_rgb[0])
# caso [R,G,B]
if isinstance(ignore_rgb, (list, tuple)) and len(ignore_rgb) == 3:
key = tuple(int(v) for v in ignore_rgb)
return cor_para_id.get(key)
except Exception:
pass
# pode já ser um inteiro simples
if isinstance(ignore_rgb, (int, np.integer)):
return int(ignore_rgb)
return None
def extrair_ids_presentes(mask_path, cor_para_id, assume_rgb=True):
"""
Extrai IDs de classes presentes na máscara.
- Se a máscara for 1 canal: retorna valores únicos como IDs diretamente.
- Se for 3 canais: pega cores únicas (BGR), converte para RGB (se assume_rgb=True),
e mapeia usando cor_para_id[(R,G,B)] -> id.
Retorna: set(ids_presentes)
"""
m = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
if m is None:
raise RuntimeError(f"Falha ao abrir máscara: {mask_path}")
# grayscale / paleta indexada
if len(m.shape) == 2 or (len(m.shape) == 3 and m.shape[2] == 1):
vals = np.unique(m).tolist()
return set(int(v) for v in vals)
# 3 canais (OpenCV lê BGR)
h, w, c = m.shape
flat = m.reshape(-1, 3)
uniq_bgr = np.unique(flat, axis=0)
ids = set()
for b, g, r in uniq_bgr:
if assume_rgb:
key = (int(r), int(g), int(b)) # converte para RGB
else:
key = (int(b), int(g), int(r)) # já em BGR no labelmap
id_ = cor_para_id.get(key)
if id_ is not None:
try:
ids.add(int(id_))
except Exception:
pass
return ids
def montar_nome_grupo(ids_presentes, id_para_nome):
"""
Constrói o nome do grupo a partir dos nomes das classes dos IDs presentes.
Preferência de ordenação: chao < erva < cana; demais nomes em ordem alfabética.
"""
nomes = []
for cid in sorted(ids_presentes):
nome = id_para_nome.get(cid, str(cid))
nomes.append(nome)
# aplicar ordenação preferida quando disponíveis
prefer = {"chao": 0, "erva": 1, "cana": 2}
nomes = sorted(nomes, key=lambda n: (prefer.get(n, 99), n))
return "_".join(nomes) if nomes else "sem_classe"
def copiar_ou_mover(img_src, mask_src, dest_img_dir, dest_mask_dir, mover=False):
garantir_pasta(dest_img_dir)
garantir_pasta(dest_mask_dir)
base_img = os.path.splitext(os.path.basename(img_src))[0]
img_ext = os.path.splitext(img_src)[1].lower()
mask_ext = os.path.splitext(mask_src)[1].lower()
dst_img = nome_disponivel(dest_img_dir, base_img, img_ext)
new_base = os.path.splitext(os.path.basename(dst_img))[0]
dst_mask = os.path.join(dest_mask_dir, new_base + mask_ext)
if os.path.exists(dst_mask):
# evita colisão invertendo a ordem do "único" para a máscara
dst_mask = nome_disponivel(dest_mask_dir, new_base, mask_ext)
new_base = os.path.splitext(os.path.basename(dst_mask))[0]
dst_img = os.path.join(dest_img_dir, new_base + img_ext)
if os.path.exists(dst_img):
dst_img = nome_disponivel(dest_img_dir, new_base, img_ext)
if mover:
shutil.move(img_src, dst_img)
shutil.move(mask_src, dst_mask)
else:
shutil.copy2(img_src, dst_img)
shutil.copy2(mask_src, dst_mask)
return dst_img, dst_mask
# ====================== Pipeline principal ======================
def processar(modelo_cli=None, mover=False, manifesto=MANIFESTO_DEFAULT,
validar_dim=True, estrito=False, labelmap_bgr=False):
# carrega config/paths
MODELO, pasta_base, labelmap_path, PASTA_NEW_IMAGES, PASTA_NEW_MASKS, PASTA_FINAL = carregar_config_e_paths()
# carrega labelmap completo
cor_para_id, _colormap_rgb, id_para_nome, ignore_rgb = carregar_labelmap_completo(labelmap_path)
ignore_id = inferir_ignore_id(ignore_rgb, cor_para_id)
# garante pastas
garantir_pasta(PASTA_NEW_IMAGES)
garantir_pasta(PASTA_NEW_MASKS)
garantir_pasta(PASTA_FINAL)
# indexa máscaras
mapa_masks = mapear_masks_por_base(PASTA_NEW_MASKS)
registros = []
totais = {"total_masks":0, "processados":0, "pulados":0, "sem_imagem":0,
"dim_mismatch":0, "erros":0}
por_grupo = {}
for base, mask_path in sorted(mapa_masks.items()):
totais["total_masks"] += 1
img_path = localizar_imagem_por_base(PASTA_NEW_IMAGES, base)
if not img_path:
totais["sem_imagem"] += 1
print(f"[SKIP] Sem imagem correspondente para máscara: {os.path.basename(mask_path)}")
continue
try:
if validar_dim:
try:
img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
msk = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
if img is None or msk is None:
raise RuntimeError("Falha ao abrir img/mask.")
hi, wi = img.shape[:2]
hm, wm = msk.shape[:2]
if (hi, wi) != (hm, wm):
totais["dim_mismatch"] += 1
msg = f"[AVISO] Dimensões diferem (img {wi}x{hi} vs mask {wm}x{hm}) para base '{base}'"
if estrito:
print(msg + " → pulando.")
totais["pulados"] += 1
continue
else:
print(msg + " → copiando mesmo assim.")
except Exception as e_dim:
print(f"[AVISO] Falha ao validar dimensões: {e_dim} → copiando mesmo assim.")
ids_presentes = extrair_ids_presentes(mask_path, cor_para_id, assume_rgb=not labelmap_bgr)
# remove classe ignorada, se conhecida
if ignore_id is not None and ignore_id in ids_presentes:
ids_presentes.discard(ignore_id)
# monta nome do grupo
grupo = montar_nome_grupo(ids_presentes, id_para_nome)
# destinos
dest_base = os.path.join(PASTA_FINAL, grupo)
dest_img_dir = os.path.join(dest_base, "images")
dest_mask_dir = os.path.join(dest_base, "masks")
dst_img, dst_mask = copiar_ou_mover(img_path, mask_path, dest_img_dir, dest_mask_dir, mover=mover)
totais["processados"] += 1
registros.append([img_path, mask_path, dst_img, dst_mask, grupo])
por_grupo[grupo] = por_grupo.get(grupo, 0) + 1
print(f"[OK] {os.path.basename(dst_img)} → grupo: {grupo}")
except Exception as e:
totais["erros"] += 1
print(f"[ERRO] base '{base}': {e}")
# manifesto
if manifesto and registros:
with open(manifesto, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["src_image", "src_mask", "dst_image", "dst_mask", "grupo"])
w.writerows(registros)
print(f"[MANIFESTO] {manifesto} salvo ({len(registros)} entradas).")
# resumo
print("\nResumo: " + " | ".join(f"{k}={v}" for k, v in totais.items()))
if por_grupo:
print("Por grupo:")
for g, c in sorted(por_grupo.items(), key=lambda x: x[0]):
print(f" - {g}: {c}")
# ====================== CLI ======================
def build_cli():
ap = argparse.ArgumentParser(
description="Agrupa pares IMG+MASK por combinação de classes presentes na máscara (a partir de new_*)."
)
ap.add_argument("--move", action="store_true", help="Move (em vez de copiar) para as pastas de grupo.")
ap.add_argument("--manifest", default=MANIFESTO_DEFAULT, help="CSV de manifesto ('' para não gerar).")
ap.add_argument("--modelo", default=None, help="Sobrescreve MODELO do config.json.")
ap.add_argument("--no-validate", action="store_true", help="Não validar dimensões de imagem/máscara.")
ap.add_argument("--strict", action="store_true", help="Se validar e forem diferentes, pular o par.")
ap.add_argument("--labels-bgr", action="store_true",
help="Use se o labelmap estiver em BGR (por padrão assume RGB).")
return ap
if __name__ == "__main__":
args = build_cli().parse_args()
manifest = None if (args.manifest.strip() == "") else args.manifest
processar(
modelo_cli=args.modelo,
mover=args.move,
manifesto=manifest,
validar_dim=not args.no_validate,
estrito=args.strict,
labelmap_bgr=args.labels_bgr
)

View File

@ -1,64 +0,0 @@
import json
import os
import shutil
import random
# ⚙️ Configurações
with open("config.json", "r") as f:
config = json.load(f)
MODELO = config["camera"]
RESOLUCAO = config["resolucao"]
pasta_origem = os.path.join(MODELO, "dataset", f"{RESOLUCAO[0]}x{RESOLUCAO[1]}")
pasta_destino = os.path.join(MODELO, "dataset", "split")
percent_train = 0.7
percent_val = 0.28
percent_test = 0.02
seed = 42
random.seed(seed)
# === Coleta imagens ===
pasta_rgb = os.path.join(pasta_origem, "images")
pasta_masks = os.path.join(pasta_origem, "masks")
arquivos = sorted([f for f in os.listdir(pasta_rgb) if f.endswith(".jpg") or f.endswith(".jpeg")])
# Embaralha
random.shuffle(arquivos)
# Divide
total = len(arquivos)
n_train = int(total * percent_train)
n_val = int(total * percent_val)
arquivos_train = arquivos[:n_train]
arquivos_val = arquivos[n_train:n_train+n_val]
arquivos_test = arquivos[n_train+n_val:]
conjuntos = {
"train": arquivos_train,
"val": arquivos_val,
"test": arquivos_test
}
# === Função auxiliar ===
def copiar(imagens, conjunto):
path_img_dest = os.path.join(pasta_destino, conjunto, "images")
path_mask_dest = os.path.join(pasta_destino, conjunto, "masks")
os.makedirs(path_img_dest, exist_ok=True)
os.makedirs(path_mask_dest, exist_ok=True)
for nome in imagens:
nome_mask = nome.replace(".jpg", ".png").replace(".jpeg", ".png")
if not os.path.exists(os.path.join(pasta_masks, nome_mask)):
continue
shutil.copy2(os.path.join(pasta_rgb, nome), os.path.join(path_img_dest, nome))
shutil.copy2(os.path.join(pasta_masks, nome_mask), os.path.join(path_mask_dest, nome_mask))
# === Executa cópia ===
for conjunto, lista in conjuntos.items():
print(f"[{conjunto}] {len(lista)} arquivos")
copiar(lista, conjunto)
print("\n✅ Dataset dividido com sucesso!")

View File

@ -0,0 +1,259 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Augmenta imagens e máscaras *por grupo*.
Entrada (via config.json -> MODELO):
MODELO/dataset/original/group/<grupo>/images
MODELO/dataset/original/group/<grupo>/masks
Saída:
MODELO/dataset/augmented/group/<grupo>/images
MODELO/dataset/augmented/group/<grupo>/masks
Se "original/group" não existir, faz fallback para:
MODELO/dataset/original/{images,masks}
MODELO/dataset/augmented/{images,masks}
Transf. geométricas (aplicam a img e máscara) e fotométricas (apenas imagem).
Uso:
python _3_augmentation_grouped.py --copies 5
python _3_augmentation_grouped.py --copies 5 --groups chao,chao_erva,cana
"""
import os
import json
import cv2
from PIL import Image
import albumentations as A
import argparse
# ⚙️ Configurações
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config.get("camera", ".")
# Pastas base
DATASET_BASE = os.path.join(MODELO, "dataset")
ORIG_GROUP_ROOT = os.path.join(DATASET_BASE, "original", "group")
AUG_GROUP_ROOT = os.path.join(DATASET_BASE, "augmented", "group")
# Fallback (modo antigo, sem grupos)
ORIG_OLD_IMG = os.path.join(DATASET_BASE, "original", "images")
ORIG_OLD_MSK = os.path.join(DATASET_BASE, "original", "masks")
AUG_OLD_IMG = os.path.join(DATASET_BASE, "augmented", "images")
AUG_OLD_MSK = os.path.join(DATASET_BASE, "augmented", "masks")
# Extensões aceitas
IMG_EXTS = (".jpg", ".jpeg", ".png")
MSK_EXTS = (".png", ".jpg", ".jpeg") # manter prioridade PNG quando possível
def garantir_dir(p):
os.makedirs(p, exist_ok=True)
# Pipeline de augmentations
train_tf = A.Compose([
A.HorizontalFlip(p=0.5),
# Geométricas (aplicam em imagem e máscara)
A.ShiftScaleRotate(
shift_limit=0.01,
scale_limit=0.10,
rotate_limit=5,
border_mode=cv2.BORDER_REFLECT_101,
interpolation=cv2.INTER_LINEAR,
p=0.30
),
# Fotométricas (somente imagem)
A.OneOf([
A.RandomBrightnessContrast(0.2, 0.2, p=1.0),
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=20, val_shift_limit=15, p=1.0),
A.RandomGamma(gamma_limit=(90, 110), p=1.0),
], p=0.70),
A.OneOf([
A.MotionBlur(blur_limit=3, p=1.0),
A.GaussianBlur(blur_limit=3, p=1.0),
], p=0.20),
A.OneOf([
A.GaussNoise(var_limit=(5.0, 15.0), p=1.0),
A.ImageCompression(quality_lower=50, quality_upper=85, p=1.0),
], p=0.20),
A.RandomShadow(p=0.10),
A.RandomSunFlare(p=0.10),
A.ChannelShuffle(p=0.05),
A.CoarseDropout(max_holes=6, max_height=16, max_width=16, p=0.10),
], additional_targets={'mask':'mask'})
def load_rgb(path):
# cv2 lê BGR → converte para RGB
im = cv2.imread(path, cv2.IMREAD_COLOR)
if im is None:
raise FileNotFoundError(path)
return cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
def save_rgb(path, arr_rgb):
Image.fromarray(arr_rgb).save(path)
def list_groups(root):
"""Lista grupos válidos (que contêm subpastas images e masks)."""
if not os.path.isdir(root):
return []
grupos = []
for name in sorted(os.listdir(root)):
gdir = os.path.join(root, name)
if not os.path.isdir(gdir):
continue
if os.path.isdir(os.path.join(gdir, "images")) and os.path.isdir(os.path.join(gdir, "masks")):
grupos.append(name)
return grupos
def map_masks_by_base(msk_dir):
"""Mapeia máscaras por base (prioriza .png)."""
by_base = {}
if not os.path.isdir(msk_dir):
return by_base
for fname in os.listdir(msk_dir):
f_lower = fname.lower()
if not f_lower.endswith(MSK_EXTS):
continue
base, ext = os.path.splitext(fname)
cand = os.path.join(msk_dir, fname)
if base not in by_base:
by_base[base] = cand
else:
# mantém .png se disponível
cur_ext = os.path.splitext(by_base[base])[1].lower()
if cur_ext != ".png" and ext.lower() == ".png":
by_base[base] = cand
return by_base
def ensure_aug_dirs(group_name=None):
"""Cria diretórios de saída para o grupo ou modo antigo."""
if group_name:
img_out = os.path.join(AUG_GROUP_ROOT, group_name, "images")
msk_out = os.path.join(AUG_GROUP_ROOT, group_name, "masks")
else:
img_out = AUG_OLD_IMG
msk_out = AUG_OLD_MSK
garantir_dir(img_out)
garantir_dir(msk_out)
return img_out, msk_out
def augment_pair(img_path, msk_path, img_out_dir, msk_out_dir, copies):
base_img, img_ext = os.path.splitext(os.path.basename(img_path))
base_msk, msk_ext = os.path.splitext(os.path.basename(msk_path))
# padroniza pelo base da imagem
base = base_img
img = load_rgb(img_path)
msk = load_rgb(msk_path)
gen = 0
for i in range(copies):
aug = train_tf(image=img, mask=msk)
img_aug = aug["image"]
msk_aug = aug["mask"]
out_img = os.path.join(img_out_dir, f"{base}_aug_{i:02d}{img_ext}")
out_msk = os.path.join(msk_out_dir, f"{base}_aug_{i:02d}{msk_ext}")
save_rgb(out_img, img_aug)
save_rgb(out_msk, msk_aug)
gen += 1
return gen
def process_group(group_name, copies):
"""Processa um grupo único (images/masks dentro de ORIG_GROUP_ROOT/<group_name>/)."""
img_dir = os.path.join(ORIG_GROUP_ROOT, group_name, "images")
msk_dir = os.path.join(ORIG_GROUP_ROOT, group_name, "masks")
if not (os.path.isdir(img_dir) and os.path.isdir(msk_dir)):
print(f"[WARN] Grupo '{group_name}' inválido (sem images/masks). Pulando.")
return 0
imgs = [f for f in os.listdir(img_dir) if os.path.splitext(f.lower())[1] in IMG_EXTS]
msk_map = map_masks_by_base(msk_dir)
img_out_dir, msk_out_dir = ensure_aug_dirs(group_name)
count = 0
for img_file in sorted(imgs):
base, _ = os.path.splitext(img_file)
msk_file = msk_map.get(base)
if not msk_file:
print(f"[WARN] [{group_name}] Máscara não encontrada para {img_file}, pulando.")
continue
try:
count += augment_pair(
os.path.join(img_dir, img_file),
msk_file,
img_out_dir,
msk_out_dir,
copies=copies
)
except Exception as e:
print(f"[ERRO] [{group_name}] {img_file}: {e}")
print(f"[OK] Grupo '{group_name}'{count} pares gerados.")
return count
def process_legacy(copies):
"""Fallback: modo sem grupos (original/images e original/masks)."""
if not (os.path.isdir(ORIG_OLD_IMG) and os.path.isdir(ORIG_OLD_MSK)):
print("[WARN] Modo legacy não encontrado. Nada a fazer.")
return 0
imgs = [f for f in os.listdir(ORIG_OLD_IMG) if os.path.splitext(f.lower())[1] in IMG_EXTS]
msk_map = map_masks_by_base(ORIG_OLD_MSK)
img_out_dir, msk_out_dir = ensure_aug_dirs(group_name=None)
count = 0
for img_file in sorted(imgs):
base, _ = os.path.splitext(img_file)
msk_file = msk_map.get(base)
if not msk_file:
print(f"[WARN] (legacy) Máscara não encontrada para {img_file}, pulando.")
continue
try:
count += augment_pair(
os.path.join(ORIG_OLD_IMG, img_file),
msk_file,
img_out_dir,
msk_out_dir,
copies=copies
)
except Exception as e:
print(f"[ERRO] (legacy) {img_file}: {e}")
print(f"[OK] Legacy → {count} pares gerados.")
return count
def main(copies=5, groups_csv=None):
total = 0
if os.path.isdir(ORIG_GROUP_ROOT):
grupos = list_groups(ORIG_GROUP_ROOT)
if groups_csv:
# filtra pelos grupos desejados
want = {g.strip() for g in groups_csv.split(",") if g.strip()}
grupos = [g for g in grupos if g in want]
if not grupos:
print("[WARN] Nenhum grupo válido encontrado após filtro.")
if not grupos:
print("[WARN] Nenhum grupo encontrado em original/group. Tentando modo legacy...")
total += process_legacy(copies)
else:
print(f"Grupos encontrados: {', '.join(grupos)}")
for g in grupos:
total += process_group(g, copies)
else:
# sem estrutura de grupos
total += process_legacy(copies)
print(f"\nAugmentation completed! Total: {total} pares gerados.")
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="Augmentação por grupos (images/masks)")
ap.add_argument("--copies", type=int, default=5, help="Número de cópias augmentadas por imagem (default=5).")
ap.add_argument("--groups", type=str, default=None, help="Lista de grupos separados por vírgula (ex: chao,erva_cana).")
args = ap.parse_args()
main(copies=args.copies, groups_csv=args.groups)

View File

@ -1,317 +0,0 @@
import json
import os
import time
import argparse
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from fast_scnn import FastSCNN
from roi_seg_dataset import ROISegDataset
import matplotlib.pyplot as plt
# ⚙️ Configurações
with open("config.json", "r") as f:
config = json.load(f)
MODELO = config["camera"]
MODEL_NAME = config["model_name"]
RESOLUCAO = config["resolucao"]
ROI_INICIO = config["roi_inicio"]
ROI_TAMANHO = config["roi_tamanho"]
MAIN_CLASS_NAME = config["main_class_name"]
save_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME)
dataset_path = os.path.join(MODELO, "dataset")
labelmap_path = os.path.join(dataset_path, "labelmap.txt")
batch_size = 8
num_workers = 4
# ---- Helpers de métricas ----
@torch.no_grad()
def confmat_update(confmat, pred, target, num_classes, ignore_index=None):
# pred, target: (B,H,W)
if ignore_index is not None:
mask = target != ignore_index
target = target[mask]
pred = pred[mask]
k = (target * num_classes + pred).to(torch.int64)
binc = torch.bincount(k, minlength=num_classes**2)
confmat += binc.reshape(num_classes, num_classes)
return confmat
def metrics_from_confmat(confmat, main_class_id=None):
# confmat: CxC
cm = confmat.float()
tp = torch.diag(cm)
fp = cm.sum(0) - tp
fn = cm.sum(1) - tp
denom_iou = tp + fp + fn + 1e-7
iou_per_class = tp / denom_iou
miou = iou_per_class.mean().item()
pix_acc = tp.sum() / (cm.sum() + 1e-7)
main_class_metrics = None
if main_class_id is not None and 0 <= main_class_id < cm.shape[0]:
p = tp[main_class_id] / (tp[main_class_id] + fp[main_class_id] + 1e-7)
r = tp[main_class_id] / (tp[main_class_id] + fn[main_class_id] + 1e-7)
f1 = 2 * p * r / (p + r + 1e-7)
main_class_metrics = {
"precision": p.item(),
"recall": r.item(),
"f1": f1.item(),
"iou": iou_per_class[main_class_id].item(),
}
return {
"miou": miou,
"pixel_acc": pix_acc.item(),
"iou_per_class": iou_per_class.cpu().tolist(),
"main_class": main_class_metrics
}
def train(args):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Device: {device}")
# --- Dataset ---
ds_train = ROISegDataset(
os.path.join(dataset_path, "split", "train"),
save_path, ROI_INICIO, ROI_TAMANHO,
RESOLUCAO[0], RESOLUCAO[1], labelmap_path
)
ds_val = ROISegDataset(
os.path.join(dataset_path, "split", "val"),
save_path, ROI_INICIO, ROI_TAMANHO,
RESOLUCAO[0], RESOLUCAO[1], labelmap_path
)
dl_train = DataLoader(ds_train, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True)
dl_val = DataLoader(ds_val, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)
# Detecta automaticamente o ID da classe ERVA
main_class_id = None
try:
if hasattr(ds_train, "classes") and isinstance(ds_train.classes, dict):
for k, v in ds_train.classes.items():
if isinstance(v, str) and MAIN_CLASS_NAME in v.lower():
main_class_id = k
break
elif isinstance(ds_train.classes, (list, tuple)):
main_class_id = next((i for i, c in enumerate(ds_train.classes) if isinstance(c, str) and MAIN_CLASS_NAME in c.lower()), None)
if main_class_id is not None:
print(f"🌿 Classe PRIMARIA detectada: id={main_class_id}, nome='{ds_train.classes[main_class_id]}'")
else:
print("⚠️ Classe PRIMARIA não encontrada; métricas específicas da classe primaria serão puladas.")
except Exception as e:
print(f"⚠️ Erro ao detectar classe PRIMARIA: {e}")
num_classes = len(ds_train.classes)
# --- Modelo / Otimizador / Schedulers ---
model = FastSCNN(num_classes=num_classes).to(device)
criterion = nn.CrossEntropyLoss(ignore_index=ds_train.ignore_id)
optimizer = optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
# Scheduler inteligente: começa em Cosine, muda pra Plateau se travar
min_lr = getattr(args, "min_lr", 1e-6)
plateau_factor = getattr(args, "plateau_factor", 0.5)
plateau_patience = getattr(args, "plateau_patience", 6) # épocas sem melhora antes de trocar
plateau_cooldown = getattr(args, "plateau_cooldown", 1)
cosine = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs, eta_min=min_lr)
plateau = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode="min", factor=plateau_factor,
patience=plateau_patience, cooldown=plateau_cooldown,
min_lr=min_lr, verbose=True
)
active_sched = "cosine"
scaler = torch.cuda.amp.GradScaler(enabled=args.amp)
start_epoch = 1
best_val_loss = float("inf")
best_main_class_f1 = -1.0
train_loss_history, val_loss_history, lr_history = [], [], []
f1_history, miou_history = [], []
# --- no topo (config) ---
patience_loss = 12 # ligeiramente > plateau_patience + 2
patience_f1 = 6 # deixa o F1 respirar
delta_f1_min = 0.0015 # ignora ruído
grace_after_switch = 4 # épocas de graça após mudar pro Plateau
no_imp_loss = 0
no_imp_f1 = 0
epochs_since_switch = 0
active_sched = "cosine" # como já está
# --- Checkpoint ---
if args.checkpoint and os.path.exists(args.checkpoint):
print(f"🔁 Carregando modelo salvo: {args.checkpoint}")
checkpoint = torch.load(args.checkpoint, map_location=device)
if "model" in checkpoint:
model.load_state_dict(checkpoint["model"])
optimizer.load_state_dict(checkpoint["optimizer"])
scaler.load_state_dict(checkpoint["scaler"])
start_epoch = checkpoint.get("epoch", 1) + 1
best_val_loss = checkpoint.get("best_val_loss", float("inf"))
else:
model.load_state_dict(checkpoint)
# --- Loop de treino ---
for epoch in range(start_epoch, args.epochs + 1):
t0 = time.time()
# ----- Treino -----
model.train()
running_train_loss = 0
for x, y in dl_train:
x, y = x.to(device), y.to(device)
optimizer.zero_grad(set_to_none=True)
with torch.cuda.amp.autocast(enabled=args.amp):
logits = model(x)
loss = criterion(logits, y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
running_train_loss += loss.item() * x.size(0)
avg_train_loss = running_train_loss / len(ds_train)
train_loss_history.append(avg_train_loss)
# ----- Validação + métricas -----
model.eval()
running_val_loss = 0
confmat = torch.zeros((num_classes, num_classes), dtype=torch.int64, device=device)
with torch.no_grad():
for x, y in dl_val:
x, y = x.to(device), y.to(device)
with torch.cuda.amp.autocast(enabled=args.amp):
logits = model(x)
loss = criterion(logits, y)
running_val_loss += loss.item() * x.size(0)
pred = logits.argmax(1)
confmat = confmat_update(confmat, pred, y, num_classes, ignore_index=ds_train.ignore_id)
avg_val_loss = running_val_loss / len(ds_val)
val_loss_history.append(avg_val_loss)
m = metrics_from_confmat(confmat, main_class_id=main_class_id)
miou_history.append(m["miou"])
main_class_f1 = m["main_class"]["f1"] if (m["main_class"] is not None) else None
if main_class_f1 is not None:
f1_history.append(main_class_f1)
cur_lr = optimizer.param_groups[0]["lr"]
lr_history.append(cur_lr)
elapsed = time.time() - t0
msg = (f"[{epoch}/{args.epochs}] "
f"train_loss={avg_train_loss:.4f} "
f"val_loss={avg_val_loss:.4f} "
f"mIoU={m['miou']:.4f} "
f"pixAcc={m['pixel_acc']:.4f} "
f"lr={cur_lr:.2e} "
f"time={elapsed:.1f}s")
if main_class_f1 is not None:
msg += f" | {MAIN_CLASS_NAME}: F1={main_class_f1:.4f} IoU={m['main_class']['iou']:.4f}"
print(msg)
# ----- Tracking de melhora por LOSS -----
improved_loss = avg_val_loss < best_val_loss - 1e-6
if improved_loss:
best_val_loss = avg_val_loss
no_imp_loss = 0
# checkpoint por loss
torch.save(model.state_dict(), os.path.join(save_path, f"{MODEL_NAME}_best.pth"))
torch.save({
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"scaler": scaler.state_dict(),
"epoch": epoch,
"best_val_loss": best_val_loss
}, os.path.join(save_path, f"{MODEL_NAME}_best_checkpoint.pth"))
print("✅ Novo melhor modelo salvo (val_loss).")
else:
no_imp_loss += 1
# ----- Tracking + checkpoint por F1 da classe principal -----
if main_class_f1 is not None:
if main_class_f1 > best_main_class_f1 + delta_f1_min:
best_main_class_f1 = main_class_f1
no_imp_f1 = 0
torch.save(model.state_dict(), os.path.join(save_path, f"{MODEL_NAME}_best_f1_{MAIN_CLASS_NAME}.pth"))
print(f"🌿💾 Checkpoint salvo (melhor F1 da {MAIN_CLASS_NAME}).")
else:
no_imp_f1 += 1
else:
# se não houver F1 (ex: id não definido), ignora o critério
no_imp_f1 = 0
# ----- Scheduler inteligente -----
if active_sched == "cosine":
# se travar por plateau_patience, troca pra ReduceLROnPlateau
if no_imp_loss >= plateau_patience:
active_sched = "plateau"
print("🔁 Mudando scheduler: Cosine → ReduceLROnPlateau (platô detectado).")
# resets ao trocar
no_imp_loss = 0
no_imp_f1 = 0
epochs_since_switch = 0
plateau.step(avg_val_loss) # primeiro passo do plateau
# (opcional) “adiantar” a queda do LR:
for g in optimizer.param_groups:
g['lr'] = max(g['lr'] * plateau_factor, min_lr)
else:
cosine.step()
else:
plateau.step(avg_val_loss)
epochs_since_switch += 1
# ----- Log de estagnação -----
print(f"⏳ Sem melhora — loss: {no_imp_loss}/{patience_loss}, {MAIN_CLASS_NAME}: {no_imp_f1}/{patience_f1}")
# ----- Early stopping bi-critério (com 'graça' após switch) -----
if (no_imp_loss >= patience_loss and
(main_class_f1 is None or no_imp_f1 >= patience_f1) and
(active_sched == "cosine" or epochs_since_switch >= grace_after_switch)):
print("⏹ Early stopping: loss e F1 sem melhora (com período de graça respeitado).")
break
# ----- Plots periódicos -----
if epoch % 5 == 0 or epoch == args.epochs:
x_epochs = list(range(start_epoch, start_epoch + len(train_loss_history)))
# Loss
plt.figure()
plt.plot(x_epochs, train_loss_history, marker="o", label="Train Loss")
plt.plot(x_epochs, val_loss_history, marker="s", label="Val Loss")
plt.xlabel("Época"); plt.ylabel("Loss"); plt.grid(True); plt.legend(); plt.title("Curva de Loss")
plt.tight_layout()
plt.savefig(os.path.join(save_path, "loss_curve.png")); plt.close()
# LR
plt.figure()
plt.plot(x_epochs, lr_history, marker=".")
plt.xlabel("Época"); plt.ylabel("LR"); plt.grid(True); plt.title("Learning Rate")
plt.tight_layout()
plt.savefig(os.path.join(save_path, "lr_curve.png")); plt.close()
# mIoU e F1(erva)
plt.figure()
plt.plot(x_epochs, miou_history, marker="^", label="mIoU")
if len(f1_history) == len(miou_history):
plt.plot(x_epochs, f1_history, marker="*", label=f"F1 {MAIN_CLASS_NAME}")
plt.xlabel("Época"); plt.ylabel("Score"); plt.grid(True); plt.legend(); plt.title(f"mIoU / F1({MAIN_CLASS_NAME})")
plt.tight_layout()
plt.savefig(os.path.join(save_path, "metrics_curve.png")); plt.close()
def parse_args():
ap = argparse.ArgumentParser()
ap.add_argument("--epochs", type=int, default=30)
ap.add_argument("--lr", type=float, default=3e-4)
ap.add_argument("--amp", action="store_true")
ap.add_argument("--checkpoint", type=str, default=None, help="Caminho do modelo .pth para continuar o treinamento")
return ap.parse_args()
if __name__ == "__main__":
args = parse_args()
os.makedirs(save_path, exist_ok=True)
train(args)

View File

@ -0,0 +1,238 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Normaliza/redimensiona imagens e máscaras mantendo a ESTRUTURA POR GRUPO.
Entradas (via config.json -> MODELO, RESOLUCAO):
- MODELO/dataset/original/group/<grupo>/{images,masks}
- MODELO/dataset/augmented/group/<grupo>/{images,masks}
Saídas (por resolução):
- MODELO/dataset/<WxH>/group/<grupo>/{images,masks}
Fallback (modo legado, se não houver "group/"):
- original/{images,masks} e augmented/{images,masks} -> <WxH>/{images,masks}
Conversão de máscara:
- máscara RGB e converte para IDs via utils.converter_mask_rgb_para_ids
- Ignora classe "ignore" conforme labelmap (usa índice 255 como padrão quando necessário)
"""
import argparse
import os
import json
import cv2
from typing import Dict, List, Tuple
from utils import carregar_labelmap_completo, converter_mask_rgb_para_ids
# ⚙️ Configurações
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config["camera"]
RESOLUCAO = tuple(config["resolucao"]) # [W, H] ou [width, height]
pasta_base = os.path.join(MODELO, "dataset")
labelmap_path = os.path.join(pasta_base, "labelmap.txt")
# Dimensões alvo (pode expandir para múltiplas se quiser)
RESOLUCOES = {
f"{RESOLUCAO[0]}x{RESOLUCAO[1]}": (RESOLUCAO[0], RESOLUCAO[1]),
}
# Fontes a processar
FONTES = ["original", "augmented"]
# Extensões aceitas
IMG_EXTS = (".jpg", ".jpeg", ".png")
MSK_EXTS = (".png", ".jpg", ".jpeg") # preferir .png
def infer_ignore_id(ignore_rgb, default_id=255):
"""
Tenta inferir o ID de ignore a partir do valor retornado por carregar_labelmap_completo.
- Se for [id] retorna id
- Se for (R,G,B) retorna default_id (tipicamente 255)
- Se for int, retorna direto
"""
if isinstance(ignore_rgb, (list, tuple)):
if len(ignore_rgb) == 1:
try:
return int(ignore_rgb[0])
except Exception:
return default_id
if len(ignore_rgb) == 3:
return default_id
if isinstance(ignore_rgb, int):
return ignore_rgb
return default_id
def garantir_dir(p):
os.makedirs(p, exist_ok=True)
def list_groups(root) -> List[str]:
"""Lista grupos válidos com subpastas images e masks."""
if not os.path.isdir(root):
return []
grupos = []
for name in sorted(os.listdir(root)):
gdir = os.path.join(root, name)
if not os.path.isdir(gdir):
continue
if os.path.isdir(os.path.join(gdir, "images")) and os.path.isdir(os.path.join(gdir, "masks")):
grupos.append(name)
return grupos
def map_masks_by_base(msk_dir: str) -> Dict[str, str]:
"""Retorna {base: caminho_mask}, priorizando .png quando houver múltiplas por base."""
by_base = {}
if not os.path.isdir(msk_dir):
return by_base
for fname in os.listdir(msk_dir):
f_lower = fname.lower()
if not f_lower.endswith(MSK_EXTS):
continue
base, ext = os.path.splitext(fname)
cand = os.path.join(msk_dir, fname)
if base not in by_base:
by_base[base] = cand
else:
cur_ext = os.path.splitext(by_base[base])[1].lower()
if cur_ext != ".png" and ext.lower() == ".png":
by_base[base] = cand
return by_base
def normalize_pair(caminho_rgb: str, caminho_mask: str, cor_para_id, ignore_id: int,
out_img_dir: str, out_msk_dir: str, dim: Tuple[int,int], prefix: str = ""):
"""Redimensiona e grava a imagem e a máscara (se houver)."""
img_rgb = cv2.imread(caminho_rgb)
if img_rgb is None:
print(f"[!] Erro ao ler imagem: {caminho_rgb}")
return False
# Nome de saída com prefixo para distinguir fonte (ex: original_, augmented_)
nome = os.path.basename(caminho_rgb)
if prefix:
nome_saida_img = f"{prefix}{nome}"
else:
nome_saida_img = nome
nome_saida_msk = nome_saida_img
for ext in (".jpg", ".jpeg", ".png"):
if nome_saida_msk.lower().endswith(ext):
nome_saida_msk = nome_saida_msk[: -len(ext)] + ".png"
break
# Redimensiona imagem
img_resized = cv2.resize(img_rgb, dim, interpolation=cv2.INTER_AREA)
garantir_dir(out_img_dir)
cv2.imwrite(os.path.join(out_img_dir, nome_saida_img), img_resized)
# Processa e redimensiona máscara (se existir)
if caminho_mask and os.path.isfile(caminho_mask):
msk_bgr = cv2.imread(caminho_mask, cv2.IMREAD_COLOR)
if msk_bgr is None:
print(f"[!] Erro ao ler máscara: {caminho_mask}")
else:
msk_rgb = cv2.cvtColor(msk_bgr, cv2.COLOR_BGR2RGB)
mask_ids = converter_mask_rgb_para_ids(msk_rgb, cor_para_id, ignore_id)
mask_resized = cv2.resize(mask_ids, dim, interpolation=cv2.INTER_NEAREST)
garantir_dir(out_msk_dir)
cv2.imwrite(os.path.join(out_msk_dir, nome_saida_msk), mask_resized)
return True
def process_group_root(fonte_root: str, fonte_nome: str, cor_para_id, ignore_id: int, groups_except: str = None):
"""Processa uma raiz do tipo .../<fonte>/group/ agrupando por cada subpasta de grupo."""
total = 0
grupos = list_groups(fonte_root)
if not grupos:
return 0
not_want = {g.strip() for g in groups_except.split(",") if g.strip()}
grupos_desconsiderar = [g for g in grupos if g in not_want]
for nome_res, dim in RESOLUCOES.items():
out_root = os.path.join(pasta_base, nome_res, "group")
for grupo in grupos:
if grupo in grupos_desconsiderar:
print(f"[WARN] Grupo desconsiderado nao sera processado: {grupo}")
continue
in_img_dir = os.path.join(fonte_root, grupo, "images")
in_msk_dir = os.path.join(fonte_root, grupo, "masks")
if not (os.path.isdir(in_img_dir) and os.path.isdir(in_msk_dir)):
print(f"[WARN] Grupo inválido (sem images/masks): {grupo}")
continue
out_img_dir = os.path.join(out_root, grupo, "images")
out_msk_dir = os.path.join(out_root, grupo, "masks")
msk_map = map_masks_by_base(in_msk_dir)
imgs = [f for f in os.listdir(in_img_dir) if os.path.splitext(f.lower())[1] in IMG_EXTS]
n = len(imgs)
for i, fname in enumerate(sorted(imgs), 1):
base, _ = os.path.splitext(fname)
caminho_rgb = os.path.join(in_img_dir, fname)
caminho_mask = msk_map.get(base)
ok = normalize_pair(
caminho_rgb, caminho_mask, cor_para_id, ignore_id,
out_img_dir, out_msk_dir, dim, prefix=f"{fonte_nome}_"
)
if ok:
total += 1
print(f"[{fonte_nome} | {grupo} | {nome_res}] {i}/{n}{fname}")
return total
def process_legacy_root(legacy_img: str, legacy_msk: str, fonte_nome: str, cor_para_id, ignore_id: int):
"""Processa estrutura legado (sem grupos)."""
if not (os.path.isdir(legacy_img) and os.path.isdir(legacy_msk)):
return 0
total = 0
for nome_res, dim in RESOLUCOES.items():
out_img_dir = os.path.join(pasta_base, nome_res, "images")
out_msk_dir = os.path.join(pasta_base, nome_res, "masks")
msk_map = map_masks_by_base(legacy_msk)
imgs = [f for f in os.listdir(legacy_img) if os.path.splitext(f.lower())[1] in IMG_EXTS]
n = len(imgs)
for i, fname in enumerate(sorted(imgs), 1):
base, _ = os.path.splitext(fname)
caminho_rgb = os.path.join(legacy_img, fname)
caminho_mask = msk_map.get(base)
ok = normalize_pair(
caminho_rgb, caminho_mask, cor_para_id, ignore_id,
out_img_dir, out_msk_dir, dim, prefix=f"{fonte_nome}_"
)
if ok:
total += 1
print(f"[{fonte_nome} | legacy | {nome_res}] {i}/{n}{fname}")
return total
def main(args):
# === Labelmap ===
# Espera tupla na ordem: (cor_para_id, colormap_rgb, id_para_nome, ignore_rgb)
cor_para_id, _colormap_rgb, _id_para_nome, ignore_rgb = carregar_labelmap_completo(labelmap_path)
ignore_id = infer_ignore_id(ignore_rgb, default_id=255)
total_geral = 0
# === ORIGINAL ===
orig_group_root = os.path.join(pasta_base, "original", "group")
if os.path.isdir(orig_group_root):
total_geral += process_group_root(orig_group_root, "original", cor_para_id, ignore_id, groups_except=args.groups_except)
else:
legacy_img = os.path.join(pasta_base, "original", "images")
legacy_msk = os.path.join(pasta_base, "original", "masks")
total_geral += process_legacy_root(legacy_img, legacy_msk, "original", cor_para_id, ignore_id)
# === AUGMENTED ===
aug_group_root = os.path.join(pasta_base, "augmented", "group")
if os.path.isdir(aug_group_root):
total_geral += process_group_root(aug_group_root, "augmented", cor_para_id, ignore_id, groups_except=args.groups_except)
else:
legacy_img = os.path.join(pasta_base, "augmented", "images")
legacy_msk = os.path.join(pasta_base, "augmented", "masks")
total_geral += process_legacy_root(legacy_img, legacy_msk, "augmented", cor_para_id, ignore_id)
print(f"\n✅ Concluído! Total normalizados: {total_geral}")
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="Augmentação por grupos (images/masks)")
ap.add_argument("--groups-except", type=str, default="", help="Lista de grupos para nao usar, separados por vírgula (ex: chao,erva_cana).")
args = ap.parse_args()
main(args)

View File

@ -0,0 +1,359 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Split estratificado por GRUPO com **val/test do ORIGINAL** e
garantia de NÃO VAZAMENTO entre splits (mesma família não cruza splits).
de:
MODELO/dataset/<WxH>/group/<grupo>/{images,masks}
Escreve em:
MODELO/dataset/split/<split>/group/<grupo>/{images,masks}
Definições:
- "Família" = todas as variações da MESMA base original:
original_<base>.* e augmented_<base>_aug_XX.*
- Val/Test: **original_<base>** (sem augmented)
- Train: original_<base> **e** todos augmented_<base>_aug_XX
Se não houver prefixos (legado), cai para o comportamento antigo (sem família),
mas ainda evita colocar augmented em val/test se detectar sufixo "_aug_XX".
Uso:
python _7_split_grouped_noleak.py
python _7_split_grouped_noleak.py --train 0.7 --val 0.29 --test 0.01 --seed 42
python _7_split_grouped_noleak.py --min-train 1 --min-val 1 --min-test 0
python _7_split_grouped_noleak.py --modelo OAK-1-Lite-W --resolucao 640x384
"""
import os
import re
import json
import shutil
import random
import argparse
# ⚙️ Configurações
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config.get("camera")
RESOLUCAO = tuple(config.get("resolucao"))
# Pastas
pasta_origem = os.path.join(MODELO, "dataset", f"{RESOLUCAO[0]}x{RESOLUCAO[1]}", "group")
pasta_destino = os.path.join(MODELO, "dataset", "split")
IMG_EXTS = (".jpg", ".jpeg", ".png")
MSK_EXT = ".png" # máscaras normalizadas em PNG (recomendado)
# Regex para identificar famílias
RE_ORIGINAL_PREFIX = re.compile(r'^original_(.+)$', re.IGNORECASE)
RE_AUGMENTED_FAMILY = re.compile(r'^augmented_(.+?)(?:_aug_\d+)?$', re.IGNORECASE)
RE_AUG_SUFFIX = re.compile(r'_aug_\d+$', re.IGNORECASE)
def garantir(p):
os.makedirs(p, exist_ok=True)
def lista_grupos(root):
if not os.path.isdir(root):
return []
out = []
for g in sorted(os.listdir(root)):
gdir = os.path.join(root, g)
if not os.path.isdir(gdir): continue
if os.path.isdir(os.path.join(gdir, "images")) and os.path.isdir(os.path.join(gdir, "masks")):
out.append(g)
return out
def listar_imagens(img_dir):
if not os.path.isdir(img_dir): return []
fs = []
for f in os.listdir(img_dir):
ext = os.path.splitext(f.lower())[1]
if ext in IMG_EXTS:
fs.append(f)
return sorted(fs)
def mask_from_image_name(img_name):
base, _ = os.path.splitext(img_name)
return base + MSK_EXT
def classify_source_and_family(filename_no_ext):
"""
Retorna (source, family_key)
source {"original", "augmented", "unknown"}
family_key = base associada ao original (sem prefixo/sufixos), ex: "foo_001"
"""
m = RE_ORIGINAL_PREFIX.match(filename_no_ext)
if m:
return "original", m.group(1)
m = RE_AUGMENTED_FAMILY.match(filename_no_ext)
if m:
return "augmented", m.group(1)
# legado: tenta deduzir se é augmented por sufixo, e família é o próprio nome sem sufixo
if RE_AUG_SUFFIX.search(filename_no_ext):
fam = RE_AUG_SUFFIX.sub("", filename_no_ext)
return "augmented", fam
return "unknown", filename_no_ext
def build_family_index(img_dir, msk_dir):
"""
Constroi índice de famílias a partir de img_dir/msk_dir.
Retorna: dict family -> {"original": str|None, "augmented": [str], "all": [str]}
(strings são NOMES DE ARQUIVO, não paths completos; assumem que a máscara existe)
"""
familias = {}
imgs = listar_imagens(img_dir)
for img_name in imgs:
base_no_ext, ext = os.path.splitext(img_name)
mask_name = mask_from_image_name(img_name)
if not os.path.exists(os.path.join(msk_dir, mask_name)):
continue # garante pareamento
source, fam = classify_source_and_family(base_no_ext)
d = familias.setdefault(fam, {"original": None, "augmented": [], "all": []})
d["all"].append(img_name)
if source == "original":
d["original"] = img_name
elif source == "augmented":
d["augmented"].append(img_name)
else:
# trata como original desconhecido para não perder dado
if d["original"] is None:
d["original"] = img_name
else:
d["augmented"].append(img_name)
return familias
def allocate_counts(n, p_train, p_val, p_test, min_train, min_val, min_test):
n_train = int(round(n * p_train))
n_val = int(round(n * p_val))
n_test = n - n_train - n_val
if n_test < 0:
excesso = -n_test
take_train = min(excesso, max(0, n_train))
n_train -= take_train
excesso -= take_train
if excesso > 0:
take_val = min(excesso, max(0, n_val))
n_val -= take_val
excesso -= take_val
n_test = 0
min_sum = min_train + min_val + min_test
if n >= min_sum:
n_train = max(n_train, min_train)
n_val = max(n_val, min_val)
n_test = max(n_test, min_test)
total = n_train + n_val + n_test
while total > n:
if n_test > min_test:
n_test -= 1
elif n_val > min_val:
n_val -= 1
elif n_train > min_train:
n_train -= 1
else:
break
total = n_train + n_val + n_test
while total < n:
if n_train - min_train <= n_val - min_val:
n_train += 1
else:
n_val += 1
total = n_train + n_val + n_test
else:
n_train = min(n, max(1, min_train))
resto = n - n_train
n_val = max(0, min(resto, min_val))
n_test = max(0, resto - n_val)
# ajuste final
diff = n - (n_train + n_val + n_test)
if diff != 0:
if diff > 0:
# adiciona em train, depois val
take = min(diff, n - n_train)
n_train += take
diff -= take
if diff > 0:
n_val += diff
else:
diff = -diff
# tira de test, depois val
take = min(diff, n_test)
n_test -= take
diff -= take
if diff > 0:
n_val -= diff
return n_train, n_val, n_test
def copiar(nomes, src_img_dir, src_msk_dir, dst_img_dir, dst_msk_dir):
garantir(dst_img_dir); garantir(dst_msk_dir)
moved = 0
for nome in nomes:
mask_name = mask_from_image_name(nome)
src_img = os.path.join(src_img_dir, nome)
src_msk = os.path.join(src_msk_dir, mask_name)
if not (os.path.exists(src_img) and os.path.exists(src_msk)):
continue
shutil.copy2(src_img, os.path.join(dst_img_dir, nome))
shutil.copy2(src_msk, os.path.join(dst_msk_dir, mask_name))
moved += 1
return moved
def split_group(group_name, p_train, p_val, p_test, seed, mins, caps_map=None):
src_img_dir = os.path.join(pasta_origem, group_name, "images")
src_msk_dir = os.path.join(pasta_origem, group_name, "masks")
familias = build_family_index(src_img_dir, src_msk_dir)
# apenas famílias que têm ORIGINAL para participar de val/test
familias_originais = [fam for fam, d in familias.items() if d["original"] is not None]
total_familias = len(familias_originais)
if total_familias == 0:
print(f"[{group_name}] 0 famílias com original, pulando.")
return {"train": 0, "val": 0, "test": 0, "familias": 0}
rng = random.Random(seed)
rng.shuffle(familias_originais)
n_tr, n_va, n_te = allocate_counts(
total_familias, p_train, p_val, p_test,
mins["train"], mins["val"], mins["test"]
)
fam_train = set(familias_originais[:n_tr])
fam_val = set(familias_originais[n_tr:n_tr+n_va])
fam_test = set(familias_originais[n_tr+n_va: n_tr+n_va+n_te])
# --- CAP por grupo (apenas no TRAIN) ---
if caps_map and group_name in caps_map:
cap = caps_map[group_name]
if len(fam_train) > cap:
fam_list = list(fam_train)
rng.shuffle(fam_list) # usa o rng já criado com seed
kept = set(fam_list[:cap])
dropped = set(fam_list[cap:])
fam_train = kept
print(f"[{group_name}] cap-train-families={cap} → mantidas {len(kept)} famílias, descartadas {len(dropped)} do TRAIN")
# listas de nomes por split (imagens)
nomes_train, nomes_val, nomes_test = [], [], []
for fam, d in familias.items():
if fam in fam_train:
# train recebe original + todos augmented
if d["original"]:
nomes_train.append(d["original"])
if d["augmented"]:
nomes_train.extend(d["augmented"])
elif fam in fam_val:
# val recebe somente original
if d["original"]:
nomes_val.append(d["original"])
elif fam in fam_test:
# test recebe somente original
if d["original"]:
nomes_test.append(d["original"])
else:
# famílias sem original (não devem cair aqui) ficam fora
pass
# dest dirs
dest_train_img = os.path.join(pasta_destino, "train", "group", group_name, "images")
dest_train_msk = os.path.join(pasta_destino, "train", "group", group_name, "masks")
dest_val_img = os.path.join(pasta_destino, "val", "group", group_name, "images")
dest_val_msk = os.path.join(pasta_destino, "val", "group", group_name, "masks")
dest_test_img = os.path.join(pasta_destino, "test", "group", group_name, "images")
dest_test_msk = os.path.join(pasta_destino, "test", "group", group_name, "masks")
m_train = copiar(nomes_train, src_img_dir, src_msk_dir, dest_train_img, dest_train_msk)
m_val = copiar(nomes_val, src_img_dir, src_msk_dir, dest_val_img, dest_val_msk)
m_test = copiar(nomes_test, src_img_dir, src_msk_dir, dest_test_img, dest_test_msk)
print(f"[{group_name}] famílias={total_familias} → train(imgs)={m_train}, val(imgs)={m_val}, test(imgs)={m_test}")
return {"train": m_train, "val": m_val, "test": m_test, "familias": total_familias}
def main():
ap = argparse.ArgumentParser(description="Split estratificado por grupo SEM vazamento (val/test só original).")
ap.add_argument("--train", type=float, default=0.70, help="Proporção de treino (default=0.70).")
ap.add_argument("--val", type=float, default=0.29, help="Proporção de validação (default=0.29).")
ap.add_argument("--test", type=float, default=0.01, help="Proporção de teste (default=0.01).")
ap.add_argument("--seed", type=int, default=42, help="Seed do embaralhamento (default=42).")
ap.add_argument("--min-train", type=int, default=1, help="Mínimo de FAMÍLIAS por grupo em train (default=1).")
ap.add_argument("--min-val", type=int, default=1, help="Mínimo de FAMÍLIAS por grupo em val (default=1).")
ap.add_argument("--min-test", type=int, default=0, help="Mínimo de FAMÍLIAS por grupo em test (default=0).")
ap.add_argument("--modelo", type=str, default=None, help="Sobrescreve MODELO do config.json.")
ap.add_argument("--resolucao", type=str, default=None, help="Sobrescreve resolução no formato WxH (ex: 640x480).")
ap.add_argument("--cap-train-families", type=str, default="", help="Mapa 'grupo:cap,...' p/ limitar número de FAMÍLIAS no TRAIN. Ex.: 'chao:350'")
args = ap.parse_args()
modelo = args.modelo or MODELO
if args.resolucao:
try:
w, h = args.resolucao.lower().split("x")
resolucao = (int(w), int(h))
except Exception:
resolucao = RESOLUCAO
else:
resolucao = RESOLUCAO
def parse_cap_map(s):
caps = {}
if not s: return caps
for item in s.split(","):
k,v = item.strip().split(":")
caps[k.strip()] = int(v)
return caps
caps_map = parse_cap_map(args.cap_train_families)
global pasta_origem, pasta_destino
pasta_origem = os.path.join(modelo, "dataset", f"{resolucao[0]}x{resolucao[1]}", "group")
pasta_destino = os.path.join(modelo, "dataset", "split")
soma = args.train + args.val + args.test
if soma <= 0: raise ValueError("Soma de proporções deve ser > 0.")
p_train = args.train / soma
p_val = args.val / soma
p_test = args.test / soma
mins = {"train": max(0, args.min_train), "val": max(0, args.min_val), "test": max(0, args.min_test)}
garantir(pasta_destino)
grupos = lista_grupos(pasta_origem)
if not grupos:
print(f"[WARN] Nenhum grupo encontrado em: {pasta_origem}")
return
random.seed(args.seed)
total_global = {"train":0, "val":0, "test":0, "familias":0}
print(f"Grupos: {', '.join(grupos)}")
print(f"Proporções normalizadas: train={p_train:.3f}, val={p_val:.3f}, test={p_test:.3f}")
print(f"Mínimos por grupo (famílias): train={mins['train']} val={mins['val']} test={mins['test']}")
for g in grupos:
res = split_group(g, p_train, p_val, p_test, args.seed, mins, caps_map=caps_map)
for k in total_global.keys():
total_global[k] += res.get(k, 0)
print("\nResumo global (imagens copiadas):")
print(f" train: {total_global['train']}")
print(f" val: {total_global['val']}")
print(f" test: {total_global['test']}")
print(f" famílias (total): {total_global['familias']}")
print("\n✅ Split sem vazamento concluído!")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,699 @@
# 👉 force backend headless (sem Tk)
import itertools
import os
import random
os.environ["MPLBACKEND"] = "Agg" # extra-garantia
import matplotlib
matplotlib.use("Agg") # tem que vir antes do pyplot!
import matplotlib.pyplot as plt
plt.ioff() # desliga modo interativo
import json
import time
import argparse
from PIL import Image
import numpy as np
import torch
import torch.nn.functional as F
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from fast_scnn import FastSCNN
from roi_seg_dataset import ROISegDataset
from utils import carregar_labelmap_completo, compute_roi_indices
# ⚙️ Configurações
with open("config.json", "r") as f:
config = json.load(f)
MODELO = config["camera"]
MODEL_NAME = config["model_name"]
RESOLUCAO = config["resolucao"]
ROI_INICIO = config["roi_inicio"]
ROI_TAMANHO = config["roi_tamanho"]
MAIN_CLASS_NAME = config["main_class_name"]
save_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME)
dataset_path = os.path.join(MODELO, "dataset")
labelmap_path = os.path.join(dataset_path, "labelmap.txt")
batch_size = 32
num_workers = 4
# ---- Helpers de métricas ----
def _infer_ignore_id(ignore_rgb, default_id=255):
import numpy as _np
if isinstance(ignore_rgb, (list, tuple)):
if len(ignore_rgb) == 1 and isinstance(ignore_rgb[0], (int, _np.integer)):
return int(ignore_rgb[0])
if len(ignore_rgb) == 3:
return default_id
if isinstance(ignore_rgb, (int, _np.integer)):
return int(ignore_rgb)
return default_id
def compute_class_weights_from_split(
train_split_root: str,
labelmap_path: str,
roi_inicio: float,
roi_tamanho: float,
*,
alpha: float = 1.2, # ↑ reforça classes raras (1.0 a 1.5 costuma ir bem)
w_min: float = 0.3, # piso geral
w_max: float = 4.0, # teto geral
floor_bg: float = 0.4, # piso específico pro 'chao' / background
max_samples_per_group: int = 300 # amostras p/ grupo (acelera o cálculo)
):
"""
Calcula pesos dinâmicos (median frequency balancing ^ alpha) SOBRE A ROI das máscaras do split/train.
- Normaliza por ROI (mesma fatia usada no dataset).
- Clampa pesos entre [w_min, w_max] e mantém 'chao' no mínimo floor_bg.
Retorna: tensor de pesos (indexado por ID de classe).
"""
# Labelmap
_, _, classes, ignore_rgb = carregar_labelmap_completo(labelmap_path)
ignore_id = _infer_ignore_id(ignore_rgb, default_id=255)
class_ids = sorted(classes.keys()) # ex.: [0,1,2]
counts = np.zeros(len(class_ids), dtype=np.int64)
# Onde estão as máscaras do train?
group_root = os.path.join(train_split_root, "group")
group_dirs = []
if os.path.isdir(group_root):
for g in sorted(os.listdir(group_root)):
mdir = os.path.join(group_root, g, "masks")
if os.path.isdir(mdir):
group_dirs.append(mdir)
else:
# fallback legado
mdir = os.path.join(train_split_root, "masks")
if os.path.isdir(mdir):
group_dirs.append(mdir)
# Conta pixels por classe DENTRO DA ROI
for mdir in group_dirs:
n = 0
for p in os.listdir(mdir):
if not p.lower().endswith(".png"):
continue
m = np.array(Image.open(os.path.join(mdir, p)).convert("L"))
H = m.shape[0]
y_fim, y_ini = compute_roi_indices(H, roi_inicio, roi_tamanho)
roi = m[y_fim:y_ini, :]
# ignora 'ignore' e só soma classes válidas
for i, cid in enumerate(class_ids):
if cid == ignore_id:
continue
counts[i] += int((roi == cid).sum())
n += 1
if n >= max_samples_per_group:
break
total = int(counts.sum())
if total == 0:
# fallback seguro
print("⚠️ compute_class_weights_from_split: não encontrei pixels válidos; usando pesos [1,1,...].")
return None, None
freqs = counts / total # frequência por classe
nonzero = freqs[freqs > 0]
base = np.median(nonzero) if nonzero.size > 0 else 1.0
weights_arr = np.zeros_like(freqs, dtype=np.float32)
for i, f in enumerate(freqs):
if f <= 0:
w = w_max
else:
# median-freq ^ alpha
w = (base / f) ** alpha
w = float(np.clip(w, w_min, w_max))
weights_arr[i] = w
# Piso do 'chao' (ou 'background'), se existir
for i, cid in enumerate(class_ids):
name = str(classes[cid]).lower()
if ("chao" in name) or ("background" in name):
weights_arr[i] = max(weights_arr[i], floor_bg)
# Constrói vetor na indexação por ID de classe (0..max_id)
max_cid = max(class_ids)
weights_full = np.ones(max_cid + 1, dtype=np.float32)
for i, cid in enumerate(class_ids):
weights_full[cid] = weights_arr[i]
# Log bonitinho
pretty = {int(cid): (str(classes[cid]), float(weights_full[cid]), float(freqs[i]))
for i, cid in enumerate(class_ids)}
return weights_full, pretty
@torch.no_grad()
def confmat_update(confmat, pred, target, num_classes, ignore_index=None):
# pred, target: (B,H,W)
if ignore_index is not None:
mask = target != ignore_index
target = target[mask]
pred = pred[mask]
k = (target * num_classes + pred).to(torch.int64)
binc = torch.bincount(k, minlength=num_classes**2)
confmat += binc.reshape(num_classes, num_classes)
return confmat
def metrics_from_confmat(confmat, main_class_id=None):
# confmat: CxC
cm = confmat.float()
tp = torch.diag(cm)
fp = cm.sum(0) - tp
fn = cm.sum(1) - tp
denom_iou = tp + fp + fn + 1e-7
iou_per_class = tp / denom_iou
miou = iou_per_class.mean().item()
pix_acc = tp.sum() / (cm.sum() + 1e-7)
main_class_metrics = None
if main_class_id is not None and 0 <= main_class_id < cm.shape[0]:
p = tp[main_class_id] / (tp[main_class_id] + fp[main_class_id] + 1e-7)
r = tp[main_class_id] / (tp[main_class_id] + fn[main_class_id] + 1e-7)
f1 = 2 * p * r / (p + r + 1e-7)
main_class_metrics = {
"precision": p.item(),
"recall": r.item(),
"f1": f1.item(),
"iou": iou_per_class[main_class_id].item(),
}
return {
"miou": miou,
"pixel_acc": pix_acc.item(),
"iou_per_class": iou_per_class.cpu().tolist(),
"main_class": main_class_metrics
}
def _id_by_name(d, name):
name = name.lower()
for cid, nm in d.items():
if isinstance(nm, str) and name in nm.lower():
return cid
return None
def _get_mask_roi_from_ds(ds, i, roi_inicio, roi_tamanho):
"""Tenta obter o caminho da máscara; se não der, usa ds[i]."""
mask_path = None
if hasattr(ds, "mask_paths"):
mask_path = ds.mask_paths[i]
elif hasattr(ds, "items"):
item = ds.items[i]
if isinstance(item, dict) and "mask" in item:
mask_path = item["mask"]
if mask_path is not None:
m = np.array(Image.open(mask_path).convert("L"))
else:
# fallback: carrega a máscara já processada pelo dataset
_, y = ds[i] # y: Tensor [H,W]
m = y.cpu().numpy()
H = m.shape[0]
y_fim, y_ini = compute_roi_indices(H, roi_inicio, roi_tamanho)
return m[y_fim:y_ini, :]
def compute_presence_indices(ds, class_ids, roi_inicio, roi_tamanho):
"""
presence: {cid: [idxs que CONTÊM essa classe na ROI]}
others: [idxs que NÃO contêm NENHUMA das 'class_ids' na ROI]
"""
presence = {cid: [] for cid in class_ids}
others = []
for i in range(len(ds)):
roi = _get_mask_roi_from_ds(ds, i, roi_inicio, roi_tamanho)
found_any = False
for cid in class_ids:
if (roi == cid).any():
presence[cid].append(i)
found_any = True
if not found_any:
others.append(i)
return presence, others
class EnsureClassesBatchSampler(torch.utils.data.Sampler):
"""
Garante >=1 amostra de CADA classe em 'required_classes' por batch.
Preenche o resto com índices do pool (others + todo o conjunto).
Use com DataLoader(..., batch_sampler= sampler) sem passar batch_size/sampler/shuffle.
"""
def __init__(self, presence, total_indices, batch_size, required_classes, seed=42):
self.presence = presence # dict cid -> list[idx]
self.required = [c for c in required_classes if len(presence.get(c, [])) > 0]
self.batch_size = batch_size
# iteradores cíclicos (com reposição) por classe requerida
self.iters = {
c: itertools.cycle(self.presence[c]) for c in self.required
}
# pool de preenchimento: todos os índices (mistura bem)
self.rest_iter = itertools.cycle(list(total_indices))
self.rng = random.Random(seed)
# tamanho lógico: nº de batches por época
self._length = max(1, int(np.ceil(len(total_indices) / float(batch_size))))
def __iter__(self):
for _ in range(self._length):
batch = []
# 1 de cada classe requerida (se existir)
for c in self.required:
batch.append(next(self.iters[c]))
# completa o batch
while len(batch) < self.batch_size:
batch.append(next(self.rest_iter))
self.rng.shuffle(batch)
yield batch
def __len__(self):
return self._length
def train(args):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Device: {device}")
# --- Dataset ---
ds_train = ROISegDataset(
os.path.join(dataset_path, "split", "train"),
save_path, ROI_INICIO, ROI_TAMANHO,
RESOLUCAO[0], RESOLUCAO[1], labelmap_path
)
ds_val = ROISegDataset(
os.path.join(dataset_path, "split", "val"),
save_path, ROI_INICIO, ROI_TAMANHO,
RESOLUCAO[0], RESOLUCAO[1], labelmap_path
)
# --- Mapa id->nome (usa o do dataset; se não houver, carrega do labelmap) ---
if hasattr(ds_train, "classes") and isinstance(ds_train.classes, dict) and len(ds_train.classes) > 0:
id_to_name = {int(k): str(v) for k, v in ds_train.classes.items()}
else:
# fallback seguro ao arquivo de labelmap
_, _, id_to_name, _ = carregar_labelmap_completo(labelmap_path)
id_to_name = {int(k): str(v) for k, v in id_to_name.items()}
# name->id (case-insensitive)
name_to_id = {v.strip().lower(): k for k, v in id_to_name.items()}
# --- Lista dinâmica de classes a garantir por batch ---
raw = getattr(args, "ensure_per_batch", "")
req_names = [s.strip().lower() for s in raw.split(",") if s.strip()]
req_ids = []
for nm in req_names:
cid = name_to_id.get(nm)
if cid is None:
# tenta correspondência parcial (p.ex. "erva" casa com "Erva", "weed_erva", etc.)
matches = [k for k, v in id_to_name.items() if nm in v.lower()]
if len(matches) == 1:
cid = matches[0]
elif len(matches) > 1:
print(f"⚠️ '--ensure-per-batch {nm}': ambíguo entre {[id_to_name[m] for m in matches]}; ignorando este nome.")
cid = None
else:
print(f"⚠️ '--ensure-per-batch {nm}': classe não encontrada nas classes {list(name_to_id.keys())}.")
if cid is not None and cid not in req_ids:
req_ids.append(cid)
if len(req_ids) > 0:
presence, _ = compute_presence_indices(
ds_train, class_ids=req_ids, roi_inicio=ROI_INICIO, roi_tamanho=ROI_TAMANHO
)
total_indices = range(len(ds_train))
batch_sampler = EnsureClassesBatchSampler(
presence=presence,
total_indices=total_indices,
batch_size=batch_size,
required_classes=req_ids,
seed=getattr(args, "seed", 42)
)
# ⚠️ Use 'batch_sampler' (NÃO passe batch_size/sampler/shuffle)
dl_train = DataLoader(ds_train, batch_sampler=batch_sampler,
num_workers=num_workers, pin_memory=True)
else:
dl_train = DataLoader(ds_train, batch_size=batch_size, shuffle=True,
num_workers=num_workers, pin_memory=True)
# Validação normal
dl_val = DataLoader(ds_val, batch_size=batch_size, shuffle=False,
num_workers=num_workers, pin_memory=True)
# Detecta automaticamente o ID da classe ERVA
main_class_id = None
try:
if hasattr(ds_train, "classes") and isinstance(ds_train.classes, dict):
for k, v in ds_train.classes.items():
if isinstance(v, str) and MAIN_CLASS_NAME in v.lower():
main_class_id = k
break
elif isinstance(ds_train.classes, (list, tuple)):
main_class_id = next((i for i, c in enumerate(ds_train.classes) if isinstance(c, str) and MAIN_CLASS_NAME in c.lower()), None)
if main_class_id is not None:
print(f"🌿 Classe PRIMARIA detectada: id={main_class_id}, nome='{ds_train.classes[main_class_id]}'")
else:
print("⚠️ Classe PRIMARIA não encontrada; métricas específicas da classe primaria serão puladas.")
except Exception as e:
print(f"⚠️ Erro ao detectar classe PRIMARIA: {e}")
num_classes = len(ds_train.classes)
# --- Modelo / Otimizador / Schedulers ---
model = FastSCNN(num_classes=num_classes).to(device)
# --- Pesos dinâmicos por classe (sobre a ROI do split/train) ---
train_split_root = os.path.join(dataset_path, "split", "train")
weights_np, debug_info = compute_class_weights_from_split(
train_split_root=train_split_root,
labelmap_path=labelmap_path,
roi_inicio=ROI_INICIO,
roi_tamanho=ROI_TAMANHO,
alpha=getattr(args, "cw_alpha", 1.05),
w_min=getattr(args, "cw_min", 0.3),
w_max=getattr(args, "cw_max", 2.0),
floor_bg=getattr(args, "cw_bgfloor", 0.6),
max_samples_per_group=getattr(args, "cw_max_per_group", 300)
)
if weights_np is None or not getattr(args, "use_wights", False):
# fallback seguro
weights_t = None
print("⚠️ Pesos dinâmicos indisponíveis; usando CrossEntropy sem pesos.")
else:
import pprint
pprint.pprint({"class_weights": debug_info})
weights_t = torch.tensor(weights_np, device=device)
# --- Warm-up de pesos por época ---
ones = torch.ones_like(weights_t) if weights_t is not None else None
def make_epoch_weights(epoch, *, cw_warmup=8):
"""
Interpola: w_epoch = (1 - λ) * 1 + λ * weights_t
λ cresce de 01 nas primeiras `cw_warmup` épocas.
Retorna (w_epoch, dice_w_normalized) ou (None, None) se sem pesos.
"""
if weights_t is None:
return None, None
# lê da CLI ou usa default
cw_warmup = getattr(args, "cw_warmup", cw_warmup)
# λ linear 0→1 (pode trocar por cosseno, ver abaixo)
cw_lambda = min(1.0, max(0.0, (epoch - 1) / max(1, cw_warmup)))
w_epoch = (1.0 - cw_lambda) * ones + cw_lambda * weights_t
# normaliza para o Dice (evita distorção)
dice_w = (w_epoch / w_epoch.mean()).detach()
return w_epoch, dice_w
optimizer = optim.AdamW(model.parameters(), lr=getattr(args, "lr", 3e-4), weight_decay=1e-4)
def dice_loss(logits, target, ignore_index=255, class_weights=None, eps=1e-6):
"""
logits: [N, C, H, W] (antes do softmax)
target: [N, H, W] com IDs de classe; 'ignore_index' será mascarado
class_weights: tensora opcional [C] (ex.: pesos da CE, normalizados)
"""
N, C, H, W = logits.shape
# Probabilidades por classe
pred = F.softmax(logits, dim=1) # [N,C,H,W]
# Máscara de válidos (ignora 255)
valid = (target != ignore_index) # [N,H,W]
target_clamped = torch.clamp(target, 0, C-1) # evita index out of range
# One-hot do target (com válidos)
one_hot = torch.zeros((N, C, H, W),
device=logits.device,
dtype=pred.dtype)
one_hot.scatter_(1, target_clamped.unsqueeze(1), 1.0) # [N,1,H,W] -> [N,C,H,W]
# Aplica máscara de válidos
valid = valid.unsqueeze(1) # [N,1,H,W]
pred = pred * valid
one_hot = one_hot * valid
# Dice por classe (agrega em N,H,W)
inter = (pred * one_hot).sum(dim=(0, 2, 3)) # [C]
pred_sum = pred.sum(dim=(0, 2, 3)) # [C]
tgt_sum = one_hot.sum(dim=(0, 2, 3)) # [C]
dice = (2 * inter + eps) / (pred_sum + tgt_sum + eps) # [C]
if class_weights is not None:
# opcional: ponderar o Dice com pesos (normalize antes!)
# garante shape [C]
w = torch.ones(C, device=logits.device, dtype=pred.dtype)
w[:class_weights.numel()] = class_weights
loss = 1.0 - (w * dice).sum() / (w.sum() + eps)
else:
loss = 1.0 - dice.mean()
return loss
# Scheduler inteligente: começa em Cosine, muda pra Plateau se travar
min_lr = getattr(args, "min_lr", 1e-6)
plateau_factor = getattr(args, "plateau_factor", 0.5)
plateau_patience = getattr(args, "plateau_patience", 6) # épocas sem melhora antes de trocar
plateau_cooldown = getattr(args, "plateau_cooldown", 1)
cosine = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs, eta_min=min_lr)
plateau = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode="min", factor=plateau_factor,
patience=plateau_patience, cooldown=plateau_cooldown,
min_lr=min_lr, verbose=True
)
active_sched = "cosine"
scaler = torch.amp.GradScaler('cuda', enabled=args.amp)
start_epoch = 1
best_val_loss = float("inf")
best_main_class_f1 = -1.0
train_loss_history, val_loss_history, lr_history = [], [], []
f1_history, miou_history = [], []
# --- no topo (config) ---
patience_loss = 12 # ligeiramente > plateau_patience + 2
patience_f1 = 6 # deixa o F1 respirar
delta_f1_min = 0.0015 # ignora ruído
grace_after_switch = 4 # épocas de graça após mudar pro Plateau
no_imp_loss = 0
no_imp_f1 = 0
epochs_since_switch = 0
active_sched = "cosine" # como já está
# --- Checkpoint ---
if args.checkpoint and os.path.exists(args.checkpoint):
print(f"🔁 Carregando modelo salvo: {args.checkpoint}")
checkpoint = torch.load(args.checkpoint, map_location=device)
if "model" in checkpoint:
model.load_state_dict(checkpoint["model"])
optimizer.load_state_dict(checkpoint["optimizer"])
scaler.load_state_dict(checkpoint["scaler"])
start_epoch = checkpoint.get("epoch", 1) + 1
best_val_loss = checkpoint.get("best_val_loss", float("inf"))
else:
model.load_state_dict(checkpoint)
# --- Loop de treino ---
for epoch in range(start_epoch, args.epochs + 1):
t0 = time.time()
# pesos deste epoch
w_epoch, dice_w = make_epoch_weights(epoch)
dice_mix = min(0.3, (epoch-1)/10 * 0.3) # 0.0→0.3 nas 10 primeiras
ce_mix = 1.0 - dice_mix
if w_epoch is not None:
erva_id = _id_by_name(ds_train.classes, "erva")
cana_id = _id_by_name(ds_train.classes, "cana")
chao_id = _id_by_name(ds_train.classes, "chao")
# calcula lambda atual (igual ao make_epoch_weights)
cw_warmup = getattr(args, "cw_warmup", 8)
cw_lambda = min(1.0, max(0.0, (epoch - 1) / max(1, cw_warmup)))
if cw_lambda < 1.0:
if erva_id is not None:
w_epoch[erva_id] = torch.clamp(w_epoch[erva_id], min=1.2)
if cana_id is not None:
w_epoch[cana_id] = torch.clamp(w_epoch[cana_id], max=2.2)
if chao_id is not None:
w_epoch[chao_id] = torch.clamp(w_epoch[chao_id], min=0.5)
# re-normaliza o peso do Dice após clamps
dice_w = (w_epoch / w_epoch.mean()).detach()
ce = nn.CrossEntropyLoss(ignore_index=ds_train.ignore_id, weight=w_epoch)
else:
ce = nn.CrossEntropyLoss(ignore_index=ds_train.ignore_id)
# ----- Treino -----
model.train()
running_train_loss = 0
for x, y in dl_train:
x, y = x.to(device), y.to(device)
optimizer.zero_grad(set_to_none=True)
with torch.amp.autocast('cuda', enabled=args.amp):
logits = model(x)
dloss = dice_loss(logits, y, ignore_index=ds_train.ignore_id, class_weights=None)
loss = ce_mix * ce(logits, y) + dice_mix * dloss
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
running_train_loss += loss.item() * x.size(0)
avg_train_loss = running_train_loss / len(ds_train)
train_loss_history.append(avg_train_loss)
# ----- Validação + métricas -----
model.eval()
running_val_loss = 0
confmat = torch.zeros((num_classes, num_classes), dtype=torch.int64, device=device)
with torch.no_grad():
for x, y in dl_val:
x, y = x.to(device), y.to(device)
with torch.amp.autocast('cuda', enabled=args.amp):
logits = model(x)
dloss = dice_loss(logits, y, ignore_index=ds_train.ignore_id, class_weights=None)
loss = ce_mix * ce(logits, y) + dice_mix * dloss
running_val_loss += loss.item() * x.size(0)
pred = logits.argmax(1)
confmat = confmat_update(confmat, pred, y, num_classes, ignore_index=ds_train.ignore_id)
avg_val_loss = running_val_loss / len(ds_val)
val_loss_history.append(avg_val_loss)
m = metrics_from_confmat(confmat, main_class_id=main_class_id)
miou_history.append(m["miou"])
main_class_f1 = m["main_class"]["f1"] if (m["main_class"] is not None) else None
if main_class_f1 is not None:
f1_history.append(main_class_f1)
cur_lr = optimizer.param_groups[0]["lr"]
lr_history.append(cur_lr)
elapsed = time.time() - t0
msg = (f"[{epoch}/{args.epochs}] "
f"train_loss={avg_train_loss:.4f} "
f"val_loss={avg_val_loss:.4f} "
f"mIoU={m['miou']:.4f} "
f"pixAcc={m['pixel_acc']:.4f} "
f"lr={cur_lr:.2e} "
f"time={elapsed:.1f}s")
if main_class_f1 is not None:
msg += f" | {MAIN_CLASS_NAME}: F1={main_class_f1:.4f} IoU={m['main_class']['iou']:.4f}"
print(msg)
# ----- Tracking de melhora por LOSS -----
improved_loss = avg_val_loss < best_val_loss - 1e-6
if improved_loss:
best_val_loss = avg_val_loss
no_imp_loss = 0
# checkpoint por loss
torch.save(model.state_dict(), os.path.join(save_path, f"{MODEL_NAME}_best.pth"))
torch.save({
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"scaler": scaler.state_dict(),
"epoch": epoch,
"best_val_loss": best_val_loss
}, os.path.join(save_path, f"{MODEL_NAME}_best_checkpoint.pth"))
print("✅ Novo melhor modelo salvo (val_loss).")
else:
no_imp_loss += 1
# ----- Tracking + checkpoint por F1 da classe principal -----
if main_class_f1 is not None:
if main_class_f1 > best_main_class_f1 + delta_f1_min:
best_main_class_f1 = main_class_f1
no_imp_f1 = 0
torch.save(model.state_dict(), os.path.join(save_path, f"{MODEL_NAME}_best_f1_{MAIN_CLASS_NAME}.pth"))
print(f"🌿💾 Checkpoint salvo (melhor F1 da {MAIN_CLASS_NAME}).")
else:
no_imp_f1 += 1
else:
# se não houver F1 (ex: id não definido), ignora o critério
no_imp_f1 = 0
# ----- Scheduler inteligente -----
if active_sched == "cosine":
# se travar por plateau_patience, troca pra ReduceLROnPlateau
if no_imp_loss >= plateau_patience:
active_sched = "plateau"
print("🔁 Mudando scheduler: Cosine → ReduceLROnPlateau (platô detectado).")
# resets ao trocar
no_imp_loss = 0
no_imp_f1 = 0
epochs_since_switch = 0
plateau.step(avg_val_loss) # primeiro passo do plateau
# (opcional) “adiantar” a queda do LR:
for g in optimizer.param_groups:
g['lr'] = max(g['lr'] * plateau_factor, min_lr)
else:
cosine.step()
else:
plateau.step(avg_val_loss)
epochs_since_switch += 1
# ----- Log de estagnação -----
if no_imp_loss > 0 or no_imp_f1 > 0:
print(f"⏳ Sem melhora — loss: {no_imp_loss}/{patience_loss}, {MAIN_CLASS_NAME}: {no_imp_f1}/{patience_f1}")
# ----- Early stopping bi-critério (com 'graça' após switch) -----
if (no_imp_loss >= patience_loss and
(main_class_f1 is None or no_imp_f1 >= patience_f1) and
(active_sched == "cosine" or epochs_since_switch >= grace_after_switch)):
print("⏹ Early stopping: loss e F1 sem melhora (com período de graça respeitado).")
break
# ----- Plots periódicos -----
if epoch % 5 == 0 or epoch == args.epochs:
x_epochs = list(range(start_epoch, start_epoch + len(train_loss_history)))
# Loss
plt.figure()
plt.plot(x_epochs, train_loss_history, marker="o", label="Train Loss")
plt.plot(x_epochs, val_loss_history, marker="s", label="Val Loss")
plt.xlabel("Época"); plt.ylabel("Loss"); plt.grid(True); plt.legend(); plt.title("Curva de Loss")
plt.tight_layout()
plt.savefig(os.path.join(save_path, "loss_curve.png")); plt.close()
# LR
plt.figure()
plt.plot(x_epochs, lr_history, marker=".")
plt.xlabel("Época"); plt.ylabel("LR"); plt.grid(True); plt.title("Learning Rate")
plt.tight_layout()
plt.savefig(os.path.join(save_path, "lr_curve.png")); plt.close()
# mIoU e F1(erva)
plt.figure()
plt.plot(x_epochs, miou_history, marker="^", label="mIoU")
if len(f1_history) == len(miou_history):
plt.plot(x_epochs, f1_history, marker="*", label=f"F1 {MAIN_CLASS_NAME}")
plt.xlabel("Época"); plt.ylabel("Score"); plt.grid(True); plt.legend(); plt.title(f"mIoU / F1({MAIN_CLASS_NAME})")
plt.tight_layout()
plt.savefig(os.path.join(save_path, "metrics_curve.png")); plt.close()
def parse_args():
ap = argparse.ArgumentParser()
ap.add_argument("--epochs", type=int, default=30)
ap.add_argument("--lr", type=float, default=3e-4)
ap.add_argument("--amp", action="store_true")
ap.add_argument("--checkpoint", type=str, default=None, help="Caminho do modelo .pth para continuar o treinamento")
ap.add_argument("--ensure-per-batch", type=str, default="", help="Lista de classes por nome para garantir >=1 por batch. Ex.: 'erva,cana'")
ap.add_argument("--use-weights", action="store_true")
return ap.parse_args()
if __name__ == "__main__":
args = parse_args()
os.makedirs(save_path, exist_ok=True)
train(args)

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Teste/visualização do FastSCNN com suporte a estrutura AGRUPADA.
Estrutura esperada (nova):
MODELO/dataset/split/test/group/<grupo>/{images,masks}
Fallback (legado, se não houver 'group/'):
MODELO/dataset/split/test/{images,masks}
Também suporta o modo câmera (--camera) igual ao original.
"""
import json
import os
import time
@ -9,7 +22,10 @@ import numpy as np
import depthai as dai
from PIL import Image
from fast_scnn import FastSCNN
from utils import carregar_labelmap_completo, compute_roi_indices, converter_mask_ids_para_rgb, desenhar_legenda_horizontal, desenhar_legenda_vertical, resize_keep_width
from utils import (
carregar_labelmap_completo, compute_roi_indices, converter_mask_ids_para_rgb,
desenhar_legenda_horizontal, desenhar_legenda_vertical, resize_keep_width
)
# ⚙️ Configurações
with open("config.json", "r") as f:
@ -19,32 +35,122 @@ MODEL_NAME = config["model_name"]
RESOLUCAO = config["resolucao"]
ROI_INICIO = config["roi_inicio"]
ROI_TAMANHO = config["roi_tamanho"]
MAIN_CLASS_NAME = config["main_class_name"]
use_main_class = config["use_main_class"]
model_to_use = config["model_to_use"]
dataset_path = os.path.join(MODELO, "dataset")
split_folder = "test"
split_folder = "val"
labelmap_path = os.path.join(dataset_path, "labelmap.txt")
model_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, f"{MODEL_NAME}_best{f'_f1_{MAIN_CLASS_NAME}' if use_main_class else ''}.pth")
model_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME)
model_name = ""
if model_to_use == "geral":
model_name = f"{MODEL_NAME}_best.pth"
elif model_to_use == "main_class":
MAIN_CLASS_NAME = config["main_class_name"]
model_name = f"{MODEL_NAME}_best_f1_{MAIN_CLASS_NAME}.pth"
elif model_to_use == "es":
ES_CLASSES_NAME = config["es_classes"]
model_name = f"{MODEL_NAME}_best_es_{ES_CLASSES_NAME}.pth"
else:
model_name = f"{MODEL_NAME}_best.pth"
#model_name = model_name.replace(".pth", "_bkp.pth")
IMG_EXTS = (".jpg", ".jpeg", ".png")
MSK_EXTS = (".png", ".jpg", ".jpeg") # preferir .png quando houver
def infer_ignore_id(ignore_rgb, default_id=255):
"""Tenta inferir ID de ignore a partir do labelmap."""
if isinstance(ignore_rgb, (list, tuple)):
if len(ignore_rgb) == 1 and isinstance(ignore_rgb[0], (int, np.integer)):
return int(ignore_rgb[0])
if len(ignore_rgb) == 3:
return default_id
if isinstance(ignore_rgb, (int, np.integer)):
return int(ignore_rgb)
return default_id
def list_groups(group_root):
if not os.path.isdir(group_root):
return []
out = []
for g in sorted(os.listdir(group_root)):
gdir = os.path.join(group_root, g)
if os.path.isdir(os.path.join(gdir, "images")) and os.path.isdir(os.path.join(gdir, "masks")):
out.append(g)
return out
def mask_for_base(msk_dir, base):
"""Encontra máscara correspondente, priorizando .png."""
best = None
for ext in MSK_EXTS:
cand = os.path.join(msk_dir, base + ext)
if os.path.isfile(cand):
if best is None:
best = cand
if os.path.splitext(cand)[1].lower() == ".png":
return cand
return best
def collect_pairs_grouped(test_root, want_groups=None):
"""Coleta pares img/mask de test_root com estrutura 'group/'."""
group_root = os.path.join(test_root, "group")
if not os.path.isdir(group_root):
return [], []
groups = list_groups(group_root)
if want_groups:
filt = {g.strip() for g in want_groups.split(",") if g.strip()}
groups = [g for g in groups if g in filt]
imgs, msks, groups_idx = [], [], []
for g in groups:
img_dir = os.path.join(group_root, g, "images")
msk_dir = os.path.join(group_root, g, "masks")
for p in sorted(glob.glob(os.path.join(img_dir, "*"))):
base, ext = os.path.splitext(os.path.basename(p))
if ext.lower() not in IMG_EXTS:
continue
m = mask_for_base(msk_dir, base)
if m:
imgs.append(p)
msks.append(m)
groups_idx.append(g)
return imgs, msks, groups_idx
def collect_pairs_legacy(test_root):
"""Coleta pares img/mask sem 'group/'."""
img_dir = os.path.join(test_root, "images")
msk_dir = os.path.join(test_root, "masks")
imgs = []
msks = []
groups_idx = []
for p in sorted(glob.glob(os.path.join(img_dir, "*"))):
base, ext = os.path.splitext(os.path.basename(p))
if ext.lower() not in IMG_EXTS:
continue
m = mask_for_base(msk_dir, base)
if m:
imgs.append(p)
msks.append(m)
groups_idx.append("legacy")
return imgs, msks, groups_idx
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--camera", action="store_true", help="Usar câmera em vez de imagens")
parser.add_argument("--groups", type=str, default=None, help="Filtrar grupos (ex: chao,erva_cana)")
args = parser.parse_args()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
_, colormap_rgb, classes, ignore_rgb = carregar_labelmap_completo(labelmap_path)
ignore_id = ignore_rgb[0]
ignore_id = infer_ignore_id(ignore_rgb, default_id=255)
model = FastSCNN(num_classes=len(classes))
model.load_state_dict(torch.load(model_path, map_location=device))
model.load_state_dict(torch.load(os.path.join(model_path, model_name), map_location=device))
model.to(device).eval()
mean = torch.tensor([0.485, 0.456, 0.406]).reshape(3, 1, 1).to(device)
std = torch.tensor([0.229, 0.224, 0.225]).reshape(3, 1, 1).to(device)
if args.camera:
# --- Criar pipeline da OAK-1 Lite W ---
# === Modo câmera (inalterado) ===
pipeline = dai.Pipeline()
cam_rgb = pipeline.createColorCamera()
cam_rgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
@ -57,7 +163,6 @@ def main():
xout_rgb.setStreamName("rgb")
cam_rgb.video.link(xout_rgb.input)
# --- Conectar dispositivo ---
with dai.Device(pipeline) as oak_device:
rgb_queue = oak_device.getOutputQueue(name="rgb", maxSize=4, blocking=False)
@ -83,17 +188,14 @@ def main():
pred_rgb = converter_mask_ids_para_rgb(pred_ids, colormap_rgb, ignore_id)
pred_rgb_resized = cv2.resize(pred_rgb, (roi.shape[1], roi.shape[0]), interpolation=cv2.INTER_NEAREST)
# Overlay
overlay = frame.copy()
overlay[y_fim:y_inicio, 0:W] = cv2.addWeighted(overlay[y_fim:y_inicio, 0:W], 0.4, pred_rgb_resized, 0.6, 0)
# FPS
now = time.time()
fps = 1.0 / (now - prev_time)
prev_time = now
cv2.putText(overlay, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
# === LEGENDA SOBRE A IMAGEM DA CÂMERA ===
legenda = desenhar_legenda_vertical(colormap_rgb, classes)
legenda_resized = cv2.resize(legenda, (150, 30 * len(colormap_rgb)), interpolation=cv2.INTER_AREA)
@ -110,15 +212,20 @@ def main():
cv2.destroyAllWindows()
else:
# Modo normal com imagens da pasta
image_paths = sorted(glob.glob(os.path.join(dataset_path, "split", split_folder, "images", "*")))
mask_paths = sorted(glob.glob(os.path.join(dataset_path, "split", split_folder, "masks", "*")))
assert len(image_paths) == len(mask_paths) and len(image_paths) > 0
# === Modo imagens (agrupado + fallback) ===
test_root = os.path.join(dataset_path, "split", split_folder)
image_paths, mask_paths, groups_idx = collect_pairs_grouped(test_root, want_groups=args.groups)
if not image_paths:
image_paths, mask_paths, groups_idx = collect_pairs_legacy(test_root)
assert len(image_paths) == len(mask_paths) and len(image_paths) > 0, "Nenhuma imagem/máscara encontrada no split de teste."
idx = 0
while True:
img_path = image_paths[idx]
mask_path = mask_paths[idx]
grupo = groups_idx[idx] if groups_idx else "?"
img_rgb = np.array(Image.open(img_path).convert("RGB"))
mask_gt = np.array(Image.open(mask_path).convert("L"))
@ -144,11 +251,14 @@ def main():
mask_gt_rgb = converter_mask_ids_para_rgb(mask_resized, colormap_rgb, ignore_id)
resultado = np.concatenate([img_resized, mask_gt_rgb, pred_rgb], axis=1)
# Adiciona legenda abaixo
legenda = desenhar_legenda_horizontal(colormap_rgb, classes)
legenda_resized = cv2.resize(legenda, (resultado.shape[1], legenda.shape[0]), interpolation=cv2.INTER_NEAREST)
resultado_completo = np.concatenate([resultado, legenda_resized], axis=0)
# Rotula o grupo na imagem
cv2.putText(resultado_completo, f"grupo: {grupo}", (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
cv2.imshow("Original | GroundTruth | Predito", cv2.cvtColor(resultado_completo, cv2.COLOR_RGB2BGR))
key = cv2.waitKey(0) & 0xFF
@ -162,4 +272,4 @@ def main():
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
main()

Some files were not shown because too many files have changed in this diff Show More