ajustes gerais pre teste
This commit is contained in:
parent
35202470c3
commit
9611f564b5
Binary file not shown.
Binary file not shown.
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,9 +267,9 @@ namespace AgroBase.Services
|
|||
|
||||
return new List<byte[]>()
|
||||
{
|
||||
_fix.ToArray(),
|
||||
_lat.ToArray(),
|
||||
_long.ToArray(),
|
||||
_fix.ToArray(),
|
||||
_gerais.ToArray(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -29,5 +29,5 @@
|
|||
"top_topics_and_observing_domains": [ ]
|
||||
} ],
|
||||
"hex_encoded_hmac_key": "40F346D3248C3AFDF2BEE1FE496DBD32F7CED6E5AE98B881ABC421AA7E7B5642",
|
||||
"next_scheduled_calculation_time": "13402424980889204"
|
||||
"next_scheduled_calculation_time": "13402424980889277"
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}}}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
139.0.3405.125
|
||||
140.0.3485.54
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -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":""}}}
|
||||
Binary file not shown.
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
|
|
@ -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
Binary file not shown.
Binary file not shown.
|
|
@ -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():
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -19,6 +19,7 @@ namespace AgroMonitor
|
|||
|
||||
RedisService.Iniciar();
|
||||
RedisService.LimparDadosIniciais();
|
||||
APIService.IniciarRotinas();
|
||||
|
||||
FuncoesGlobais.DefinirEventosPicBtn(picAjustes, "Erro ao definir ajustes!", async () =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "";
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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)
|
||||
|
|
@ -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})")
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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!")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
- Lê 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)
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Split estratificado por GRUPO com **val/test só do ORIGINAL** e
|
||||
garantia de NÃO VAZAMENTO entre splits (mesma família não cruza splits).
|
||||
|
||||
Lê 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: só **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()
|
||||
|
|
@ -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 0→1 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
|
|
@ -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
Loading…
Reference in New Issue