Pure pursuit agora é filtrado em saindo rua, MPC simula os próximos candidatos de acordo com a posição real do motor, libera a pressão da linha após concluir operação, controle do agitador através da base, sincronização dos parametros da operação na IHM através da base
This commit is contained in:
parent
5fb8caa88c
commit
bd1526cdaf
|
|
@ -23,6 +23,9 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
private readonly ucParametrosMapa _ucMapa;
|
||||
|
||||
private OperacaoModel operacaoParametrizando;
|
||||
private OperacaoModel _operacaoFonteCarregada;
|
||||
private OperacaoParametrosModel _parametrosFonteCarregados;
|
||||
private OperacaoParametrosControleModel _controleFonteCarregado;
|
||||
|
||||
public ucOperacaoParametros()
|
||||
{
|
||||
|
|
@ -135,6 +138,11 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
private void DefinirOperacaoParametrizando(OperacaoModel operacao, bool recarregarMapa = true)
|
||||
{
|
||||
operacaoParametrizando = operacao;
|
||||
|
||||
_operacaoFonteCarregada = operacao;
|
||||
_parametrosFonteCarregados = operacao?.Parametros;
|
||||
_controleFonteCarregado = operacao?.Parametros?.Controle;
|
||||
|
||||
CarregarDadosGerais();
|
||||
_ucMovimento.DefinirOperacao(operacaoParametrizando);
|
||||
_ucDirecional.DefinirOperacao(operacaoParametrizando);
|
||||
|
|
@ -298,12 +306,46 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
}
|
||||
|
||||
|
||||
public void AtualizarParametrosTela(OperacaoModel op)
|
||||
public void AtualizarParametrosTela(
|
||||
OperacaoModel op,
|
||||
bool recarregarMapa = true)
|
||||
{
|
||||
DefinirOperacaoParametrizando(op, true);
|
||||
DefinirOperacaoParametrizando(op, recarregarMapa);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recarrega os controles somente quando a operação ou alguma das
|
||||
/// instâncias autoritativas de parâmetros foi substituída. Isso cobre
|
||||
/// a parametrização parcial recebida da base sem apagar uma edição
|
||||
/// local que o operador esteja fazendo sobre a mesma instância.
|
||||
/// </summary>
|
||||
public void SincronizarComOperacaoAtualSeNecessario()
|
||||
{
|
||||
OperacaoModel atual = Variaveis.OperacaoEmAndamento;
|
||||
|
||||
if (atual == null)
|
||||
return;
|
||||
|
||||
bool fonteAlterada =
|
||||
!ReferenceEquals(_operacaoFonteCarregada, atual) ||
|
||||
!ReferenceEquals(_parametrosFonteCarregados, atual.Parametros) ||
|
||||
!ReferenceEquals(
|
||||
_controleFonteCarregado,
|
||||
atual.Parametros?.Controle
|
||||
);
|
||||
|
||||
if (!fonteAlterada)
|
||||
return;
|
||||
|
||||
// Em atualização parcial a operação e o mapa continuam os mesmos.
|
||||
// Recarregar o WebView aqui é desnecessário e pode perder estado
|
||||
// visual; os demais controles devem refletir a nova autoridade.
|
||||
DefinirOperacaoParametrizando(atual, recarregarMapa: false);
|
||||
}
|
||||
|
||||
public void AtualizarDadosTela()
|
||||
{
|
||||
SincronizarComOperacaoAtualSeNecessario();
|
||||
operacaoParametrizando?.Mapa?.AtualizarInteratividadeMapaAsync(parametrizacao: true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,9 +73,40 @@ namespace AgroBase.Forms.IHM
|
|||
}
|
||||
}
|
||||
|
||||
public void AtualizarParametrosTela(OperacaoModel op)
|
||||
public void AtualizarParametrosTela(
|
||||
OperacaoModel op,
|
||||
bool recarregarMapa = true)
|
||||
{
|
||||
_ucParametros.AtualizarParametrosTela(op);
|
||||
if (IsDisposed || Disposing)
|
||||
return;
|
||||
|
||||
// Se o formulário ainda não criou o handle, não force sua criação
|
||||
// na thread MQTT. Ao abrir a aba, a sincronização por referência
|
||||
// abaixo carregará a operação atual.
|
||||
if (!IsHandleCreated)
|
||||
return;
|
||||
|
||||
if (InvokeRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
BeginInvoke(new Action<OperacaoModel, bool>(
|
||||
AtualizarParametrosTela
|
||||
), op, recarregarMapa);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// O formulário pode estar encerrando entre a verificação
|
||||
// e o BeginInvoke. A próxima abertura sincroniza a fonte.
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_ucParametros.AtualizarParametrosTela(op, recarregarMapa);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -124,6 +155,7 @@ namespace AgroBase.Forms.IHM
|
|||
break;
|
||||
case AbaOperacaoIHM.Parametros:
|
||||
lblContextoAba.Text = "Parâmetros da operação atual e configuração local";
|
||||
_ucParametros.SincronizarComOperacaoAtualSeNecessario();
|
||||
ExibirConteudo(_ucParametros);
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ namespace AgroBase.Forms
|
|||
|
||||
private double velocidadeCarroMs;
|
||||
private double _ultimoAnguloControleAplicado;
|
||||
private double _ultimoAnguloControleAlvoAtrasado;
|
||||
|
||||
// Modelo simples do atuador direcional: a fila existente representa o
|
||||
// atraso de transporte; este limite representa o tempo físico de giro.
|
||||
// Mantido no código para não exigir mudança no Designer nesta versão.
|
||||
private double VelocidadeAngularDirecionalGrausSegundo => VariaveisEquipamento.VelocidadeAngularDirecionalGrausSegundo;
|
||||
private GPSModel posicaoAtual = new GPSModel();
|
||||
|
||||
// Mantida para compatibilidade e para os dados apresentados na tela.
|
||||
|
|
@ -398,11 +404,26 @@ namespace AgroBase.Forms
|
|||
|
||||
if (atualizarAnguloControle)
|
||||
{
|
||||
anguloControle = CalcularAnguloControleAtrasado(
|
||||
_ultimoAnguloControleAlvoAtrasado = CalcularAnguloControleAtrasado(
|
||||
Volatile.Read(ref _delayComandoPassos)
|
||||
);
|
||||
}
|
||||
|
||||
// Diferente da versão anterior, o fim da fila vira um alvo e
|
||||
// não um salto instantâneo. A pose usa somente o ângulo físico.
|
||||
anguloControle = AtualizarAnguloControleFisico(
|
||||
_ultimoAnguloControleAlvoAtrasado,
|
||||
tempoPassadoSegundos
|
||||
);
|
||||
|
||||
if (operacao.DispMvd?.Dados != null)
|
||||
{
|
||||
operacao.DispMvd.Dados.AnguloDirecionalSimuladoCanonico =
|
||||
anguloControle;
|
||||
operacao.DispMvd.Dados.MomentoAnguloDirecionalSimulado =
|
||||
DateTime.UtcNow;
|
||||
}
|
||||
|
||||
GPSModel gps = GPSService.GetSnapshot();
|
||||
|
||||
double anguloAtual =
|
||||
|
|
@ -1614,7 +1635,10 @@ namespace AgroBase.Forms
|
|||
);
|
||||
|
||||
lock (_estadoSimulacaoLock)
|
||||
{
|
||||
_ultimoAnguloControleAlvoAtrasado = angulo;
|
||||
_ultimoAnguloControleAplicado = angulo;
|
||||
}
|
||||
|
||||
txtAnguloControle.Text = angulo.ToString("0.00");
|
||||
}
|
||||
|
|
@ -1638,9 +1662,26 @@ namespace AgroBase.Forms
|
|||
historicoAngulosControle.Dequeue();
|
||||
}
|
||||
|
||||
_ultimoAnguloControleAplicado =
|
||||
historicoAngulosControle.Peek();
|
||||
return historicoAngulosControle.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
private double AtualizarAnguloControleFisico(
|
||||
double anguloAlvo,
|
||||
double dtSegundos)
|
||||
{
|
||||
if (!ValorFinito(anguloAlvo))
|
||||
anguloAlvo = 0.0;
|
||||
|
||||
double passoMaximo =
|
||||
VelocidadeAngularDirecionalGrausSegundo *
|
||||
Math.Max(0.0, dtSegundos);
|
||||
|
||||
lock (_estadoSimulacaoLock)
|
||||
{
|
||||
double erro = anguloAlvo - _ultimoAnguloControleAplicado;
|
||||
double passo = Math.Max(-passoMaximo, Math.Min(passoMaximo, erro));
|
||||
_ultimoAnguloControleAplicado += passo;
|
||||
return _ultimoAnguloControleAplicado;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,11 @@ namespace AgroBase.Models.Modules
|
|||
private DateTime _ultimaDescobertaEnviada = DateTime.MinValue;
|
||||
private readonly object _estadoLock = new object();
|
||||
private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1);
|
||||
private readonly SemaphoreSlim _alivioPressaoGate = new SemaphoreSlim(1, 1);
|
||||
private readonly object _calibragemCtsLock = new object();
|
||||
private CancellationTokenSource _calibragemCts;
|
||||
private bool _sincronizacaoSeguraPendente;
|
||||
private volatile bool _alivioPressaoLinhaEmAndamento;
|
||||
|
||||
[JsonIgnore]
|
||||
internal object EstadoSyncRoot => _estadoLock;
|
||||
|
|
@ -1817,6 +1819,15 @@ namespace AgroBase.Models.Modules
|
|||
if (op?.DispAtu?.Dados?.Conectado != true)
|
||||
return;
|
||||
|
||||
/*
|
||||
* Durante o alívio de pressão, a sequência explícita abaixo é a
|
||||
* única dona das saídas do atuador. Sem esta barreira, o ciclo de
|
||||
* segurança da finalização fecharia os bicos antes de completar
|
||||
* a janela de despressurização.
|
||||
*/
|
||||
if (_alivioPressaoLinhaEmAndamento)
|
||||
return;
|
||||
|
||||
bool operacaoConcluida = op.Sensoriamento?.Operacao?.StatusOperacaoAtual == StatusOperacao.Concluido;
|
||||
|
||||
bool pulverizadorAutomatico = op.Parametros?.Controle?.PulverizadorAutomatico ?? false;
|
||||
|
|
@ -1898,6 +1909,10 @@ namespace AgroBase.Models.Modules
|
|||
foreach (var bomba in BombasPressurizadoras.Where(x => x != null && x.Inicializado))
|
||||
{
|
||||
bool ehBombaPrincipal = bomba.ID == BombaPressurizadora?.ID;
|
||||
bool ehAgitador = bomba.ID == AgitadorDeCalda?.ID;
|
||||
bool agitadorManual =
|
||||
ehAgitador &&
|
||||
op.Parametros?.Controle?.AtuAgitadorModo == ModoAgitadorCalda.Manual;
|
||||
|
||||
bool comandoSolicitado = bomba.Comandar && bomba.ComandoAtuar;
|
||||
|
||||
|
|
@ -1914,6 +1929,21 @@ namespace AgroBase.Models.Modules
|
|||
{
|
||||
statusDesejado = comandoSolicitado && atuacaoPermitida;
|
||||
}
|
||||
/*
|
||||
* O agitador em Manual pode ser ligado pela base antes do
|
||||
* início da operação para preparar a calda. Continuam valendo
|
||||
* os bloqueios absolutos de emergência, finalização e operação
|
||||
* concluída; somente a exigência OperacaoIniciada/Pausa deixa
|
||||
* de se aplicar neste modo explícito do operador.
|
||||
*/
|
||||
else if (agitadorManual)
|
||||
{
|
||||
statusDesejado =
|
||||
comandoSolicitado &&
|
||||
!op.Sensoriamento.Operacao.Finalizando &&
|
||||
!op.Sensoriamento.Operacao.Emergencia &&
|
||||
!operacaoConcluida;
|
||||
}
|
||||
/*
|
||||
* Fora da calibração, a bomba principal automática
|
||||
* deve permanecer ligada enquanto a operação estiver
|
||||
|
|
@ -2070,7 +2100,10 @@ namespace AgroBase.Models.Modules
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool PodeEnviarComandoFisico(bool statusLigado, string componente)
|
||||
private bool PodeEnviarComandoFisico(
|
||||
bool statusLigado,
|
||||
string componente,
|
||||
bool permitirDuranteFinalizacao = false)
|
||||
{
|
||||
var dados = Variaveis.OperacaoEmAndamento?.DispAtu?.Dados;
|
||||
|
||||
|
|
@ -2084,6 +2117,24 @@ namespace AgroBase.Models.Modules
|
|||
if (!BloqueioAbsolutoAtuacao(out string motivo))
|
||||
return true;
|
||||
|
||||
/*
|
||||
* Exceção estreita, usada exclusivamente pela rotina de alívio:
|
||||
* permite abrir bicos com a bomba já desligada enquanto a operação
|
||||
* está em Finalizando. Emergência, aplicação encerrando e operação
|
||||
* concluída continuam bloqueando o comando.
|
||||
*/
|
||||
var operacao = Variaveis.OperacaoEmAndamento?.Sensoriamento?.Operacao;
|
||||
|
||||
if (permitirDuranteFinalizacao &&
|
||||
_alivioPressaoLinhaEmAndamento &&
|
||||
operacao?.Finalizando == true &&
|
||||
operacao.Emergencia != true &&
|
||||
operacao.StatusOperacaoAtual != StatusOperacao.Concluido &&
|
||||
!Variaveis.Fechando)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Variaveis.MostrarLog($"[ATU/SEGURANCA] Comando ON bloqueado para {componente}: {motivo}");
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2108,12 +2159,13 @@ namespace AgroBase.Models.Modules
|
|||
CanMessagePosicaoDados dado,
|
||||
bool setarControle,
|
||||
bool status,
|
||||
double? angulo = null
|
||||
double? angulo = null,
|
||||
bool permitirDuranteFinalizacao = false
|
||||
)
|
||||
{
|
||||
var dispAtu = Variaveis.OperacaoEmAndamento?.DispAtu;
|
||||
|
||||
if (bico == null || !PodeEnviarComandoFisico(status, bico.ID))
|
||||
if (bico == null || !PodeEnviarComandoFisico(status, bico.ID, permitirDuranteFinalizacao))
|
||||
return;
|
||||
|
||||
bool comandoControle = bico.ComandoAtuar;
|
||||
|
|
@ -2228,6 +2280,130 @@ namespace AgroBase.Models.Modules
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Desliga as bombas, abre todos os bicos comandáveis por uma janela
|
||||
/// curta para aliviar a pressão residual da linha e volta tudo ao
|
||||
/// estado seguro. Deve ser chamada depois de marcar a operação como
|
||||
/// Finalizando, para impedir que qualquer automação religue a bomba.
|
||||
/// </summary>
|
||||
public async Task AliviarPressaoLinhaAsync(int tempoAlivioMs = 1000)
|
||||
{
|
||||
tempoAlivioMs = Math.Max(0, tempoAlivioMs);
|
||||
|
||||
await _alivioPressaoGate.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
|
||||
if (op?.Sensoriamento?.Operacao?.Finalizando != true)
|
||||
{
|
||||
Variaveis.MostrarLog("[ATU/ALIVIO] Ignorado: operação não está finalizando.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Conectado)
|
||||
{
|
||||
Variaveis.MostrarLog("[ATU/ALIVIO] Ignorado: atuador desconectado.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (op.Sensoriamento.Operacao.Emergencia || Variaveis.Fechando)
|
||||
{
|
||||
Variaveis.MostrarLog("[ATU/ALIVIO] Ignorado por bloqueio de segurança.");
|
||||
return;
|
||||
}
|
||||
|
||||
var bicos = (BicosPulverizadores ?? new List<AtuadorBicoModel>())
|
||||
.Where(x => x != null && x.Inicializado && x.Comandar)
|
||||
.ToList();
|
||||
|
||||
_alivioPressaoLinhaEmAndamento = true;
|
||||
|
||||
Variaveis.MostrarLog(
|
||||
$"[ATU/ALIVIO] Iniciando: bombas OFF, {bicos.Count} bico(s) aberto(s) por {tempoAlivioMs} ms."
|
||||
);
|
||||
|
||||
/* A bomba precisa estar desligada antes de qualquer bico abrir. */
|
||||
foreach (var bomba in BombasPressurizadoras ?? new List<AtuadorBombaModel>())
|
||||
{
|
||||
if (bomba?.Inicializado != true)
|
||||
continue;
|
||||
|
||||
double pressaoOff = bomba.Sensor_ID_Num > -1 ? 0 : -1;
|
||||
|
||||
EnviarComandoFisicoBomba(
|
||||
bomba,
|
||||
CanMessagePosicaoDados.Command1,
|
||||
setarControle: true,
|
||||
status: false,
|
||||
potencia: 0,
|
||||
pressao: pressaoOff
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var bico in bicos)
|
||||
{
|
||||
EnviarComandoFisicoBico(
|
||||
bico,
|
||||
CanMessagePosicaoDados.Command1,
|
||||
setarControle: true,
|
||||
status: true,
|
||||
angulo: bico.AnguloAbertura,
|
||||
permitirDuranteFinalizacao: true
|
||||
);
|
||||
}
|
||||
|
||||
if (tempoAlivioMs > 0)
|
||||
await Task.Delay(tempoAlivioMs).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Variaveis.MostrarLog("[ATU/ALIVIO] Falha durante o alívio: " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
/*
|
||||
* O fechamento é obrigatório inclusive se ocorrer exceção ou
|
||||
* cancelamento no meio da janela.
|
||||
*/
|
||||
foreach (var bico in BicosPulverizadores ?? new List<AtuadorBicoModel>())
|
||||
{
|
||||
if (bico?.Inicializado == true)
|
||||
{
|
||||
EnviarComandoFisicoBico(
|
||||
bico,
|
||||
CanMessagePosicaoDados.Command1,
|
||||
setarControle: true,
|
||||
status: false,
|
||||
angulo: bico.AnguloNeutro
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bomba in BombasPressurizadoras ?? new List<AtuadorBombaModel>())
|
||||
{
|
||||
if (bomba?.Inicializado == true)
|
||||
{
|
||||
double pressaoOff = bomba.Sensor_ID_Num > -1 ? 0 : -1;
|
||||
|
||||
EnviarComandoFisicoBomba(
|
||||
bomba,
|
||||
CanMessagePosicaoDados.Command1,
|
||||
setarControle: true,
|
||||
status: false,
|
||||
potencia: 0,
|
||||
pressao: pressaoOff
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_alivioPressaoLinhaEmAndamento = false;
|
||||
_alivioPressaoGate.Release();
|
||||
Variaveis.MostrarLog("[ATU/ALIVIO] Concluído: bicos e bombas em OFF.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void AtualizaComponentesIniciados()
|
||||
|
|
|
|||
|
|
@ -313,6 +313,38 @@ namespace AgroBase.Models.Modules
|
|||
public Dictionary<TipoMovimentoDirecional, ConfiguracaoSentidoMotor> ConfigSentidos { get; set; }
|
||||
public List<FuncoesPinout> Funcoes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converte o sinal local do módulo (que depende do lado e da montagem
|
||||
/// do motor) para a convenção única usada pelo controle:
|
||||
/// positivo = curva/yaw para a direita; negativo = para a esquerda.
|
||||
///
|
||||
/// A conversão usa a própria configuração que gera os comandos, evitando
|
||||
/// duplicar uma tabela de sinais que poderia divergir da atuação real.
|
||||
/// </summary>
|
||||
public double AnguloCanonicoControle(
|
||||
TipoMovimentoDirecional tipoMovimento,
|
||||
double anguloLocal)
|
||||
{
|
||||
if (double.IsNaN(anguloLocal) || double.IsInfinity(anguloLocal))
|
||||
return 0.0;
|
||||
|
||||
ConfiguracaoSentidoMotor config;
|
||||
if (ConfigSentidos == null ||
|
||||
!ConfigSentidos.TryGetValue(tipoMovimento, out config))
|
||||
{
|
||||
return anguloLocal;
|
||||
}
|
||||
|
||||
// O sentido configurado para Direita define qual sinal local
|
||||
// corresponde ao comando canônico positivo.
|
||||
double fator =
|
||||
config.Direita == Sentido.Horario ? 1.0 :
|
||||
config.Direita == Sentido.Antihorario ? -1.0 :
|
||||
0.0;
|
||||
|
||||
return fator == 0.0 ? 0.0 : anguloLocal * fator;
|
||||
}
|
||||
|
||||
|
||||
public static DirecionalModel CarregarParametrosIniciais(string Mod_ID, byte _EnderecoTx, byte _EnderecoRx)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,9 +51,174 @@ namespace AgroBase.Models.Modules
|
|||
private readonly SemaphoreSlim _gateMovimentoCombinado = new SemaphoreSlim(1, 1);
|
||||
private CancellationTokenSource _ctsMovimentoCombinado;
|
||||
|
||||
// Estado físico separado do comando no simulador. No rover real o
|
||||
// estado é obtido diretamente de AnguloLeitura dos módulos.
|
||||
public double AnguloDirecionalSimuladoCanonico { get; set; } = 0.0;
|
||||
public DateTime MomentoAnguloDirecionalSimulado { get; set; } = DateTime.MinValue;
|
||||
|
||||
public class EstadoDirecionalCanonico
|
||||
{
|
||||
public TipoMovimentoDirecional TipoMovimento { get; set; }
|
||||
public double AnguloComando { get; set; }
|
||||
public double AnguloSP { get; set; }
|
||||
public double AnguloFisico { get; set; }
|
||||
public double Dispersao { get; set; }
|
||||
public int RodasValidas { get; set; }
|
||||
public bool Valido { get; set; }
|
||||
public bool EmTransicao { get; set; }
|
||||
public long TimestampUnixMs { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public List<ModuloMvdModel> Modulos { get; set; } = new List<ModuloMvdModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Consolida as quatro direções em um estado canônico para o MPC.
|
||||
/// Somente os eixos ativos no modo atual participam. A mediana torna o
|
||||
/// valor resistente a uma leitura isolada e a dispersão expõe desacordo.
|
||||
/// </summary>
|
||||
public EstadoDirecionalCanonico ObterEstadoDirecionalCanonico(
|
||||
TipoMovimentoDirecional tipoMovimento,
|
||||
double anguloComando,
|
||||
bool simulando)
|
||||
{
|
||||
var agora = DateTime.UtcNow;
|
||||
var leituras = new List<double>();
|
||||
var setpoints = new List<double>();
|
||||
DateTime momentoLeituraMaisRecente = DateTime.MinValue;
|
||||
|
||||
foreach (var modulo in Modulos ?? new List<ModuloMvdModel>())
|
||||
{
|
||||
if (modulo?.DirMotor == null ||
|
||||
!ModuloAtivoNoTipo(modulo.Modulo_ID, tipoMovimento))
|
||||
continue;
|
||||
|
||||
var dir = modulo.DirMotor;
|
||||
if (!simulando && !dir.Inicializado)
|
||||
continue;
|
||||
|
||||
if (!simulando)
|
||||
{
|
||||
DateTime momentoLeitura =
|
||||
dir.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue;
|
||||
if (momentoLeitura == DateTime.MinValue ||
|
||||
DateTime.Now - momentoLeitura > TimeSpan.FromSeconds(2))
|
||||
continue;
|
||||
if (momentoLeitura > momentoLeituraMaisRecente)
|
||||
momentoLeituraMaisRecente = momentoLeitura;
|
||||
}
|
||||
|
||||
double leitura = dir.AnguloCanonicoControle(
|
||||
tipoMovimento,
|
||||
dir.AnguloLeitura
|
||||
);
|
||||
double sp = dir.AnguloCanonicoControle(
|
||||
tipoMovimento,
|
||||
dir.AnguloSPAtual
|
||||
);
|
||||
|
||||
if (!double.IsNaN(leitura) && !double.IsInfinity(leitura))
|
||||
leituras.Add(leitura);
|
||||
if (!double.IsNaN(sp) && !double.IsInfinity(sp))
|
||||
setpoints.Add(sp);
|
||||
}
|
||||
|
||||
double anguloFisico;
|
||||
if (simulando && MomentoAnguloDirecionalSimulado != DateTime.MinValue)
|
||||
{
|
||||
anguloFisico = AnguloDirecionalSimuladoCanonico;
|
||||
}
|
||||
else
|
||||
{
|
||||
anguloFisico = Mediana(leituras);
|
||||
}
|
||||
|
||||
double anguloSP = setpoints.Count > 0
|
||||
? Mediana(setpoints)
|
||||
: anguloComando;
|
||||
|
||||
double dispersao = leituras.Count > 0
|
||||
? leituras.Max(x => Math.Abs(x - Mediana(leituras)))
|
||||
: 0.0;
|
||||
|
||||
int rodasValidas = simulando ? QuantidadeRodasAtivas(tipoMovimento) : leituras.Count;
|
||||
bool valido = simulando
|
||||
? MomentoAnguloDirecionalSimulado != DateTime.MinValue
|
||||
: rodasValidas >= QuantidadeMinimaRodasValidas(tipoMovimento) && dispersao <= 8.0;
|
||||
|
||||
return new EstadoDirecionalCanonico
|
||||
{
|
||||
TipoMovimento = tipoMovimento,
|
||||
AnguloComando = anguloComando,
|
||||
AnguloSP = anguloSP,
|
||||
AnguloFisico = anguloFisico,
|
||||
Dispersao = dispersao,
|
||||
RodasValidas = rodasValidas,
|
||||
Valido = valido,
|
||||
EmTransicao = Math.Abs(anguloSP - anguloFisico) > 1.0,
|
||||
TimestampUnixMs = simulando
|
||||
? new DateTimeOffset(
|
||||
MomentoAnguloDirecionalSimulado == DateTime.MinValue
|
||||
? agora
|
||||
: MomentoAnguloDirecionalSimulado
|
||||
).ToUnixTimeMilliseconds()
|
||||
: (momentoLeituraMaisRecente == DateTime.MinValue
|
||||
? 0
|
||||
: new DateTimeOffset(momentoLeituraMaisRecente)
|
||||
.ToUnixTimeMilliseconds())
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ModuloAtivoNoTipo(
|
||||
string moduloId,
|
||||
TipoMovimentoDirecional tipo)
|
||||
{
|
||||
bool dianteiro = (moduloId ?? "").EndsWith("F", StringComparison.OrdinalIgnoreCase);
|
||||
bool traseiro = (moduloId ?? "").EndsWith("T", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
switch (tipo)
|
||||
{
|
||||
case TipoMovimentoDirecional.RodasDianteiras:
|
||||
return dianteiro;
|
||||
case TipoMovimentoDirecional.RodasTraseiras:
|
||||
return traseiro;
|
||||
case TipoMovimentoDirecional.MovimentoArco:
|
||||
case TipoMovimentoDirecional.MovimentoDiagonal:
|
||||
case TipoMovimentoDirecional.MovimentoLateral:
|
||||
case TipoMovimentoDirecional.RotacionarNoEixo:
|
||||
return dianteiro || traseiro;
|
||||
default:
|
||||
return dianteiro;
|
||||
}
|
||||
}
|
||||
|
||||
private static int QuantidadeRodasAtivas(TipoMovimentoDirecional tipo)
|
||||
{
|
||||
return tipo == TipoMovimentoDirecional.RodasDianteiras ||
|
||||
tipo == TipoMovimentoDirecional.RodasTraseiras ? 2 : 4;
|
||||
}
|
||||
|
||||
private static int QuantidadeMinimaRodasValidas(TipoMovimentoDirecional tipo)
|
||||
{
|
||||
return QuantidadeRodasAtivas(tipo) == 2 ? 1 : 2;
|
||||
}
|
||||
|
||||
private static double Mediana(IEnumerable<double> valores)
|
||||
{
|
||||
var ordenados = (valores ?? Enumerable.Empty<double>())
|
||||
.Where(x => !double.IsNaN(x) && !double.IsInfinity(x))
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
if (ordenados.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
int meio = ordenados.Count / 2;
|
||||
return ordenados.Count % 2 == 1
|
||||
? ordenados[meio]
|
||||
: (ordenados[meio - 1] + ordenados[meio]) / 2.0;
|
||||
}
|
||||
|
||||
public (bool, List<string>) StatusModulosMOV(Direcao Lado)
|
||||
{
|
||||
string Prefixo = Lado == Direcao.Esquerda ? "E" : Lado == Direcao.Direita ? "D" : "";
|
||||
|
|
|
|||
|
|
@ -1451,6 +1451,14 @@ namespace AgroBase.Models
|
|||
|
||||
op.Sensoriamento.Operacao.Finalizando = true;
|
||||
|
||||
/*
|
||||
* Com a operação já bloqueada para novas atuações automáticas,
|
||||
* desliga as bombas e abre os bicos por 1 s para aliviar a pressão
|
||||
* residual da linha. A própria rotina garante o fechamento final.
|
||||
*/
|
||||
await (op.DispAtu?.Dados?.AliviarPressaoLinhaAsync(1000)
|
||||
?? Task.CompletedTask).ConfigureAwait(false);
|
||||
|
||||
op.Trajetoria?.LoopAtualizaDados();
|
||||
|
||||
await Task.Delay(2000);
|
||||
|
|
@ -2664,6 +2672,16 @@ namespace AgroBase.Models
|
|||
op?.DispAtu?.Dados?.PrepararAtuadorParaOperacao();
|
||||
|
||||
HealthWorkerService.AtualizarDadosOperacao(true);
|
||||
|
||||
/*
|
||||
* A atualização parcial substitui principalmente a instância de
|
||||
* Parametros.Controle. Os controles da IHM mantinham a referência
|
||||
* visual anterior e podiam regravar valores antigos ao alterar um
|
||||
* único campo. Recarrega a tela na thread da própria IHM, sem
|
||||
* reconstruir o mapa da operação em andamento.
|
||||
*/
|
||||
frmInstancial.frmIHM?._frmOperacao?
|
||||
.AtualizarParametrosTela(op, recarregarMapa: false);
|
||||
}
|
||||
|
||||
private void ExecutarLiberacaoHumanaAutonomia(OperacaoComandoBaseModel controleBase, OperacaoModel op)
|
||||
|
|
@ -2943,6 +2961,12 @@ namespace AgroBase.Models
|
|||
case S_Code.sBMB:
|
||||
var bomba = dadosAtu.BombasPressurizadoras?.FirstOrDefault(x => x.ID == controleBase._comp_id);
|
||||
|
||||
if (bomba == null)
|
||||
{
|
||||
Variaveis.MostrarLog("[ComandoBase] Bomba ATU não encontrada: " + controleBase._comp_id);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (controleBase.Controle.Comando)
|
||||
{
|
||||
case CanMessagePosicaoDados.Command2:
|
||||
|
|
@ -2960,8 +2984,31 @@ namespace AgroBase.Models
|
|||
|
||||
bool ligarBomba = controleBase.Controle.Estado == true;
|
||||
|
||||
if (bomba.ID == dadosAtu?.AgitadorDeCalda?.ID)
|
||||
op.Parametros.Controle.AtuAgitadorModo = ligarBomba ? ModoAgitadorCalda.Manual : ModoAgitadorCalda.SemAgitacao;
|
||||
bool ehAgitador = bomba.ID == dadosAtu?.AgitadorDeCalda?.ID;
|
||||
|
||||
if (ehAgitador && op.Parametros?.Controle != null)
|
||||
{
|
||||
/*
|
||||
* O comando direto da base assume a fonte de verdade do
|
||||
* agitador. Manual impede o controle automático de
|
||||
* sobrescrever o ON; SemAgitacao mantém o OFF até uma
|
||||
* nova parametrização da operação.
|
||||
*/
|
||||
op.Parametros.Controle.AtuAgitadorModo = ligarBomba
|
||||
? ModoAgitadorCalda.Manual
|
||||
: ModoAgitadorCalda.SemAgitacao;
|
||||
|
||||
bomba.ComandoAtuar = ligarBomba;
|
||||
bomba.PotenciaSP = ligarBomba
|
||||
? FuncoesMatematicas.Clamp(controleBase.Controle.Potencia ?? bomba.PotenciaSP, 0, 100)
|
||||
: 0;
|
||||
bomba.PressaoSP = -1;
|
||||
|
||||
Variaveis.MostrarLog(
|
||||
$"[ComandoBase][Agitador] Estado={(ligarBomba ? "ON" : "OFF")}, " +
|
||||
$"Modo={op.Parametros.Controle.AtuAgitadorModo}, Potencia={bomba.PotenciaSP:0.##}%"
|
||||
);
|
||||
}
|
||||
|
||||
dadosAtu?.EnviarComandoFisicoBomba(
|
||||
bomba,
|
||||
|
|
@ -2970,7 +3017,9 @@ namespace AgroBase.Models
|
|||
status: ligarBomba,
|
||||
potencia: controleBase.Controle.Potencia,
|
||||
pressao: controleBase.Controle.Pressao,
|
||||
modo: op.Parametros.Controle.AtuModoControle
|
||||
modo: ehAgitador
|
||||
? bomba.Controle.Modo
|
||||
: op.Parametros?.Controle?.AtuModoControle
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,7 @@ namespace AgroBase.Models
|
|||
public static double DistanciaEntreEixosCm { get; } = 92.0;
|
||||
public static double BitolaRodasCm { get; } = 75.0;
|
||||
public static double KuDirecional { get; } = 2.65;
|
||||
public static double VelocidadeAngularDirecionalGrausSegundo { get; } = 50.0;
|
||||
public static double LeverArmFrontalCm { get; set; } = 37.0; // cm
|
||||
public static double LeverArmLateralCm { get; set; } = 0.0; // cm
|
||||
public static double LarguraEquipamentoMm
|
||||
|
|
|
|||
|
|
@ -193,6 +193,13 @@ namespace AgroBase.Services.Operadores
|
|||
|
||||
bool carroEmMovimento = comandoMovimentoAtivo && velocidadeFisicaAtiva;
|
||||
|
||||
var estadoDirecional = op.DispMvd?.Dados?
|
||||
.ObterEstadoDirecionalCanonico(
|
||||
op.Controle.TipoMovimento,
|
||||
op.Controle.Angulo,
|
||||
op.Simulando
|
||||
);
|
||||
|
||||
var bombaLinha = op?.DispAtu?.Dados?.BombaPressurizadora;
|
||||
double pressaoAlvo = bombaLinha?._PressaoSPControle > 0 ? bombaLinha._PressaoSPControle : op.Parametros?.Controle?.AtuPressaoLinha ?? 0;
|
||||
double pressaoLinha = bombaLinha?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0;
|
||||
|
|
@ -252,6 +259,19 @@ namespace AgroBase.Services.Operadores
|
|||
percentual_bateria = _Sensoriamento.Bateria?.PercentualBateria ?? 0,
|
||||
}
|
||||
),
|
||||
("Direcional", estadoDirecional != null ? new
|
||||
{
|
||||
tipo_movimento = (int)estadoDirecional.TipoMovimento,
|
||||
angulo_comando = estadoDirecional.AnguloComando,
|
||||
angulo_sp = estadoDirecional.AnguloSP,
|
||||
angulo_fisico = estadoDirecional.AnguloFisico,
|
||||
dispersao = estadoDirecional.Dispersao,
|
||||
rodas_validas = estadoDirecional.RodasValidas,
|
||||
valido = estadoDirecional.Valido,
|
||||
em_transicao = estadoDirecional.EmTransicao,
|
||||
timestamp_unix_ms = estadoDirecional.TimestampUnixMs,
|
||||
} : null
|
||||
),
|
||||
("Atuador", new
|
||||
{
|
||||
pressao_linha_regular = pressaoLinhaRegular,
|
||||
|
|
@ -427,6 +447,7 @@ namespace AgroBase.Services.Operadores
|
|||
tracking_row_hybrid_diagonal_enabled = true,
|
||||
entry_capture_enabled = false,
|
||||
tracking_axis_debug = false,
|
||||
atuador_dir_taxa_graus_s = VariaveisEquipamento.VelocidadeAngularDirecionalGrausSegundo
|
||||
}
|
||||
}
|
||||
),
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ def _comando_mapa_gps_mpc(contexto: dict):
|
|||
parada_necessaria=True,
|
||||
)
|
||||
|
||||
comando_anterior = _ler_comando_anterior_para_mpc()
|
||||
comando_anterior = _ler_comando_anterior_para_mpc(contexto)
|
||||
|
||||
inicio = time.perf_counter()
|
||||
comando = mpc.compute_receding(contexto, comando_anterior)
|
||||
|
|
@ -461,6 +461,7 @@ def _comando_mapeamento_visual(contexto: dict):
|
|||
def _montar_contexto_direcional(*, operacao, controle, contexto_global, equipamento):
|
||||
trajetoria = _dict(contexto_global.get("Trajetoria", {}))
|
||||
gerais = _dict(contexto_global.get("Gerais", {}))
|
||||
direcional_fisico = _dict(contexto_global.get("Direcional", {}))
|
||||
gps_info = _montar_contexto_gps(operacao)
|
||||
imu_contexto = _montar_contexto_imu_direcional(_dict(ContextoGlobalRedis.get_modulo(T_Code.Imu)))
|
||||
|
||||
|
|
@ -513,6 +514,38 @@ def _montar_contexto_direcional(*, operacao, controle, contexto_global, equipame
|
|||
1,
|
||||
),
|
||||
},
|
||||
"Direcional": {
|
||||
"TipoMovimento": _int(
|
||||
direcional_fisico.get(
|
||||
"tipo_movimento",
|
||||
controle.get(
|
||||
"tipo_movimento_direcional",
|
||||
TipoMovimentoDirecional.RodasDianteiras.value,
|
||||
),
|
||||
),
|
||||
TipoMovimentoDirecional.RodasDianteiras.value,
|
||||
),
|
||||
"AnguloComando": _float(
|
||||
direcional_fisico.get("angulo_comando", controle.get("angulo_sp", 0.0)),
|
||||
0.0,
|
||||
),
|
||||
"AnguloSP": _float(
|
||||
direcional_fisico.get("angulo_sp", controle.get("angulo_sp", 0.0)),
|
||||
0.0,
|
||||
),
|
||||
"AnguloFisico": _float(
|
||||
direcional_fisico.get("angulo_fisico", controle.get("angulo_sp_ant", 0.0)),
|
||||
0.0,
|
||||
),
|
||||
"Dispersao": _float(direcional_fisico.get("dispersao", 0.0), 0.0),
|
||||
"RodasValidas": _int(direcional_fisico.get("rodas_validas", 0), 0),
|
||||
"Valido": _bool(direcional_fisico.get("valido", False)),
|
||||
"EmTransicao": _bool(direcional_fisico.get("em_transicao", False)),
|
||||
"TimestampUnixMs": _float(
|
||||
direcional_fisico.get("timestamp_unix_ms", 0.0),
|
||||
0.0,
|
||||
),
|
||||
},
|
||||
"VisualWorker": visual_worker,
|
||||
"IMU": imu_contexto,
|
||||
"Equipamento": {
|
||||
|
|
@ -1125,10 +1158,16 @@ def _montar_comando_retorno(comando, latencia=-1.0):
|
|||
return comando
|
||||
|
||||
|
||||
def _ler_comando_anterior_para_mpc():
|
||||
def _ler_comando_anterior_para_mpc(contexto=None):
|
||||
controle = _dict(ContextoGlobalRedis.get_controle())
|
||||
direcional = _dict(_dict(contexto).get("Direcional", {}))
|
||||
|
||||
return {
|
||||
telemetria_valida = _bool(direcional.get("Valido", False))
|
||||
timestamp_ms = _float(direcional.get("TimestampUnixMs", 0.0), 0.0)
|
||||
idade_ms = max(0.0, time.time() * 1000.0 - timestamp_ms) if timestamp_ms > 0 else 1e9
|
||||
telemetria_valida = telemetria_valida and idade_ms <= 1500.0
|
||||
|
||||
comando = {
|
||||
"angulo": _float(
|
||||
controle.get("angulo_sp_ant", controle.get("angulo_sp", 0.0)),
|
||||
0.0,
|
||||
|
|
@ -1148,6 +1187,25 @@ def _ler_comando_anterior_para_mpc():
|
|||
),
|
||||
}
|
||||
|
||||
comando["angulo_aplicado"] = (
|
||||
_float(direcional.get("AnguloFisico", comando["angulo"]), comando["angulo"])
|
||||
if telemetria_valida else comando["angulo"]
|
||||
)
|
||||
comando["angulo_sp_aplicado"] = (
|
||||
_float(direcional.get("AnguloSP", comando["angulo"]), comando["angulo"])
|
||||
if telemetria_valida else comando["angulo"]
|
||||
)
|
||||
comando["tipo_aplicado"] = (
|
||||
_normalizar_tipo_movimento(direcional.get("TipoMovimento", comando["tipo"]))
|
||||
if telemetria_valida else comando["tipo"]
|
||||
)
|
||||
comando["telemetria_direcional_valida"] = telemetria_valida
|
||||
comando["telemetria_direcional_idade_ms"] = idade_ms
|
||||
comando["telemetria_direcional_dispersao"] = _float(
|
||||
direcional.get("Dispersao", 0.0), 0.0
|
||||
)
|
||||
return comando
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers de matriz
|
||||
|
|
|
|||
|
|
@ -450,6 +450,27 @@ class ControladorMPC:
|
|||
1.0, min_value=0.25, max_value=2.0,
|
||||
)
|
||||
|
||||
# V22 - amortecimento exclusivo do handoff antecipado em SaindoRua.
|
||||
#
|
||||
# Nesse estado o rover ja opera a 15% e ainda percorre o trecho reto
|
||||
# antes da Dubins. O Pure Pursuit bruto pode alternar pequenos comandos
|
||||
# de Arco quando a projecao/target cruza de um lado para o outro da
|
||||
# centerline. Aplicamos filtro, rate-limit, deadband e passagem por zero
|
||||
# SOMENTE nessa fase. Ao entrar em Manobrando, o PP continua bruto e
|
||||
# preserva integralmente a autoridade angular validada em campo.
|
||||
self._dubins_saida_filter_tau_s = _safe_float(
|
||||
parametros_mpc.get("dubins_saida_filter_tau_s", 0.30),
|
||||
0.30, min_value=0.0, max_value=2.0,
|
||||
)
|
||||
self._dubins_saida_rate_graus_s = _safe_float(
|
||||
parametros_mpc.get("dubins_saida_rate_graus_s", 18.0),
|
||||
18.0, min_value=2.0, max_value=120.0,
|
||||
)
|
||||
self._dubins_saida_deadband_graus = _safe_float(
|
||||
parametros_mpc.get("dubins_saida_deadband_graus", 1.0),
|
||||
1.0, min_value=0.0, max_value=5.0,
|
||||
)
|
||||
|
||||
# V18 - handoff antecipado para o Pure Pursuit.
|
||||
#
|
||||
# SaindoRua ja nasce alguns metros antes da BordaSaida para reduzir a
|
||||
|
|
@ -773,6 +794,12 @@ class ControladorMPC:
|
|||
parametros_mpc.get("cinematica_entre_eixos_m", 0.94),
|
||||
0.94, min_value=0.20, max_value=4.0,
|
||||
)
|
||||
# Dinâmica física da direção. O valor comandado é o alvo; o estado
|
||||
# usado na cinemática avança gradualmente a partir da telemetria real.
|
||||
self._atuador_dir_taxa_graus_s = _safe_float(
|
||||
parametros_mpc.get("atuador_dir_taxa_graus_s", 50.0),
|
||||
50.0, min_value=3.0, max_value=180.0,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# V14 - CURVE TRACK: yaw contínuo quando a própria rua está curvando
|
||||
|
|
@ -1571,14 +1598,93 @@ class ControladorMPC:
|
|||
|
||||
return False
|
||||
|
||||
def _estabilizar_pp_saindo_rua(
|
||||
self, angulo_desejado_rad, comando_anterior, dt_controle
|
||||
):
|
||||
"""V22: amortece o PP apenas no trecho reto de SaindoRua.
|
||||
|
||||
Regras de seguranca:
|
||||
- limita a taxa angular por segundo;
|
||||
- filtra o comando por primeira ordem;
|
||||
- elimina ruido dentro da deadband;
|
||||
- numa inversao real, passa deliberadamente por zero;
|
||||
- nunca devolve comando no hemisferio oposto ao desejado.
|
||||
|
||||
Manobrando nao chama esta funcao e continua recebendo o PP bruto.
|
||||
"""
|
||||
limite = math.radians(float(self.angulo_max_graus))
|
||||
desejado = float(np.clip(angulo_desejado_rad, -limite, limite))
|
||||
|
||||
anterior = math.radians(_safe_float(
|
||||
_as_dict(comando_anterior).get("angulo", 0.0),
|
||||
0.0,
|
||||
min_value=-self.angulo_max_graus,
|
||||
max_value=self.angulo_max_graus,
|
||||
))
|
||||
dt = _safe_float(dt_controle, 0.5, min_value=0.05, max_value=1.5)
|
||||
dead = math.radians(float(self._dubins_saida_deadband_graus))
|
||||
|
||||
# Perto de zero, pequenas mudancas de lado da projecao nao devem
|
||||
# movimentar as quatro rodas em Arco enquanto o rover ainda esta reto.
|
||||
if abs(desejado) <= dead:
|
||||
desejado = 0.0
|
||||
|
||||
inversao_significativa = bool(
|
||||
abs(desejado) > dead
|
||||
and abs(anterior) > dead
|
||||
and (desejado * anterior) < 0.0
|
||||
)
|
||||
|
||||
if inversao_significativa:
|
||||
filtrado = 0.0
|
||||
else:
|
||||
tau = float(self._dubins_saida_filter_tau_s)
|
||||
alpha = 1.0 if tau <= 1e-6 else dt / (tau + dt)
|
||||
filtrado = anterior + alpha * (desejado - anterior)
|
||||
|
||||
max_delta = math.radians(
|
||||
float(self._dubins_saida_rate_graus_s)
|
||||
) * dt
|
||||
filtrado = anterior + float(np.clip(
|
||||
filtrado - anterior,
|
||||
-max_delta,
|
||||
max_delta,
|
||||
))
|
||||
|
||||
if desejado == 0.0 and abs(filtrado) <= 2.0 * dead:
|
||||
filtrado = 0.0
|
||||
|
||||
# Barreira final contra a manutencao de esterco no sentido antigo.
|
||||
if (
|
||||
abs(desejado) > dead
|
||||
and abs(filtrado) > dead
|
||||
and (desejado * filtrado) < 0.0
|
||||
):
|
||||
filtrado = 0.0
|
||||
|
||||
return float(np.clip(filtrado, -limite, limite))
|
||||
|
||||
def _comando_seguidor_dubins(
|
||||
self, contexto, comando_anterior, x, y, theta, velocidade, status_carro,
|
||||
idx_alvo_correcao, pos_lat, pos_lon, pos_theta, pos_latencia, ponto_alvo_real
|
||||
):
|
||||
angulo_rad, dbg = self._referencia_seguidor_dubins(
|
||||
angulo_bruto_rad, dbg = self._referencia_seguidor_dubins(
|
||||
x, y, theta, idx_alvo_correcao, velocidade, contexto
|
||||
)
|
||||
|
||||
pp_antecipado = _status_in(
|
||||
status_carro, [StatusCarroMapa.SaindoRua]
|
||||
)
|
||||
angulo_rad = (
|
||||
self._estabilizar_pp_saindo_rua(
|
||||
angulo_bruto_rad,
|
||||
comando_anterior,
|
||||
self.tempo_execucao_local,
|
||||
)
|
||||
if pp_antecipado
|
||||
else float(angulo_bruto_rad)
|
||||
)
|
||||
|
||||
preview_xy = self._simular_preview_seguidor_dubins(
|
||||
x, y, theta, idx_alvo_correcao, velocidade, contexto,
|
||||
self.tempo_execucao_local,
|
||||
|
|
@ -1620,7 +1726,7 @@ class ControladorMPC:
|
|||
"debug_seguidor_dubins": {
|
||||
"modo": (
|
||||
"pure_pursuit_4ws_antecipado"
|
||||
if _status_in(status_carro, [StatusCarroMapa.SaindoRua])
|
||||
if pp_antecipado
|
||||
else "pure_pursuit_4ws"
|
||||
),
|
||||
"lookahead_m": float(dbg["lookahead_m"]),
|
||||
|
|
@ -1628,7 +1734,9 @@ class ControladorMPC:
|
|||
"alpha_graus": float(math.degrees(dbg["alpha"])),
|
||||
"kappa_pp": float(dbg["kappa_pp"]),
|
||||
"kappa_cmd": float(dbg["kappa_cmd"]),
|
||||
"comando_graus": float(math.degrees(dbg["delta_final"])),
|
||||
"comando_bruto_graus": float(math.degrees(angulo_bruto_rad)),
|
||||
"comando_graus": float(math.degrees(angulo_rad)),
|
||||
"amortecimento_saida_ativo": bool(pp_antecipado),
|
||||
},
|
||||
"candidatos_testados": 0,
|
||||
"motivos": [],
|
||||
|
|
@ -2272,6 +2380,13 @@ class ControladorMPC:
|
|||
)["omega"]
|
||||
)
|
||||
|
||||
def _avancar_atuador_direcional(self, angulo_atual, angulo_alvo, dt):
|
||||
"""Aplica limite de velocidade ao estado físico da direção (radianos)."""
|
||||
atual = float(angulo_atual)
|
||||
alvo = float(angulo_alvo)
|
||||
passo_max = np.radians(self._atuador_dir_taxa_graus_s) * max(0.0, float(dt))
|
||||
return float(atual + np.clip(alvo - atual, -passo_max, passo_max))
|
||||
|
||||
@staticmethod
|
||||
def _posicoes_eixos_tracking(x, y, theta, entre_eixos):
|
||||
"""Retorna centros dos eixos dianteiro/traseiro a partir do centro.
|
||||
|
|
@ -3921,6 +4036,9 @@ class ControladorMPC:
|
|||
self.dt = old_dt
|
||||
|
||||
# --- integra por tempo, quebrando cada trecho em subpassos ~dt ---
|
||||
angulo_estado = float(
|
||||
u_hold.get('angulo_inicial', u_hold.get('angulo', 0.0))
|
||||
)
|
||||
t_rem = latency_s
|
||||
for dur_s, u in cmd_seq:
|
||||
if t_rem <= 0.0:
|
||||
|
|
@ -3935,7 +4053,18 @@ class ControladorMPC:
|
|||
for _ in range(n):
|
||||
if time_left_ms() <= 0.0:
|
||||
return x, y, theta, visitados
|
||||
x, y, theta = aplicar_passo_dt(x, y, theta, u, dt_eff)
|
||||
angulo_estado = self._avancar_atuador_direcional(
|
||||
angulo_estado,
|
||||
u.get('angulo', angulo_estado),
|
||||
dt_eff,
|
||||
)
|
||||
u_aplicado = dict(u)
|
||||
u_aplicado['angulo'] = angulo_estado
|
||||
# omega precisa acompanhar o ângulo físico, não o alvo antigo.
|
||||
u_aplicado.pop('omega', None)
|
||||
x, y, theta = aplicar_passo_dt(
|
||||
x, y, theta, u_aplicado, dt_eff
|
||||
)
|
||||
idx_alvo = self._corrigir_pontos_visitados(
|
||||
x,
|
||||
y,
|
||||
|
|
@ -4190,19 +4319,31 @@ class ControladorMPC:
|
|||
# -------------------- Correção por latência (vida real) --------------------
|
||||
# Mantém correção com 'self.dt' e 'velocidade' REAIS
|
||||
u_hold = {
|
||||
"tipo": comando_anterior.get("tipo"),
|
||||
"angulo": np.radians(comando_anterior.get("angulo", 0.0)),
|
||||
"v": velocidade,
|
||||
"omega": self._calcular_omega_4ws(
|
||||
velocidade,
|
||||
np.radians(comando_anterior.get("angulo", 0.0)),
|
||||
comando_anterior.get("tipo")
|
||||
"tipo": comando_anterior.get(
|
||||
"tipo_aplicado", comando_anterior.get("tipo")
|
||||
),
|
||||
"angulo": np.radians(
|
||||
comando_anterior.get(
|
||||
"angulo_sp_aplicado",
|
||||
comando_anterior.get("angulo", 0.0),
|
||||
)
|
||||
),
|
||||
"angulo_inicial": np.radians(
|
||||
comando_anterior.get(
|
||||
"angulo_aplicado",
|
||||
comando_anterior.get("angulo", 0.0),
|
||||
)
|
||||
),
|
||||
"v": velocidade,
|
||||
}
|
||||
cmd_seq = None
|
||||
latencia_compensada_s = min(
|
||||
pos_latencia + self.tempo_execucao_local,
|
||||
LAT_MAX,
|
||||
)
|
||||
x, y, theta, self.visitados_execucao = self.corrigir_pose_por_latencia(
|
||||
x, y, theta, self.visitados_execucao,
|
||||
latency_s=min(pos_latencia + self.tempo_execucao_local, LAT_MAX),
|
||||
latency_s=latencia_compensada_s,
|
||||
dt=self.dt,
|
||||
nova_posicao_fn=self._nova_posicao,
|
||||
u_hold=u_hold,
|
||||
|
|
@ -4210,6 +4351,19 @@ class ControladorMPC:
|
|||
time_left_ms=lambda: (deadline - now()) * 1000.0,
|
||||
calc_omega_fn=self._calcular_omega_4ws
|
||||
)
|
||||
|
||||
# A pose acima já foi trazida até o presente. O estado inicial do
|
||||
# beam search precisa avançar pelo mesmo intervalo para não voltar
|
||||
# ao ângulo físico antigo recebido junto com a posição GNSS.
|
||||
angulo_aplicado_estimado = self._avancar_atuador_direcional(
|
||||
u_hold["angulo_inicial"],
|
||||
u_hold["angulo"],
|
||||
latencia_compensada_s,
|
||||
)
|
||||
comando_anterior = dict(comando_anterior)
|
||||
comando_anterior["angulo_aplicado"] = float(
|
||||
np.degrees(angulo_aplicado_estimado)
|
||||
)
|
||||
idx_alvo_correcao = self._corrigir_pontos_visitados(
|
||||
x,
|
||||
y,
|
||||
|
|
@ -4407,6 +4561,12 @@ class ControladorMPC:
|
|||
"x": x, "y": y, "theta": theta,
|
||||
"custo": 0.0,
|
||||
"comandos": [(tipo_anterior, angulo_anterior)],
|
||||
"angulo_atuador": np.radians(
|
||||
comando_anterior.get(
|
||||
"angulo_aplicado",
|
||||
comando_anterior.get("angulo", 0.0),
|
||||
)
|
||||
),
|
||||
"trajetoria": [],
|
||||
"visitados": self.visitados_execucao,
|
||||
"inicial": True
|
||||
|
|
@ -4433,6 +4593,10 @@ class ControladorMPC:
|
|||
cmd_anterior_local = {
|
||||
"tipo": candidato["comandos"][-1][0],
|
||||
"angulo": candidato["comandos"][-1][1],
|
||||
"angulo_aplicado": candidato.get(
|
||||
"angulo_atuador",
|
||||
candidato["comandos"][-1][1],
|
||||
),
|
||||
}
|
||||
|
||||
#look_ahead = passo > 2 and status_carro not in [StatusCarroMapa.Manobrando.value, StatusCarroMapa.Direcionando.value]
|
||||
|
|
@ -4525,7 +4689,7 @@ class ControladorMPC:
|
|||
t_c_ini = now()
|
||||
try:
|
||||
# >>> CHANGED: passa dt_pred e v_sim para a simulação de um PASSO
|
||||
custo, sim, valido, visitados_sim, _debug_custo = self._simular_passo(
|
||||
custo, sim, valido, visitados_sim, _debug_custo, angulo_atuador_final = self._simular_passo(
|
||||
x_atual, y_atual, theta_atual,
|
||||
tipo_k, ang_k,
|
||||
custos_candidatos,
|
||||
|
|
@ -4543,6 +4707,7 @@ class ControladorMPC:
|
|||
"x": x_f, "y": y_f, "theta": theta_f,
|
||||
"custo": candidato["custo"] + float(custo),
|
||||
"comandos": candidato["comandos"] + [(tipo_k, ang_k)],
|
||||
"angulo_atuador": angulo_atuador_final,
|
||||
"trajetoria": candidato["trajetoria"] + sim,
|
||||
"visitados": visitados_sim,
|
||||
"inicial": False
|
||||
|
|
@ -5789,6 +5954,9 @@ class ControladorMPC:
|
|||
simulacoes = []
|
||||
prev_ang = float(comando_anterior.get("angulo", 0.0))
|
||||
prev_tipo = comando_anterior.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value)
|
||||
angulo_atuador = float(
|
||||
comando_anterior.get("angulo_aplicado", prev_ang)
|
||||
)
|
||||
pontos_visitados = visitados.copy()
|
||||
|
||||
# chave para custo heurístico vindo do VisualWorker (por ângulo/tipo)
|
||||
|
|
@ -5822,8 +5990,6 @@ class ControladorMPC:
|
|||
custo_visual_worker = 0.0
|
||||
|
||||
# omega e sub-stepping
|
||||
omega_const = self._calcular_omega_4ws(v_planejado, angulo_testado, tipo)
|
||||
|
||||
#passos_local = max(1, int(getattr(self, "passos_horizonte_local", 1)))
|
||||
#dt_total = float(dt_pred) * passos_local
|
||||
#SUB_MAX = float(getattr(self, "_dt_sub_max", 0.15)) # ~150 ms por subpasso (ajuste teu comentário)
|
||||
|
|
@ -5846,11 +6012,21 @@ class ControladorMPC:
|
|||
side_mem_local = 0
|
||||
|
||||
for n_sub in range(n_subs):
|
||||
angulo_atuador = self._avancar_atuador_direcional(
|
||||
angulo_atuador,
|
||||
angulo_testado,
|
||||
sub_dt,
|
||||
)
|
||||
omega_aplicado = self._calcular_omega_4ws(
|
||||
v_planejado,
|
||||
angulo_atuador,
|
||||
tipo,
|
||||
)
|
||||
# IMPORTANTE: _nova_posicao precisa aceitar dt opcional
|
||||
x_sim, y_sim, theta_sim = self._nova_posicao(
|
||||
x_sim, y_sim, theta_sim,
|
||||
omega_const, v_planejado, # (omega, v, tipo, angulo)
|
||||
tipo, angulo_testado,
|
||||
omega_aplicado, v_planejado,
|
||||
tipo, angulo_atuador,
|
||||
dt=sub_dt
|
||||
)
|
||||
|
||||
|
|
@ -6004,7 +6180,7 @@ class ControladorMPC:
|
|||
|
||||
#print(
|
||||
# f"ang:{np.degrees(angulo_testado):.2f} "
|
||||
# f"tipo:{tipo.name} omg:{omega_const:.3f} "
|
||||
# f"tipo:{tipo.name} omg:{omega_aplicado:.3f} "
|
||||
# f"xsim:{finais[0]:.5f} ysim:{finais[1]:.5f} tsim:{finais[2]:.5f} "
|
||||
# f"xalv:{finais[3]:.5f} yalv:{finais[4]:.5f} "
|
||||
# f"epos:{somas[5]:.4f} eposn:{somas[6]:.4f} "
|
||||
|
|
@ -6021,7 +6197,7 @@ class ControladorMPC:
|
|||
debug_custo = {
|
||||
"angulo": np.degrees(angulo_testado),
|
||||
"tipo": tipo.value,
|
||||
"omega": omega_const,
|
||||
"omega": omega_aplicado,
|
||||
"x_sim": finais[0],
|
||||
"y_sim": finais[1],
|
||||
"theta_sim": finais[2],
|
||||
|
|
@ -6057,11 +6233,21 @@ class ControladorMPC:
|
|||
}
|
||||
#print(custo_visual_worker)
|
||||
|
||||
return float(custo_total), simulacoes, True, pontos_visitados, debug_custo
|
||||
debug_custo["atuador_direcional"] = {
|
||||
"angulo_inicial_deg": float(np.degrees(
|
||||
comando_anterior.get("angulo_aplicado", prev_ang)
|
||||
)),
|
||||
"angulo_alvo_deg": float(np.degrees(angulo_testado)),
|
||||
"angulo_final_deg": float(np.degrees(angulo_atuador)),
|
||||
"taxa_graus_s": float(self._atuador_dir_taxa_graus_s),
|
||||
}
|
||||
return float(custo_total), simulacoes, True, pontos_visitados, debug_custo, angulo_atuador
|
||||
|
||||
except Exception as e:
|
||||
mostrar_log(f"Erro ao simular passo para {tipo.name} | angulo: {np.degrees(angulo_testado):.2f}: {e}")
|
||||
return float('inf'), [(0.0, 0.0, 0.0)], False, visitados
|
||||
return float('inf'), [(0.0, 0.0, 0.0)], False, visitados, {}, float(
|
||||
comando_anterior.get("angulo_aplicado", comando_anterior.get("angulo", 0.0))
|
||||
)
|
||||
|
||||
def _nova_posicao(self, x, y, theta, omega, velocidade, tipo, angulo_rad, dt=None):
|
||||
"""V16: integra UM passo com a cinemática 4WS unificada.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,8 +8,10 @@ Exporta o checkpoint PyTorch do SegFormer Multi-Head OAK-FCC-3 para ONNX.
|
|||
|
||||
Exemplo:
|
||||
|
||||
python .\_10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.pt --out backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.onnx --include-norm --postprocess argmax_fullres
|
||||
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --opset 17 --device cuda --include-norm --postprocess argmax_fullres
|
||||
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --checkpoint .\backup\segformer_b1\2026_09_18\stacked_raw5\best_score.pt --out .\backup\segformer_b1\2026_09_18\stacked_raw5\best_score_audit.onnx --opset 17 --device cuda --include-norm --norm-stats .\backup\segformer_b1\2026_09_18\stacked_raw5\norm_stats.json --postprocess none
|
||||
|
||||
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --checkpoint .\backup\segformer_b1\2026_09_18\stacked_raw5\best_operational.pt --out .\backup\segformer_b1\2026_09_18\stacked_raw5\best_operation.onnx --opset 17 --device cuda --include-norm --norm-stats .\backup\segformer_b1\2026_09_18\stacked_raw5\norm_stats.json --postprocess argmax_fullres
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,56 +2,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
_2_apply_review_masks_final.py
|
||||
_2_apply_review_masks.py
|
||||
|
||||
Aplica as máscaras revisadas de volta em dataset/brutas/group.
|
||||
Aplica máscaras revisadas de volta em dataset/brutas/group com política
|
||||
CONSERVADORA: qualquer ambiguidade é isolada em quarentena e NUNCA entra no
|
||||
--apply.
|
||||
|
||||
VERSÃO FINAL / RÁPIDA
|
||||
Regras de segurança:
|
||||
- 1 máscara de review + 1 destino nas BRUTAS -> elegível para apply.
|
||||
- Mesmo base em mais de um grupo/destino das BRUTAS -> QUARENTENA.
|
||||
- Mesmo base mais de uma vez no REVIEW -> QUARENTENA, mesmo se as máscaras
|
||||
forem pixel-a-pixel idênticas.
|
||||
- Duplicidade simultânea em REVIEW e BRUTAS -> QUARENTENA.
|
||||
- Bases sem destino -> SKIP, sem alteração.
|
||||
- O --apply contém uma checagem final de invariantes e só aceita exatamente
|
||||
1 review mask + 1 target por base.
|
||||
|
||||
Princípio:
|
||||
- O nome-base da máscara revisada é a chave.
|
||||
- Os previews das BRUTAS são indexados UMA ÚNICA VEZ em memória.
|
||||
- Se o mesmo base existir em MAIS DE UM grupo das BRUTAS, a máscara revisada
|
||||
é aplicada em TODOS eles. Isso é intencional: são duplicatas que serão
|
||||
consolidadas posteriormente pelo reorganizador do dataset.
|
||||
- Se o mesmo base existir MAIS DE UMA VEZ dentro do próprio REVIEW:
|
||||
* se as máscaras forem pixel-a-pixel equivalentes após normalização para
|
||||
raw_size, tratamos como uma única revisão;
|
||||
* se forem diferentes, é CONFLITO REAL e esse base é pulado para não
|
||||
escolher arbitrariamente uma revisão.
|
||||
Quarentena:
|
||||
- É gerada também no dry-run (somente cópias; nada nas BRUTAS é alterado).
|
||||
- Para cada caso ambíguo é criada uma pasta por combinação review x preview.
|
||||
- Cada pasta contém:
|
||||
preview.<ext> -> preview original das BRUTAS
|
||||
mask.png -> máscara original vinda do REVIEW
|
||||
info.txt -> origem exata, grupos e target correspondente
|
||||
|
||||
Transformação:
|
||||
- config["resolucao"] = resolução do review/treino, ex. 960x600
|
||||
- config["raw_size"] = resolução RAW, ex. 1280x800
|
||||
- Máscaras categóricas são redimensionadas EXCLUSIVAMENTE com INTER_NEAREST.
|
||||
Exemplo:
|
||||
dataset/brutas/review_quarantine/run_YYYYMMDD_HHMMSS/
|
||||
QUARENTENA_MULTI_TARGET/
|
||||
<base>/
|
||||
pair_001__review-X__preview-A/
|
||||
preview.jpg
|
||||
mask.png
|
||||
info.txt
|
||||
pair_002__review-X__preview-B/
|
||||
...
|
||||
|
||||
Performance:
|
||||
- O dry-run não lê milhares de imagens.
|
||||
- Só lê imagens no dry-run quando um mesmo base aparece várias vezes no REVIEW,
|
||||
para confirmar se essas revisões duplicadas são equivalentes.
|
||||
- No --apply, cada base lógico é lido/redimensionado apenas UMA vez e depois
|
||||
gravado em todos os destinos encontrados.
|
||||
- A máscara antiga NÃO é lida.
|
||||
- Não há comparação com máscara antiga, hash global, paleta ou backup.
|
||||
Uso dry-run:
|
||||
|
||||
Uso:
|
||||
|
||||
python .\\_2_apply_review_masks_final.py `
|
||||
python .\\_2_apply_review_masks.py `
|
||||
--review-root "C:\\Users\\USER\\Desktop\\fotos_multiespectrais\\review" `
|
||||
--brutas-root dataset\\brutas\\group `
|
||||
--labelmap dataset\\labelmap.txt `
|
||||
--config config.json
|
||||
|
||||
python .\\_2_apply_review_masks_final.py `
|
||||
Uso apply:
|
||||
|
||||
python .\\_2_apply_review_masks.py `
|
||||
--review-root "C:\\Users\\USER\\Desktop\\fotos_multiespectrais\\review" `
|
||||
--brutas-root dataset\\brutas\\group `
|
||||
--labelmap dataset\\labelmap.txt `
|
||||
--config config.json `
|
||||
--apply
|
||||
|
||||
Opcional:
|
||||
--quarantine-root caminho\\para\\quarentena
|
||||
|
||||
Observação:
|
||||
--labelmap é aceito apenas para manter compatibilidade com o comando antigo.
|
||||
Esta versão não precisa dele.
|
||||
--labelmap é aceito apenas para compatibilidade com o comando antigo.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -60,12 +67,14 @@ import argparse
|
|||
import csv
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Tuple
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
|
@ -89,7 +98,13 @@ class LogicalReview:
|
|||
representative: Path
|
||||
targets: List[Path]
|
||||
preview_paths: List[Path]
|
||||
duplicate_review_equivalent: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuarantineResult:
|
||||
case_dir: Path
|
||||
pair_count: int
|
||||
errors: List[str]
|
||||
|
||||
|
||||
def load_sizes(config_path: Path) -> Sizes:
|
||||
|
|
@ -111,9 +126,8 @@ def load_sizes(config_path: Path) -> Sizes:
|
|||
try:
|
||||
w, h = int(value[0]), int(value[1])
|
||||
except Exception:
|
||||
raise RuntimeError(
|
||||
f'config["{key}"] inválido: {value!r}'
|
||||
)
|
||||
raise RuntimeError(f'config["{key}"] inválido: {value!r}')
|
||||
|
||||
if w <= 0 or h <= 0:
|
||||
raise RuntimeError(
|
||||
f'config["{key}"] deve conter valores positivos: {value!r}'
|
||||
|
|
@ -123,7 +137,6 @@ def load_sizes(config_path: Path) -> Sizes:
|
|||
review_w, review_h = parse("resolucao")
|
||||
raw_w, raw_h = parse("raw_size")
|
||||
|
||||
# Resize puro é seguro geometricamente apenas com a mesma proporção.
|
||||
if review_w * raw_h != raw_w * review_h:
|
||||
raise RuntimeError(
|
||||
"resolucao e raw_size possuem aspect ratios diferentes: "
|
||||
|
|
@ -146,8 +159,7 @@ def discover_review_masks(review_root: Path) -> List[Path]:
|
|||
e:
|
||||
review/<grupo>/final_masks/<base>.png
|
||||
|
||||
Se existir final_masks, usa somente esses arquivos para não engolir
|
||||
predictions/previews auxiliares.
|
||||
Se existir qualquer final_masks, usa somente arquivos dentro de final_masks.
|
||||
"""
|
||||
final_masks = sorted(
|
||||
p for p in review_root.rglob("*.png")
|
||||
|
|
@ -158,9 +170,7 @@ def discover_review_masks(review_root: Path) -> List[Path]:
|
|||
|
||||
masks: List[Path] = []
|
||||
for group_dir in sorted(p for p in review_root.iterdir() if p.is_dir()):
|
||||
masks.extend(
|
||||
sorted(p for p in group_dir.glob("*.png") if p.is_file())
|
||||
)
|
||||
masks.extend(sorted(p for p in group_dir.glob("*.png") if p.is_file()))
|
||||
return masks
|
||||
|
||||
|
||||
|
|
@ -170,13 +180,12 @@ def review_group(path: Path) -> str:
|
|||
return path.parent.name
|
||||
|
||||
|
||||
def build_preview_index(brutas_root: Path) -> Dict[str, List[Path]]:
|
||||
"""
|
||||
Varre o filesystem UMA ÚNICA VEZ.
|
||||
def preview_group(path: Path) -> str:
|
||||
# .../<grupo>/previews/<base>.<ext>
|
||||
return path.parent.parent.name
|
||||
|
||||
Resultado:
|
||||
base -> [preview_path_1, preview_path_2, ...]
|
||||
"""
|
||||
|
||||
def build_preview_index(brutas_root: Path) -> Dict[str, List[Path]]:
|
||||
index: Dict[str, List[Path]] = defaultdict(list)
|
||||
|
||||
for group_dir in sorted(p for p in brutas_root.iterdir() if p.is_dir()):
|
||||
|
|
@ -199,15 +208,6 @@ def group_reviews_by_base(masks: Iterable[Path]) -> Dict[str, List[Path]]:
|
|||
|
||||
|
||||
def load_and_resize_mask(path: Path, sizes: Sizes) -> np.ndarray:
|
||||
"""
|
||||
Abre uma máscara e a normaliza para raw_size.
|
||||
|
||||
Aceita:
|
||||
- config.resolucao
|
||||
- config.raw_size
|
||||
|
||||
Qualquer outro tamanho é erro.
|
||||
"""
|
||||
img = cv2.imread(str(path), cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
raise RuntimeError(f"não consegui abrir máscara: {path}")
|
||||
|
|
@ -236,9 +236,8 @@ def duplicate_reviews_are_equivalent(
|
|||
sizes: Sizes,
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Usado SOMENTE quando o mesmo base aparece várias vezes no review.
|
||||
Como são pouquíssimos casos, podemos comparar pixel a pixel sem afetar
|
||||
a performance geral.
|
||||
A equivalência serve APENAS para diagnóstico/relatório.
|
||||
Mesmo equivalentes, reviews duplicados continuam indo para quarentena.
|
||||
"""
|
||||
if len(paths) <= 1:
|
||||
return True, ""
|
||||
|
|
@ -261,17 +260,10 @@ def duplicate_reviews_are_equivalent(
|
|||
+ " | ".join(str(p) for p in paths),
|
||||
)
|
||||
|
||||
return True, ""
|
||||
return True, "máscaras duplicadas equivalentes pixel-a-pixel após normalização"
|
||||
|
||||
|
||||
def unique_targets_from_previews(
|
||||
previews: List[Path],
|
||||
base: str,
|
||||
) -> List[Path]:
|
||||
"""
|
||||
Um mesmo stem pode, em teoria, aparecer em PNG e JPG dentro da mesma pasta.
|
||||
Deduplicamos pelo caminho final da mask.
|
||||
"""
|
||||
def unique_targets_from_previews(previews: List[Path], base: str) -> List[Path]:
|
||||
by_target: Dict[str, Path] = {}
|
||||
|
||||
for preview in previews:
|
||||
|
|
@ -282,12 +274,7 @@ def unique_targets_from_previews(
|
|||
|
||||
|
||||
def atomic_write_png(img: np.ndarray, dst: Path) -> None:
|
||||
"""
|
||||
PNG continua lossless. Compressão 1 prioriza velocidade.
|
||||
Escreve em temporário e troca atomicamente.
|
||||
"""
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tmp = dst.with_name(dst.stem + ".tmp_review_apply.png")
|
||||
|
||||
ok = cv2.imwrite(
|
||||
|
|
@ -301,11 +288,115 @@ def atomic_write_png(img: np.ndarray, dst: Path) -> None:
|
|||
os.replace(tmp, dst)
|
||||
|
||||
|
||||
def safe_component(value: str, max_len: int = 120) -> str:
|
||||
value = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", value)
|
||||
value = value.strip().rstrip(".")
|
||||
if not value:
|
||||
value = "sem_nome"
|
||||
return value[:max_len]
|
||||
|
||||
|
||||
def quarantine_case(
|
||||
*,
|
||||
quarantine_root: Path,
|
||||
status: str,
|
||||
base: str,
|
||||
review_paths: List[Path],
|
||||
previews: List[Path],
|
||||
targets: List[Path],
|
||||
message: str,
|
||||
) -> QuarantineResult:
|
||||
"""
|
||||
Gera uma pasta por combinação REVIEW x PREVIEW.
|
||||
|
||||
Isso é deliberadamente redundante: para poucos casos ambíguos, é melhor
|
||||
mostrar todas as combinações ao operador do que tentar adivinhar qual
|
||||
pareamento é o correto.
|
||||
"""
|
||||
case_dir = quarantine_root / safe_component(status) / safe_component(base)
|
||||
case_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
errors: List[str] = []
|
||||
pair_count = 0
|
||||
|
||||
# Manifesto geral do caso.
|
||||
manifest = {
|
||||
"status": status,
|
||||
"base": base,
|
||||
"message": message,
|
||||
"review_masks": [str(p) for p in review_paths],
|
||||
"previews": [str(p) for p in previews],
|
||||
"targets": [str(p) for p in targets],
|
||||
}
|
||||
try:
|
||||
(case_dir / "case.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(f"falha ao gravar case.json: {e}")
|
||||
|
||||
# Casos de quarentena aqui sempre devem ter pelo menos um preview e uma mask.
|
||||
# Ainda assim, tratamos defensivamente para não abortar o preflight inteiro.
|
||||
if not review_paths or not previews:
|
||||
errors.append("caso sem review mask ou sem preview; nenhuma dupla criada")
|
||||
return QuarantineResult(case_dir=case_dir, pair_count=0, errors=errors)
|
||||
|
||||
pair_no = 0
|
||||
for r_idx, review_path in enumerate(review_paths, 1):
|
||||
r_group = review_group(review_path)
|
||||
|
||||
for p_idx, preview_path in enumerate(previews, 1):
|
||||
p_group = preview_group(preview_path)
|
||||
pair_no += 1
|
||||
|
||||
pair_name = (
|
||||
f"pair_{pair_no:03d}__"
|
||||
f"review-{safe_component(r_group, 45)}__"
|
||||
f"preview-{safe_component(p_group, 45)}"
|
||||
)
|
||||
pair_dir = case_dir / pair_name
|
||||
pair_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
preview_dst = pair_dir / f"preview{preview_path.suffix.lower()}"
|
||||
mask_dst = pair_dir / "mask.png"
|
||||
|
||||
target = preview_path.parent.parent / "masks" / f"{base}.png"
|
||||
|
||||
try:
|
||||
shutil.copy2(preview_path, preview_dst)
|
||||
shutil.copy2(review_path, mask_dst)
|
||||
|
||||
info = (
|
||||
f"status={status}\n"
|
||||
f"base={base}\n"
|
||||
f"message={message}\n"
|
||||
f"review_index={r_idx}/{len(review_paths)}\n"
|
||||
f"review_group={r_group}\n"
|
||||
f"review_original={review_path}\n"
|
||||
f"preview_index={p_idx}/{len(previews)}\n"
|
||||
f"preview_group={p_group}\n"
|
||||
f"preview_original={preview_path}\n"
|
||||
f"target_correspondente={target}\n"
|
||||
)
|
||||
(pair_dir / "info.txt").write_text(info, encoding="utf-8")
|
||||
pair_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"{pair_name}: {e}")
|
||||
|
||||
return QuarantineResult(
|
||||
case_dir=case_dir,
|
||||
pair_count=pair_count,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
def write_report(path: Path, rows: List[dict]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
fields = [
|
||||
"status",
|
||||
"apply_eligible",
|
||||
"base",
|
||||
"review_count",
|
||||
"review_groups",
|
||||
|
|
@ -315,6 +406,8 @@ def write_report(path: Path, rows: List[dict]) -> None:
|
|||
"previews",
|
||||
"target_count",
|
||||
"targets",
|
||||
"quarantine_dir",
|
||||
"quarantine_pairs",
|
||||
"message",
|
||||
]
|
||||
|
||||
|
|
@ -332,8 +425,8 @@ def write_report(path: Path, rows: List[dict]) -> None:
|
|||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Aplica rapidamente masks revisadas em todas as cópias "
|
||||
"correspondentes encontradas nas BRUTAS."
|
||||
"Aplica masks revisadas somente quando o mapeamento é 1:1; "
|
||||
"toda duplicidade é enviada para quarentena visual."
|
||||
)
|
||||
)
|
||||
ap.add_argument("--review-root", required=True)
|
||||
|
|
@ -344,10 +437,18 @@ def main() -> int:
|
|||
ap.add_argument("--labelmap", default="", help=argparse.SUPPRESS)
|
||||
|
||||
ap.add_argument("--report", default="")
|
||||
ap.add_argument(
|
||||
"--quarantine-root",
|
||||
default="",
|
||||
help=(
|
||||
"Pasta-base da quarentena. Se omitido, usa "
|
||||
"<brutas-root>/../review_quarantine/run_TIMESTAMP."
|
||||
),
|
||||
)
|
||||
ap.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Sem esta flag, faz somente dry-run.",
|
||||
help="Sem esta flag, faz dry-run e gera somente relatório/quarentena.",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
|
|
@ -356,15 +457,24 @@ def main() -> int:
|
|||
config_path = Path(args.config).resolve()
|
||||
|
||||
if not review_root.is_dir():
|
||||
raise SystemExit(
|
||||
f"[ERRO] review-root não encontrado: {review_root}"
|
||||
)
|
||||
raise SystemExit(f"[ERRO] review-root não encontrado: {review_root}")
|
||||
if not brutas_root.is_dir():
|
||||
raise SystemExit(
|
||||
f"[ERRO] brutas-root não encontrado: {brutas_root}"
|
||||
)
|
||||
raise SystemExit(f"[ERRO] brutas-root não encontrado: {brutas_root}")
|
||||
|
||||
sizes = load_sizes(config_path)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
report_path = (
|
||||
Path(args.report).resolve()
|
||||
if args.report
|
||||
else brutas_root.parent / f"apply_review_masks_safe_{timestamp}.csv"
|
||||
)
|
||||
|
||||
quarantine_root = (
|
||||
Path(args.quarantine_root).resolve() / f"run_{timestamp}"
|
||||
if args.quarantine_root
|
||||
else brutas_root.parent / "review_quarantine" / f"run_{timestamp}"
|
||||
)
|
||||
|
||||
print("[CONFIG]")
|
||||
print(
|
||||
|
|
@ -375,9 +485,9 @@ def main() -> int:
|
|||
f" raw_size : {sizes.raw_w}x{sizes.raw_h} "
|
||||
"(brutas)"
|
||||
)
|
||||
print(
|
||||
" resize : cv2.INTER_NEAREST"
|
||||
)
|
||||
print(" resize : cv2.INTER_NEAREST")
|
||||
print(" política : SOMENTE mapeamento 1 review : 1 target")
|
||||
print(f" quarantine root : {quarantine_root}")
|
||||
|
||||
masks = discover_review_masks(review_root)
|
||||
if not masks:
|
||||
|
|
@ -394,34 +504,33 @@ def main() -> int:
|
|||
"encontrados nas BRUTAS."
|
||||
)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
report_path = (
|
||||
Path(args.report).resolve()
|
||||
if args.report
|
||||
else brutas_root.parent
|
||||
/ f"apply_review_masks_final_{timestamp}.csv"
|
||||
)
|
||||
|
||||
print("=" * 96)
|
||||
print("APPLY REVIEW MASKS - FINAL")
|
||||
print("=" * 100)
|
||||
print("APPLY REVIEW MASKS - SAFE / QUARANTINE")
|
||||
print(f"Modo : {'APPLY' if args.apply else 'DRY-RUN'}")
|
||||
print(f"Review root : {review_root}")
|
||||
print(f"Brutas root : {brutas_root}")
|
||||
print(f"Arquivos review : {len(masks)}")
|
||||
print(f"Bases review : {len(reviews_by_base)}")
|
||||
print("=" * 96)
|
||||
print("=" * 100)
|
||||
|
||||
rows: List[dict] = []
|
||||
logical_reviews: List[LogicalReview] = []
|
||||
|
||||
not_found = 0
|
||||
duplicate_review_ok = 0
|
||||
duplicate_review_conflict = 0
|
||||
multi_target_bases = 0
|
||||
quarantine_bases = 0
|
||||
quarantine_pairs = 0
|
||||
quarantine_copy_errors = 0
|
||||
|
||||
q_multi_target = 0
|
||||
q_review_dup_equivalent = 0
|
||||
q_review_dup_conflict = 0
|
||||
q_mixed_equivalent = 0
|
||||
q_mixed_conflict = 0
|
||||
|
||||
total_target_files = 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PREFLIGHT LEVE
|
||||
# PREFLIGHT CONSERVADOR
|
||||
# ------------------------------------------------------------------
|
||||
for base in sorted(reviews_by_base):
|
||||
review_paths = reviews_by_base[base]
|
||||
|
|
@ -429,12 +538,11 @@ def main() -> int:
|
|||
targets = unique_targets_from_previews(previews, base)
|
||||
|
||||
groups_review = sorted({review_group(p) for p in review_paths})
|
||||
groups_preview = sorted(
|
||||
{p.parent.parent.name for p in previews}
|
||||
)
|
||||
groups_preview = sorted({preview_group(p) for p in previews})
|
||||
|
||||
row = {
|
||||
"status": "",
|
||||
"apply_eligible": "NAO",
|
||||
"base": base,
|
||||
"review_count": len(review_paths),
|
||||
"review_groups": " | ".join(groups_review),
|
||||
|
|
@ -444,53 +552,101 @@ def main() -> int:
|
|||
"previews": " | ".join(str(p) for p in previews),
|
||||
"target_count": len(targets),
|
||||
"targets": " | ".join(str(p) for p in targets),
|
||||
"quarantine_dir": "",
|
||||
"quarantine_pairs": 0,
|
||||
"message": "",
|
||||
}
|
||||
|
||||
if not targets:
|
||||
not_found += 1
|
||||
row["status"] = "NAO_ENCONTRADO"
|
||||
row["message"] = (
|
||||
"nenhum preview correspondente encontrado nas BRUTAS"
|
||||
)
|
||||
row["message"] = "nenhum preview correspondente encontrado nas BRUTAS"
|
||||
rows.append(row)
|
||||
continue
|
||||
|
||||
duplicate_equivalent = False
|
||||
review_duplicate = len(review_paths) > 1
|
||||
multi_target = len(targets) > 1
|
||||
|
||||
if len(review_paths) > 1:
|
||||
quarantine_status: Optional[str] = None
|
||||
quarantine_message = ""
|
||||
|
||||
if review_duplicate:
|
||||
equivalent, reason = duplicate_reviews_are_equivalent(
|
||||
review_paths,
|
||||
sizes,
|
||||
)
|
||||
if not equivalent:
|
||||
duplicate_review_conflict += 1
|
||||
row["status"] = "CONFLITO_REVIEW_DUPLICADO"
|
||||
row["message"] = reason
|
||||
|
||||
if multi_target:
|
||||
if equivalent:
|
||||
q_mixed_equivalent += 1
|
||||
quarantine_status = "QUARENTENA_DUPLICIDADE_MISTA_EQUIVALENTE"
|
||||
kind = "equivalentes"
|
||||
else:
|
||||
q_mixed_conflict += 1
|
||||
quarantine_status = "QUARENTENA_DUPLICIDADE_MISTA_CONFLITANTE"
|
||||
kind = "diferentes/conflitantes"
|
||||
quarantine_message = (
|
||||
f"base duplicado no REVIEW ({len(review_paths)} masks, {kind}) "
|
||||
f"e também nas BRUTAS ({len(targets)} targets). {reason}"
|
||||
)
|
||||
elif equivalent:
|
||||
q_review_dup_equivalent += 1
|
||||
quarantine_status = "QUARENTENA_REVIEW_DUPLICADO_EQUIVALENTE"
|
||||
quarantine_message = (
|
||||
f"mesmo base aparece {len(review_paths)} vezes no REVIEW; "
|
||||
"apesar de equivalente, política segura exige revisão manual. "
|
||||
f"{reason}"
|
||||
)
|
||||
else:
|
||||
q_review_dup_conflict += 1
|
||||
quarantine_status = "QUARENTENA_CONFLITO_REVIEW_DUPLICADO"
|
||||
quarantine_message = reason
|
||||
|
||||
elif multi_target:
|
||||
q_multi_target += 1
|
||||
quarantine_status = "QUARENTENA_MULTI_TARGET"
|
||||
quarantine_message = (
|
||||
f"mesmo base aponta para {len(targets)} destinos nas BRUTAS; "
|
||||
"nenhuma cópia será alterada automaticamente"
|
||||
)
|
||||
|
||||
if quarantine_status is not None:
|
||||
quarantine_bases += 1
|
||||
qres = quarantine_case(
|
||||
quarantine_root=quarantine_root,
|
||||
status=quarantine_status,
|
||||
base=base,
|
||||
review_paths=review_paths,
|
||||
previews=previews,
|
||||
targets=targets,
|
||||
message=quarantine_message,
|
||||
)
|
||||
|
||||
quarantine_pairs += qres.pair_count
|
||||
quarantine_copy_errors += len(qres.errors)
|
||||
|
||||
row["status"] = quarantine_status
|
||||
row["quarantine_dir"] = str(qres.case_dir)
|
||||
row["quarantine_pairs"] = qres.pair_count
|
||||
row["message"] = quarantine_message
|
||||
if qres.errors:
|
||||
row["message"] += " | ERROS_QUARENTENA: " + " || ".join(qres.errors)
|
||||
|
||||
rows.append(row)
|
||||
continue
|
||||
|
||||
duplicate_equivalent = True
|
||||
duplicate_review_ok += 1
|
||||
|
||||
if len(targets) > 1:
|
||||
multi_target_bases += 1
|
||||
row["status"] = "MULTI_TARGET"
|
||||
row["message"] = (
|
||||
f"mesma revisão será aplicada em {len(targets)} "
|
||||
"cópias das BRUTAS"
|
||||
# Só chega aqui quando é estritamente 1 review : 1 target.
|
||||
if len(review_paths) != 1 or len(targets) != 1:
|
||||
raise RuntimeError(
|
||||
f"invariante de segurança quebrada no preflight: {base} "
|
||||
f"review_count={len(review_paths)} target_count={len(targets)}"
|
||||
)
|
||||
elif duplicate_equivalent:
|
||||
row["status"] = "REVIEW_DUPLICADO_OK"
|
||||
row["message"] = (
|
||||
"mesmo base apareceu várias vezes no review, "
|
||||
"mas as masks são equivalentes"
|
||||
)
|
||||
else:
|
||||
row["status"] = "RESOLVIDO"
|
||||
|
||||
total_target_files += len(targets)
|
||||
row["status"] = "RESOLVIDO_1x1"
|
||||
row["apply_eligible"] = "SIM"
|
||||
row["message"] = "mapeamento estritamente 1 review : 1 target"
|
||||
|
||||
total_target_files += 1
|
||||
logical_reviews.append(
|
||||
LogicalReview(
|
||||
base=base,
|
||||
|
|
@ -498,114 +654,112 @@ def main() -> int:
|
|||
representative=review_paths[0],
|
||||
targets=targets,
|
||||
preview_paths=previews,
|
||||
duplicate_review_equivalent=duplicate_equivalent,
|
||||
)
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
print("[PREFLIGHT]")
|
||||
print(f" Bases prontas : {len(logical_reviews)}")
|
||||
print(f" Bases com múltiplos alvos : {multi_target_bases}")
|
||||
print(f" Review duplicado idêntico : {duplicate_review_ok}")
|
||||
print(f" Conflito review duplicado : {duplicate_review_conflict}")
|
||||
print(f" Bases elegíveis 1:1 : {len(logical_reviews)}")
|
||||
print(f" Bases em quarentena : {quarantine_bases}")
|
||||
print(f" Multi-target : {q_multi_target}")
|
||||
print(f" Review duplicado equivalente : {q_review_dup_equivalent}")
|
||||
print(f" Review duplicado conflitante : {q_review_dup_conflict}")
|
||||
print(f" Duplicidade mista equivalente : {q_mixed_equivalent}")
|
||||
print(f" Duplicidade mista conflitante : {q_mixed_conflict}")
|
||||
print(f" Duplas visuais geradas : {quarantine_pairs}")
|
||||
print(f" Erros ao copiar quarentena : {quarantine_copy_errors}")
|
||||
print(f" Não encontrados : {not_found}")
|
||||
print(f" Arquivos de mask a gravar : {total_target_files}")
|
||||
print(f" Arquivos que o apply pode gravar : {total_target_files}")
|
||||
|
||||
write_report(report_path, rows)
|
||||
|
||||
if quarantine_bases:
|
||||
print(f"[QUARENTENA] {quarantine_root}")
|
||||
|
||||
if not args.apply:
|
||||
print(f"[DRY-RUN] Relatório: {report_path}")
|
||||
print("[DRY-RUN] Nenhuma mask dentro das BRUTAS foi alterada.")
|
||||
print(
|
||||
"[DRY-RUN] Nenhuma mask das BRUTAS foi lida ou alterada."
|
||||
"[DRY-RUN] A quarentena contém somente CÓPIAS dos casos ambíguos "
|
||||
"para inspeção manual."
|
||||
)
|
||||
if quarantine_copy_errors:
|
||||
print(
|
||||
"[INFO] Bases com múltiplos previews NÃO são mais ambíguas: "
|
||||
"a mesma mask revisada será aplicada em todas as cópias."
|
||||
)
|
||||
if duplicate_review_conflict:
|
||||
print(
|
||||
"[ATENÇÃO] Existem conflitos reais entre masks duplicadas "
|
||||
"no próprio review. Esses bases serão pulados também no apply."
|
||||
"[ATENÇÃO] Houve erro ao montar parte da quarentena. "
|
||||
"Não rode --apply até revisar o relatório."
|
||||
)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
# Se a própria quarentena falhou, aborta o apply inteiro. Melhor não correr.
|
||||
if quarantine_copy_errors:
|
||||
print(
|
||||
"[ABORTADO] Houve falha ao copiar arquivos da quarentena. "
|
||||
"Nenhuma mask será aplicada."
|
||||
)
|
||||
return 2
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# APPLY
|
||||
# APPLY ESTRITAMENTE 1:1
|
||||
# ------------------------------------------------------------------
|
||||
applied_bases = 0
|
||||
applied_files = 0
|
||||
image_errors = 0
|
||||
|
||||
for i, item in enumerate(logical_reviews, 1):
|
||||
for item in logical_reviews:
|
||||
try:
|
||||
# Lê/redimensiona UMA vez por base lógico.
|
||||
# Segunda barreira de segurança antes de qualquer escrita.
|
||||
if len(item.review_masks) != 1 or len(item.targets) != 1:
|
||||
raise RuntimeError(
|
||||
"item ambíguo chegou ao APPLY; operação bloqueada "
|
||||
f"(reviews={len(item.review_masks)}, targets={len(item.targets)})"
|
||||
)
|
||||
|
||||
img = load_and_resize_mask(item.representative, sizes)
|
||||
|
||||
# Aplica em TODAS as cópias existentes das BRUTAS.
|
||||
for target in item.targets:
|
||||
target = item.targets[0]
|
||||
atomic_write_png(img, target)
|
||||
applied_files += 1
|
||||
|
||||
applied_files += 1
|
||||
applied_bases += 1
|
||||
|
||||
if (
|
||||
applied_bases <= 10
|
||||
or applied_bases % 100 == 0
|
||||
or len(item.targets) > 1
|
||||
):
|
||||
suffix = (
|
||||
f" -> {len(item.targets)} cópias"
|
||||
if len(item.targets) > 1
|
||||
else ""
|
||||
)
|
||||
if applied_bases <= 10 or applied_bases % 100 == 0:
|
||||
print(
|
||||
f"[{applied_bases}/{len(logical_reviews)}] "
|
||||
f"{item.base}{suffix}"
|
||||
f"{item.base} -> 1 target"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
image_errors += 1
|
||||
print(f"[ERRO] {item.base}: {e}")
|
||||
|
||||
# Atualiza a linha correspondente no relatório.
|
||||
for row in rows:
|
||||
if row["base"] == item.base:
|
||||
row["status"] = "ERRO_APPLY"
|
||||
row["apply_eligible"] = "NAO"
|
||||
row["message"] = str(e)
|
||||
break
|
||||
|
||||
# Atualiza status dos itens que chegaram ao fim sem erro.
|
||||
error_bases = {
|
||||
row["base"]
|
||||
for row in rows
|
||||
if row["status"] == "ERRO_APPLY"
|
||||
}
|
||||
|
||||
for row in rows:
|
||||
if (
|
||||
row["base"] not in error_bases
|
||||
and row["status"]
|
||||
in {"RESOLVIDO", "MULTI_TARGET", "REVIEW_DUPLICADO_OK"}
|
||||
):
|
||||
if row["base"] not in error_bases and row["status"] == "RESOLVIDO_1x1":
|
||||
row["status"] = "APLICADO"
|
||||
|
||||
write_report(report_path, rows)
|
||||
|
||||
print("=" * 96)
|
||||
print(f"[OK] Bases aplicadas : {applied_bases}")
|
||||
print("=" * 100)
|
||||
print(f"[OK] Bases aplicadas 1:1 : {applied_bases}")
|
||||
print(f"[OK] Arquivos gravados : {applied_files}")
|
||||
print(f"[SKIP] Bases em quarentena : {quarantine_bases}")
|
||||
print(f"[SKIP] Não encontrados : {not_found}")
|
||||
print(
|
||||
f"[SKIP] Conflito review duplic.: "
|
||||
f"{duplicate_review_conflict}"
|
||||
)
|
||||
print(f"[ERRO] Falhas de imagem/escrita: {image_errors}")
|
||||
print(f"[ERRO] Falhas imagem/escrita : {image_errors}")
|
||||
print(f"[OK] Relatório : {report_path}")
|
||||
|
||||
if duplicate_review_conflict:
|
||||
print(
|
||||
"[INFO] Conflitos de revisão duplicada foram deixados intactos "
|
||||
"para resolução manual."
|
||||
)
|
||||
if quarantine_bases:
|
||||
print(f"[OK] Quarentena : {quarantine_root}")
|
||||
|
||||
return 0 if image_errors == 0 else 2
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -54,6 +54,8 @@ python _7_split.py ^
|
|||
--synthetic-train-only ^
|
||||
--synthetic-respect-family-split ^
|
||||
--clear-dst
|
||||
|
||||
python .\_7_split.py --src-root dataset/960x600/group --train 0.85 --val 0.15 --test 0 --strategy agri_diverse --use-tensor-features --merge-near-duplicate-families --seed 42 --clear-dst
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"camera": "oak-fcc-3",
|
||||
"modelo": "segformer_b1",
|
||||
"model_name": "2026_09_08",
|
||||
"model_name": "2026_09_18",
|
||||
"main_class_name": "erva",
|
||||
"es_classes": "",
|
||||
"model_to_use": "geral",
|
||||
|
|
|
|||
Loading…
Reference in New Issue