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 readonly ucParametrosMapa _ucMapa;
|
||||||
|
|
||||||
private OperacaoModel operacaoParametrizando;
|
private OperacaoModel operacaoParametrizando;
|
||||||
|
private OperacaoModel _operacaoFonteCarregada;
|
||||||
|
private OperacaoParametrosModel _parametrosFonteCarregados;
|
||||||
|
private OperacaoParametrosControleModel _controleFonteCarregado;
|
||||||
|
|
||||||
public ucOperacaoParametros()
|
public ucOperacaoParametros()
|
||||||
{
|
{
|
||||||
|
|
@ -135,6 +138,11 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
private void DefinirOperacaoParametrizando(OperacaoModel operacao, bool recarregarMapa = true)
|
private void DefinirOperacaoParametrizando(OperacaoModel operacao, bool recarregarMapa = true)
|
||||||
{
|
{
|
||||||
operacaoParametrizando = operacao;
|
operacaoParametrizando = operacao;
|
||||||
|
|
||||||
|
_operacaoFonteCarregada = operacao;
|
||||||
|
_parametrosFonteCarregados = operacao?.Parametros;
|
||||||
|
_controleFonteCarregado = operacao?.Parametros?.Controle;
|
||||||
|
|
||||||
CarregarDadosGerais();
|
CarregarDadosGerais();
|
||||||
_ucMovimento.DefinirOperacao(operacaoParametrizando);
|
_ucMovimento.DefinirOperacao(operacaoParametrizando);
|
||||||
_ucDirecional.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()
|
public void AtualizarDadosTela()
|
||||||
{
|
{
|
||||||
|
SincronizarComOperacaoAtualSeNecessario();
|
||||||
operacaoParametrizando?.Mapa?.AtualizarInteratividadeMapaAsync(parametrizacao: true);
|
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;
|
break;
|
||||||
case AbaOperacaoIHM.Parametros:
|
case AbaOperacaoIHM.Parametros:
|
||||||
lblContextoAba.Text = "Parâmetros da operação atual e configuração local";
|
lblContextoAba.Text = "Parâmetros da operação atual e configuração local";
|
||||||
|
_ucParametros.SincronizarComOperacaoAtualSeNecessario();
|
||||||
ExibirConteudo(_ucParametros);
|
ExibirConteudo(_ucParametros);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,12 @@ namespace AgroBase.Forms
|
||||||
|
|
||||||
private double velocidadeCarroMs;
|
private double velocidadeCarroMs;
|
||||||
private double _ultimoAnguloControleAplicado;
|
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();
|
private GPSModel posicaoAtual = new GPSModel();
|
||||||
|
|
||||||
// Mantida para compatibilidade e para os dados apresentados na tela.
|
// Mantida para compatibilidade e para os dados apresentados na tela.
|
||||||
|
|
@ -398,11 +404,26 @@ namespace AgroBase.Forms
|
||||||
|
|
||||||
if (atualizarAnguloControle)
|
if (atualizarAnguloControle)
|
||||||
{
|
{
|
||||||
anguloControle = CalcularAnguloControleAtrasado(
|
_ultimoAnguloControleAlvoAtrasado = CalcularAnguloControleAtrasado(
|
||||||
Volatile.Read(ref _delayComandoPassos)
|
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();
|
GPSModel gps = GPSService.GetSnapshot();
|
||||||
|
|
||||||
double anguloAtual =
|
double anguloAtual =
|
||||||
|
|
@ -1614,7 +1635,10 @@ namespace AgroBase.Forms
|
||||||
);
|
);
|
||||||
|
|
||||||
lock (_estadoSimulacaoLock)
|
lock (_estadoSimulacaoLock)
|
||||||
|
{
|
||||||
|
_ultimoAnguloControleAlvoAtrasado = angulo;
|
||||||
_ultimoAnguloControleAplicado = angulo;
|
_ultimoAnguloControleAplicado = angulo;
|
||||||
|
}
|
||||||
|
|
||||||
txtAnguloControle.Text = angulo.ToString("0.00");
|
txtAnguloControle.Text = angulo.ToString("0.00");
|
||||||
}
|
}
|
||||||
|
|
@ -1638,9 +1662,26 @@ namespace AgroBase.Forms
|
||||||
historicoAngulosControle.Dequeue();
|
historicoAngulosControle.Dequeue();
|
||||||
}
|
}
|
||||||
|
|
||||||
_ultimoAnguloControleAplicado =
|
return historicoAngulosControle.Peek();
|
||||||
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;
|
return _ultimoAnguloControleAplicado;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,11 @@ namespace AgroBase.Models.Modules
|
||||||
private DateTime _ultimaDescobertaEnviada = DateTime.MinValue;
|
private DateTime _ultimaDescobertaEnviada = DateTime.MinValue;
|
||||||
private readonly object _estadoLock = new object();
|
private readonly object _estadoLock = new object();
|
||||||
private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly SemaphoreSlim _alivioPressaoGate = new SemaphoreSlim(1, 1);
|
||||||
private readonly object _calibragemCtsLock = new object();
|
private readonly object _calibragemCtsLock = new object();
|
||||||
private CancellationTokenSource _calibragemCts;
|
private CancellationTokenSource _calibragemCts;
|
||||||
private bool _sincronizacaoSeguraPendente;
|
private bool _sincronizacaoSeguraPendente;
|
||||||
|
private volatile bool _alivioPressaoLinhaEmAndamento;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
internal object EstadoSyncRoot => _estadoLock;
|
internal object EstadoSyncRoot => _estadoLock;
|
||||||
|
|
@ -1817,6 +1819,15 @@ namespace AgroBase.Models.Modules
|
||||||
if (op?.DispAtu?.Dados?.Conectado != true)
|
if (op?.DispAtu?.Dados?.Conectado != true)
|
||||||
return;
|
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 operacaoConcluida = op.Sensoriamento?.Operacao?.StatusOperacaoAtual == StatusOperacao.Concluido;
|
||||||
|
|
||||||
bool pulverizadorAutomatico = op.Parametros?.Controle?.PulverizadorAutomatico ?? false;
|
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))
|
foreach (var bomba in BombasPressurizadoras.Where(x => x != null && x.Inicializado))
|
||||||
{
|
{
|
||||||
bool ehBombaPrincipal = bomba.ID == BombaPressurizadora?.ID;
|
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;
|
bool comandoSolicitado = bomba.Comandar && bomba.ComandoAtuar;
|
||||||
|
|
||||||
|
|
@ -1914,6 +1929,21 @@ namespace AgroBase.Models.Modules
|
||||||
{
|
{
|
||||||
statusDesejado = comandoSolicitado && atuacaoPermitida;
|
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
|
* Fora da calibração, a bomba principal automática
|
||||||
* deve permanecer ligada enquanto a operação estiver
|
* deve permanecer ligada enquanto a operação estiver
|
||||||
|
|
@ -2070,7 +2100,10 @@ namespace AgroBase.Models.Modules
|
||||||
return false;
|
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;
|
var dados = Variaveis.OperacaoEmAndamento?.DispAtu?.Dados;
|
||||||
|
|
||||||
|
|
@ -2084,6 +2117,24 @@ namespace AgroBase.Models.Modules
|
||||||
if (!BloqueioAbsolutoAtuacao(out string motivo))
|
if (!BloqueioAbsolutoAtuacao(out string motivo))
|
||||||
return true;
|
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}");
|
Variaveis.MostrarLog($"[ATU/SEGURANCA] Comando ON bloqueado para {componente}: {motivo}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -2108,12 +2159,13 @@ namespace AgroBase.Models.Modules
|
||||||
CanMessagePosicaoDados dado,
|
CanMessagePosicaoDados dado,
|
||||||
bool setarControle,
|
bool setarControle,
|
||||||
bool status,
|
bool status,
|
||||||
double? angulo = null
|
double? angulo = null,
|
||||||
|
bool permitirDuranteFinalizacao = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var dispAtu = Variaveis.OperacaoEmAndamento?.DispAtu;
|
var dispAtu = Variaveis.OperacaoEmAndamento?.DispAtu;
|
||||||
|
|
||||||
if (bico == null || !PodeEnviarComandoFisico(status, bico.ID))
|
if (bico == null || !PodeEnviarComandoFisico(status, bico.ID, permitirDuranteFinalizacao))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool comandoControle = bico.ComandoAtuar;
|
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()
|
private void AtualizaComponentesIniciados()
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,38 @@ namespace AgroBase.Models.Modules
|
||||||
public Dictionary<TipoMovimentoDirecional, ConfiguracaoSentidoMotor> ConfigSentidos { get; set; }
|
public Dictionary<TipoMovimentoDirecional, ConfiguracaoSentidoMotor> ConfigSentidos { get; set; }
|
||||||
public List<FuncoesPinout> Funcoes { 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)
|
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 readonly SemaphoreSlim _gateMovimentoCombinado = new SemaphoreSlim(1, 1);
|
||||||
private CancellationTokenSource _ctsMovimentoCombinado;
|
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>();
|
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)
|
public (bool, List<string>) StatusModulosMOV(Direcao Lado)
|
||||||
{
|
{
|
||||||
string Prefixo = Lado == Direcao.Esquerda ? "E" : Lado == Direcao.Direita ? "D" : "";
|
string Prefixo = Lado == Direcao.Esquerda ? "E" : Lado == Direcao.Direita ? "D" : "";
|
||||||
|
|
|
||||||
|
|
@ -1451,6 +1451,14 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
op.Sensoriamento.Operacao.Finalizando = true;
|
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();
|
op.Trajetoria?.LoopAtualizaDados();
|
||||||
|
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
|
|
@ -2664,6 +2672,16 @@ namespace AgroBase.Models
|
||||||
op?.DispAtu?.Dados?.PrepararAtuadorParaOperacao();
|
op?.DispAtu?.Dados?.PrepararAtuadorParaOperacao();
|
||||||
|
|
||||||
HealthWorkerService.AtualizarDadosOperacao(true);
|
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)
|
private void ExecutarLiberacaoHumanaAutonomia(OperacaoComandoBaseModel controleBase, OperacaoModel op)
|
||||||
|
|
@ -2943,6 +2961,12 @@ namespace AgroBase.Models
|
||||||
case S_Code.sBMB:
|
case S_Code.sBMB:
|
||||||
var bomba = dadosAtu.BombasPressurizadoras?.FirstOrDefault(x => x.ID == controleBase._comp_id);
|
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)
|
switch (controleBase.Controle.Comando)
|
||||||
{
|
{
|
||||||
case CanMessagePosicaoDados.Command2:
|
case CanMessagePosicaoDados.Command2:
|
||||||
|
|
@ -2960,8 +2984,31 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
bool ligarBomba = controleBase.Controle.Estado == true;
|
bool ligarBomba = controleBase.Controle.Estado == true;
|
||||||
|
|
||||||
if (bomba.ID == dadosAtu?.AgitadorDeCalda?.ID)
|
bool ehAgitador = bomba.ID == dadosAtu?.AgitadorDeCalda?.ID;
|
||||||
op.Parametros.Controle.AtuAgitadorModo = ligarBomba ? ModoAgitadorCalda.Manual : ModoAgitadorCalda.SemAgitacao;
|
|
||||||
|
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(
|
dadosAtu?.EnviarComandoFisicoBomba(
|
||||||
bomba,
|
bomba,
|
||||||
|
|
@ -2970,7 +3017,9 @@ namespace AgroBase.Models
|
||||||
status: ligarBomba,
|
status: ligarBomba,
|
||||||
potencia: controleBase.Controle.Potencia,
|
potencia: controleBase.Controle.Potencia,
|
||||||
pressao: controleBase.Controle.Pressao,
|
pressao: controleBase.Controle.Pressao,
|
||||||
modo: op.Parametros.Controle.AtuModoControle
|
modo: ehAgitador
|
||||||
|
? bomba.Controle.Modo
|
||||||
|
: op.Parametros?.Controle?.AtuModoControle
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1021,6 +1021,7 @@ namespace AgroBase.Models
|
||||||
public static double DistanciaEntreEixosCm { get; } = 92.0;
|
public static double DistanciaEntreEixosCm { get; } = 92.0;
|
||||||
public static double BitolaRodasCm { get; } = 75.0;
|
public static double BitolaRodasCm { get; } = 75.0;
|
||||||
public static double KuDirecional { get; } = 2.65;
|
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 LeverArmFrontalCm { get; set; } = 37.0; // cm
|
||||||
public static double LeverArmLateralCm { get; set; } = 0.0; // cm
|
public static double LeverArmLateralCm { get; set; } = 0.0; // cm
|
||||||
public static double LarguraEquipamentoMm
|
public static double LarguraEquipamentoMm
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,13 @@ namespace AgroBase.Services.Operadores
|
||||||
|
|
||||||
bool carroEmMovimento = comandoMovimentoAtivo && velocidadeFisicaAtiva;
|
bool carroEmMovimento = comandoMovimentoAtivo && velocidadeFisicaAtiva;
|
||||||
|
|
||||||
|
var estadoDirecional = op.DispMvd?.Dados?
|
||||||
|
.ObterEstadoDirecionalCanonico(
|
||||||
|
op.Controle.TipoMovimento,
|
||||||
|
op.Controle.Angulo,
|
||||||
|
op.Simulando
|
||||||
|
);
|
||||||
|
|
||||||
var bombaLinha = op?.DispAtu?.Dados?.BombaPressurizadora;
|
var bombaLinha = op?.DispAtu?.Dados?.BombaPressurizadora;
|
||||||
double pressaoAlvo = bombaLinha?._PressaoSPControle > 0 ? bombaLinha._PressaoSPControle : op.Parametros?.Controle?.AtuPressaoLinha ?? 0;
|
double pressaoAlvo = bombaLinha?._PressaoSPControle > 0 ? bombaLinha._PressaoSPControle : op.Parametros?.Controle?.AtuPressaoLinha ?? 0;
|
||||||
double pressaoLinha = bombaLinha?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0;
|
double pressaoLinha = bombaLinha?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0;
|
||||||
|
|
@ -252,6 +259,19 @@ namespace AgroBase.Services.Operadores
|
||||||
percentual_bateria = _Sensoriamento.Bateria?.PercentualBateria ?? 0,
|
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
|
("Atuador", new
|
||||||
{
|
{
|
||||||
pressao_linha_regular = pressaoLinhaRegular,
|
pressao_linha_regular = pressaoLinhaRegular,
|
||||||
|
|
@ -427,6 +447,7 @@ namespace AgroBase.Services.Operadores
|
||||||
tracking_row_hybrid_diagonal_enabled = true,
|
tracking_row_hybrid_diagonal_enabled = true,
|
||||||
entry_capture_enabled = false,
|
entry_capture_enabled = false,
|
||||||
tracking_axis_debug = 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,
|
parada_necessaria=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
comando_anterior = _ler_comando_anterior_para_mpc()
|
comando_anterior = _ler_comando_anterior_para_mpc(contexto)
|
||||||
|
|
||||||
inicio = time.perf_counter()
|
inicio = time.perf_counter()
|
||||||
comando = mpc.compute_receding(contexto, comando_anterior)
|
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):
|
def _montar_contexto_direcional(*, operacao, controle, contexto_global, equipamento):
|
||||||
trajetoria = _dict(contexto_global.get("Trajetoria", {}))
|
trajetoria = _dict(contexto_global.get("Trajetoria", {}))
|
||||||
gerais = _dict(contexto_global.get("Gerais", {}))
|
gerais = _dict(contexto_global.get("Gerais", {}))
|
||||||
|
direcional_fisico = _dict(contexto_global.get("Direcional", {}))
|
||||||
gps_info = _montar_contexto_gps(operacao)
|
gps_info = _montar_contexto_gps(operacao)
|
||||||
imu_contexto = _montar_contexto_imu_direcional(_dict(ContextoGlobalRedis.get_modulo(T_Code.Imu)))
|
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,
|
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,
|
"VisualWorker": visual_worker,
|
||||||
"IMU": imu_contexto,
|
"IMU": imu_contexto,
|
||||||
"Equipamento": {
|
"Equipamento": {
|
||||||
|
|
@ -1125,10 +1158,16 @@ def _montar_comando_retorno(comando, latencia=-1.0):
|
||||||
return comando
|
return comando
|
||||||
|
|
||||||
|
|
||||||
def _ler_comando_anterior_para_mpc():
|
def _ler_comando_anterior_para_mpc(contexto=None):
|
||||||
controle = _dict(ContextoGlobalRedis.get_controle())
|
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(
|
"angulo": _float(
|
||||||
controle.get("angulo_sp_ant", controle.get("angulo_sp", 0.0)),
|
controle.get("angulo_sp_ant", controle.get("angulo_sp", 0.0)),
|
||||||
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
|
# Helpers de matriz
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,27 @@ class ControladorMPC:
|
||||||
1.0, min_value=0.25, max_value=2.0,
|
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.
|
# V18 - handoff antecipado para o Pure Pursuit.
|
||||||
#
|
#
|
||||||
# SaindoRua ja nasce alguns metros antes da BordaSaida para reduzir a
|
# 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),
|
parametros_mpc.get("cinematica_entre_eixos_m", 0.94),
|
||||||
0.94, min_value=0.20, max_value=4.0,
|
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
|
# V14 - CURVE TRACK: yaw contínuo quando a própria rua está curvando
|
||||||
|
|
@ -1571,14 +1598,93 @@ class ControladorMPC:
|
||||||
|
|
||||||
return False
|
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(
|
def _comando_seguidor_dubins(
|
||||||
self, contexto, comando_anterior, x, y, theta, velocidade, status_carro,
|
self, contexto, comando_anterior, x, y, theta, velocidade, status_carro,
|
||||||
idx_alvo_correcao, pos_lat, pos_lon, pos_theta, pos_latencia, ponto_alvo_real
|
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
|
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(
|
preview_xy = self._simular_preview_seguidor_dubins(
|
||||||
x, y, theta, idx_alvo_correcao, velocidade, contexto,
|
x, y, theta, idx_alvo_correcao, velocidade, contexto,
|
||||||
self.tempo_execucao_local,
|
self.tempo_execucao_local,
|
||||||
|
|
@ -1620,7 +1726,7 @@ class ControladorMPC:
|
||||||
"debug_seguidor_dubins": {
|
"debug_seguidor_dubins": {
|
||||||
"modo": (
|
"modo": (
|
||||||
"pure_pursuit_4ws_antecipado"
|
"pure_pursuit_4ws_antecipado"
|
||||||
if _status_in(status_carro, [StatusCarroMapa.SaindoRua])
|
if pp_antecipado
|
||||||
else "pure_pursuit_4ws"
|
else "pure_pursuit_4ws"
|
||||||
),
|
),
|
||||||
"lookahead_m": float(dbg["lookahead_m"]),
|
"lookahead_m": float(dbg["lookahead_m"]),
|
||||||
|
|
@ -1628,7 +1734,9 @@ class ControladorMPC:
|
||||||
"alpha_graus": float(math.degrees(dbg["alpha"])),
|
"alpha_graus": float(math.degrees(dbg["alpha"])),
|
||||||
"kappa_pp": float(dbg["kappa_pp"]),
|
"kappa_pp": float(dbg["kappa_pp"]),
|
||||||
"kappa_cmd": float(dbg["kappa_cmd"]),
|
"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,
|
"candidatos_testados": 0,
|
||||||
"motivos": [],
|
"motivos": [],
|
||||||
|
|
@ -2272,6 +2380,13 @@ class ControladorMPC:
|
||||||
)["omega"]
|
)["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
|
@staticmethod
|
||||||
def _posicoes_eixos_tracking(x, y, theta, entre_eixos):
|
def _posicoes_eixos_tracking(x, y, theta, entre_eixos):
|
||||||
"""Retorna centros dos eixos dianteiro/traseiro a partir do centro.
|
"""Retorna centros dos eixos dianteiro/traseiro a partir do centro.
|
||||||
|
|
@ -3921,6 +4036,9 @@ class ControladorMPC:
|
||||||
self.dt = old_dt
|
self.dt = old_dt
|
||||||
|
|
||||||
# --- integra por tempo, quebrando cada trecho em subpassos ~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
|
t_rem = latency_s
|
||||||
for dur_s, u in cmd_seq:
|
for dur_s, u in cmd_seq:
|
||||||
if t_rem <= 0.0:
|
if t_rem <= 0.0:
|
||||||
|
|
@ -3935,7 +4053,18 @@ class ControladorMPC:
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
if time_left_ms() <= 0.0:
|
if time_left_ms() <= 0.0:
|
||||||
return x, y, theta, visitados
|
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(
|
idx_alvo = self._corrigir_pontos_visitados(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
@ -4190,19 +4319,31 @@ class ControladorMPC:
|
||||||
# -------------------- Correção por latência (vida real) --------------------
|
# -------------------- Correção por latência (vida real) --------------------
|
||||||
# Mantém correção com 'self.dt' e 'velocidade' REAIS
|
# Mantém correção com 'self.dt' e 'velocidade' REAIS
|
||||||
u_hold = {
|
u_hold = {
|
||||||
"tipo": comando_anterior.get("tipo"),
|
"tipo": comando_anterior.get(
|
||||||
"angulo": np.radians(comando_anterior.get("angulo", 0.0)),
|
"tipo_aplicado", comando_anterior.get("tipo")
|
||||||
"v": velocidade,
|
),
|
||||||
"omega": self._calcular_omega_4ws(
|
"angulo": np.radians(
|
||||||
velocidade,
|
comando_anterior.get(
|
||||||
np.radians(comando_anterior.get("angulo", 0.0)),
|
"angulo_sp_aplicado",
|
||||||
comando_anterior.get("tipo")
|
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
|
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 = self.corrigir_pose_por_latencia(
|
||||||
x, y, theta, self.visitados_execucao,
|
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,
|
dt=self.dt,
|
||||||
nova_posicao_fn=self._nova_posicao,
|
nova_posicao_fn=self._nova_posicao,
|
||||||
u_hold=u_hold,
|
u_hold=u_hold,
|
||||||
|
|
@ -4210,6 +4351,19 @@ class ControladorMPC:
|
||||||
time_left_ms=lambda: (deadline - now()) * 1000.0,
|
time_left_ms=lambda: (deadline - now()) * 1000.0,
|
||||||
calc_omega_fn=self._calcular_omega_4ws
|
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(
|
idx_alvo_correcao = self._corrigir_pontos_visitados(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
@ -4407,6 +4561,12 @@ class ControladorMPC:
|
||||||
"x": x, "y": y, "theta": theta,
|
"x": x, "y": y, "theta": theta,
|
||||||
"custo": 0.0,
|
"custo": 0.0,
|
||||||
"comandos": [(tipo_anterior, angulo_anterior)],
|
"comandos": [(tipo_anterior, angulo_anterior)],
|
||||||
|
"angulo_atuador": np.radians(
|
||||||
|
comando_anterior.get(
|
||||||
|
"angulo_aplicado",
|
||||||
|
comando_anterior.get("angulo", 0.0),
|
||||||
|
)
|
||||||
|
),
|
||||||
"trajetoria": [],
|
"trajetoria": [],
|
||||||
"visitados": self.visitados_execucao,
|
"visitados": self.visitados_execucao,
|
||||||
"inicial": True
|
"inicial": True
|
||||||
|
|
@ -4433,6 +4593,10 @@ class ControladorMPC:
|
||||||
cmd_anterior_local = {
|
cmd_anterior_local = {
|
||||||
"tipo": candidato["comandos"][-1][0],
|
"tipo": candidato["comandos"][-1][0],
|
||||||
"angulo": candidato["comandos"][-1][1],
|
"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]
|
#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()
|
t_c_ini = now()
|
||||||
try:
|
try:
|
||||||
# >>> CHANGED: passa dt_pred e v_sim para a simulação de um PASSO
|
# >>> 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,
|
x_atual, y_atual, theta_atual,
|
||||||
tipo_k, ang_k,
|
tipo_k, ang_k,
|
||||||
custos_candidatos,
|
custos_candidatos,
|
||||||
|
|
@ -4543,6 +4707,7 @@ class ControladorMPC:
|
||||||
"x": x_f, "y": y_f, "theta": theta_f,
|
"x": x_f, "y": y_f, "theta": theta_f,
|
||||||
"custo": candidato["custo"] + float(custo),
|
"custo": candidato["custo"] + float(custo),
|
||||||
"comandos": candidato["comandos"] + [(tipo_k, ang_k)],
|
"comandos": candidato["comandos"] + [(tipo_k, ang_k)],
|
||||||
|
"angulo_atuador": angulo_atuador_final,
|
||||||
"trajetoria": candidato["trajetoria"] + sim,
|
"trajetoria": candidato["trajetoria"] + sim,
|
||||||
"visitados": visitados_sim,
|
"visitados": visitados_sim,
|
||||||
"inicial": False
|
"inicial": False
|
||||||
|
|
@ -5789,6 +5954,9 @@ class ControladorMPC:
|
||||||
simulacoes = []
|
simulacoes = []
|
||||||
prev_ang = float(comando_anterior.get("angulo", 0.0))
|
prev_ang = float(comando_anterior.get("angulo", 0.0))
|
||||||
prev_tipo = comando_anterior.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value)
|
prev_tipo = comando_anterior.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value)
|
||||||
|
angulo_atuador = float(
|
||||||
|
comando_anterior.get("angulo_aplicado", prev_ang)
|
||||||
|
)
|
||||||
pontos_visitados = visitados.copy()
|
pontos_visitados = visitados.copy()
|
||||||
|
|
||||||
# chave para custo heurístico vindo do VisualWorker (por ângulo/tipo)
|
# chave para custo heurístico vindo do VisualWorker (por ângulo/tipo)
|
||||||
|
|
@ -5822,8 +5990,6 @@ class ControladorMPC:
|
||||||
custo_visual_worker = 0.0
|
custo_visual_worker = 0.0
|
||||||
|
|
||||||
# omega e sub-stepping
|
# 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)))
|
#passos_local = max(1, int(getattr(self, "passos_horizonte_local", 1)))
|
||||||
#dt_total = float(dt_pred) * passos_local
|
#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)
|
#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
|
side_mem_local = 0
|
||||||
|
|
||||||
for n_sub in range(n_subs):
|
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
|
# IMPORTANTE: _nova_posicao precisa aceitar dt opcional
|
||||||
x_sim, y_sim, theta_sim = self._nova_posicao(
|
x_sim, y_sim, theta_sim = self._nova_posicao(
|
||||||
x_sim, y_sim, theta_sim,
|
x_sim, y_sim, theta_sim,
|
||||||
omega_const, v_planejado, # (omega, v, tipo, angulo)
|
omega_aplicado, v_planejado,
|
||||||
tipo, angulo_testado,
|
tipo, angulo_atuador,
|
||||||
dt=sub_dt
|
dt=sub_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -6004,7 +6180,7 @@ class ControladorMPC:
|
||||||
|
|
||||||
#print(
|
#print(
|
||||||
# f"ang:{np.degrees(angulo_testado):.2f} "
|
# 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"xsim:{finais[0]:.5f} ysim:{finais[1]:.5f} tsim:{finais[2]:.5f} "
|
||||||
# f"xalv:{finais[3]:.5f} yalv:{finais[4]:.5f} "
|
# f"xalv:{finais[3]:.5f} yalv:{finais[4]:.5f} "
|
||||||
# f"epos:{somas[5]:.4f} eposn:{somas[6]:.4f} "
|
# f"epos:{somas[5]:.4f} eposn:{somas[6]:.4f} "
|
||||||
|
|
@ -6021,7 +6197,7 @@ class ControladorMPC:
|
||||||
debug_custo = {
|
debug_custo = {
|
||||||
"angulo": np.degrees(angulo_testado),
|
"angulo": np.degrees(angulo_testado),
|
||||||
"tipo": tipo.value,
|
"tipo": tipo.value,
|
||||||
"omega": omega_const,
|
"omega": omega_aplicado,
|
||||||
"x_sim": finais[0],
|
"x_sim": finais[0],
|
||||||
"y_sim": finais[1],
|
"y_sim": finais[1],
|
||||||
"theta_sim": finais[2],
|
"theta_sim": finais[2],
|
||||||
|
|
@ -6057,11 +6233,21 @@ class ControladorMPC:
|
||||||
}
|
}
|
||||||
#print(custo_visual_worker)
|
#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:
|
except Exception as e:
|
||||||
mostrar_log(f"Erro ao simular passo para {tipo.name} | angulo: {np.degrees(angulo_testado):.2f}: {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):
|
def _nova_posicao(self, x, y, theta, omega, velocidade, tipo, angulo_rad, dt=None):
|
||||||
"""V16: integra UM passo com a cinemática 4WS unificada.
|
"""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:
|
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 --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 --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_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
|
from __future__ import annotations
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,56 +2,63 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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:
|
Quarentena:
|
||||||
- O nome-base da máscara revisada é a chave.
|
- É gerada também no dry-run (somente cópias; nada nas BRUTAS é alterado).
|
||||||
- Os previews das BRUTAS são indexados UMA ÚNICA VEZ em memória.
|
- Para cada caso ambíguo é criada uma pasta por combinação review x preview.
|
||||||
- Se o mesmo base existir em MAIS DE UM grupo das BRUTAS, a máscara revisada
|
- Cada pasta contém:
|
||||||
é aplicada em TODOS eles. Isso é intencional: são duplicatas que serão
|
preview.<ext> -> preview original das BRUTAS
|
||||||
consolidadas posteriormente pelo reorganizador do dataset.
|
mask.png -> máscara original vinda do REVIEW
|
||||||
- Se o mesmo base existir MAIS DE UMA VEZ dentro do próprio REVIEW:
|
info.txt -> origem exata, grupos e target correspondente
|
||||||
* 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.
|
|
||||||
|
|
||||||
Transformação:
|
Exemplo:
|
||||||
- config["resolucao"] = resolução do review/treino, ex. 960x600
|
dataset/brutas/review_quarantine/run_YYYYMMDD_HHMMSS/
|
||||||
- config["raw_size"] = resolução RAW, ex. 1280x800
|
QUARENTENA_MULTI_TARGET/
|
||||||
- Máscaras categóricas são redimensionadas EXCLUSIVAMENTE com INTER_NEAREST.
|
<base>/
|
||||||
|
pair_001__review-X__preview-A/
|
||||||
|
preview.jpg
|
||||||
|
mask.png
|
||||||
|
info.txt
|
||||||
|
pair_002__review-X__preview-B/
|
||||||
|
...
|
||||||
|
|
||||||
Performance:
|
Uso dry-run:
|
||||||
- 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:
|
python .\\_2_apply_review_masks.py `
|
||||||
|
|
||||||
python .\\_2_apply_review_masks_final.py `
|
|
||||||
--review-root "C:\\Users\\USER\\Desktop\\fotos_multiespectrais\\review" `
|
--review-root "C:\\Users\\USER\\Desktop\\fotos_multiespectrais\\review" `
|
||||||
--brutas-root dataset\\brutas\\group `
|
--brutas-root dataset\\brutas\\group `
|
||||||
--labelmap dataset\\labelmap.txt `
|
--labelmap dataset\\labelmap.txt `
|
||||||
--config config.json
|
--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" `
|
--review-root "C:\\Users\\USER\\Desktop\\fotos_multiespectrais\\review" `
|
||||||
--brutas-root dataset\\brutas\\group `
|
--brutas-root dataset\\brutas\\group `
|
||||||
--labelmap dataset\\labelmap.txt `
|
--labelmap dataset\\labelmap.txt `
|
||||||
--config config.json `
|
--config config.json `
|
||||||
--apply
|
--apply
|
||||||
|
|
||||||
|
Opcional:
|
||||||
|
--quarantine-root caminho\\para\\quarentena
|
||||||
|
|
||||||
Observação:
|
Observação:
|
||||||
--labelmap é aceito apenas para manter compatibilidade com o comando antigo.
|
--labelmap é aceito apenas para compatibilidade com o comando antigo.
|
||||||
Esta versão não precisa dele.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -60,12 +67,14 @@ import argparse
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List, Tuple
|
from typing import Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -89,7 +98,13 @@ class LogicalReview:
|
||||||
representative: Path
|
representative: Path
|
||||||
targets: List[Path]
|
targets: List[Path]
|
||||||
preview_paths: 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:
|
def load_sizes(config_path: Path) -> Sizes:
|
||||||
|
|
@ -111,9 +126,8 @@ def load_sizes(config_path: Path) -> Sizes:
|
||||||
try:
|
try:
|
||||||
w, h = int(value[0]), int(value[1])
|
w, h = int(value[0]), int(value[1])
|
||||||
except Exception:
|
except Exception:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f'config["{key}"] inválido: {value!r}')
|
||||||
f'config["{key}"] inválido: {value!r}'
|
|
||||||
)
|
|
||||||
if w <= 0 or h <= 0:
|
if w <= 0 or h <= 0:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'config["{key}"] deve conter valores positivos: {value!r}'
|
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")
|
review_w, review_h = parse("resolucao")
|
||||||
raw_w, raw_h = parse("raw_size")
|
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:
|
if review_w * raw_h != raw_w * review_h:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"resolucao e raw_size possuem aspect ratios diferentes: "
|
"resolucao e raw_size possuem aspect ratios diferentes: "
|
||||||
|
|
@ -146,8 +159,7 @@ def discover_review_masks(review_root: Path) -> List[Path]:
|
||||||
e:
|
e:
|
||||||
review/<grupo>/final_masks/<base>.png
|
review/<grupo>/final_masks/<base>.png
|
||||||
|
|
||||||
Se existir final_masks, usa somente esses arquivos para não engolir
|
Se existir qualquer final_masks, usa somente arquivos dentro de final_masks.
|
||||||
predictions/previews auxiliares.
|
|
||||||
"""
|
"""
|
||||||
final_masks = sorted(
|
final_masks = sorted(
|
||||||
p for p in review_root.rglob("*.png")
|
p for p in review_root.rglob("*.png")
|
||||||
|
|
@ -158,9 +170,7 @@ def discover_review_masks(review_root: Path) -> List[Path]:
|
||||||
|
|
||||||
masks: List[Path] = []
|
masks: List[Path] = []
|
||||||
for group_dir in sorted(p for p in review_root.iterdir() if p.is_dir()):
|
for group_dir in sorted(p for p in review_root.iterdir() if p.is_dir()):
|
||||||
masks.extend(
|
masks.extend(sorted(p for p in group_dir.glob("*.png") if p.is_file()))
|
||||||
sorted(p for p in group_dir.glob("*.png") if p.is_file())
|
|
||||||
)
|
|
||||||
return masks
|
return masks
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -170,13 +180,12 @@ def review_group(path: Path) -> str:
|
||||||
return path.parent.name
|
return path.parent.name
|
||||||
|
|
||||||
|
|
||||||
def build_preview_index(brutas_root: Path) -> Dict[str, List[Path]]:
|
def preview_group(path: Path) -> str:
|
||||||
"""
|
# .../<grupo>/previews/<base>.<ext>
|
||||||
Varre o filesystem UMA ÚNICA VEZ.
|
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)
|
index: Dict[str, List[Path]] = defaultdict(list)
|
||||||
|
|
||||||
for group_dir in sorted(p for p in brutas_root.iterdir() if p.is_dir()):
|
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:
|
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)
|
img = cv2.imread(str(path), cv2.IMREAD_COLOR)
|
||||||
if img is None:
|
if img is None:
|
||||||
raise RuntimeError(f"não consegui abrir máscara: {path}")
|
raise RuntimeError(f"não consegui abrir máscara: {path}")
|
||||||
|
|
@ -236,9 +236,8 @@ def duplicate_reviews_are_equivalent(
|
||||||
sizes: Sizes,
|
sizes: Sizes,
|
||||||
) -> Tuple[bool, str]:
|
) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Usado SOMENTE quando o mesmo base aparece várias vezes no review.
|
A equivalência serve APENAS para diagnóstico/relatório.
|
||||||
Como são pouquíssimos casos, podemos comparar pixel a pixel sem afetar
|
Mesmo equivalentes, reviews duplicados continuam indo para quarentena.
|
||||||
a performance geral.
|
|
||||||
"""
|
"""
|
||||||
if len(paths) <= 1:
|
if len(paths) <= 1:
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
@ -261,17 +260,10 @@ def duplicate_reviews_are_equivalent(
|
||||||
+ " | ".join(str(p) for p in paths),
|
+ " | ".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(
|
def unique_targets_from_previews(previews: List[Path], base: str) -> List[Path]:
|
||||||
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.
|
|
||||||
"""
|
|
||||||
by_target: Dict[str, Path] = {}
|
by_target: Dict[str, Path] = {}
|
||||||
|
|
||||||
for preview in previews:
|
for preview in previews:
|
||||||
|
|
@ -282,12 +274,7 @@ def unique_targets_from_previews(
|
||||||
|
|
||||||
|
|
||||||
def atomic_write_png(img: np.ndarray, dst: Path) -> None:
|
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)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
tmp = dst.with_name(dst.stem + ".tmp_review_apply.png")
|
tmp = dst.with_name(dst.stem + ".tmp_review_apply.png")
|
||||||
|
|
||||||
ok = cv2.imwrite(
|
ok = cv2.imwrite(
|
||||||
|
|
@ -301,11 +288,115 @@ def atomic_write_png(img: np.ndarray, dst: Path) -> None:
|
||||||
os.replace(tmp, dst)
|
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:
|
def write_report(path: Path, rows: List[dict]) -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
"status",
|
"status",
|
||||||
|
"apply_eligible",
|
||||||
"base",
|
"base",
|
||||||
"review_count",
|
"review_count",
|
||||||
"review_groups",
|
"review_groups",
|
||||||
|
|
@ -315,6 +406,8 @@ def write_report(path: Path, rows: List[dict]) -> None:
|
||||||
"previews",
|
"previews",
|
||||||
"target_count",
|
"target_count",
|
||||||
"targets",
|
"targets",
|
||||||
|
"quarantine_dir",
|
||||||
|
"quarantine_pairs",
|
||||||
"message",
|
"message",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -332,8 +425,8 @@ def write_report(path: Path, rows: List[dict]) -> None:
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
ap = argparse.ArgumentParser(
|
ap = argparse.ArgumentParser(
|
||||||
description=(
|
description=(
|
||||||
"Aplica rapidamente masks revisadas em todas as cópias "
|
"Aplica masks revisadas somente quando o mapeamento é 1:1; "
|
||||||
"correspondentes encontradas nas BRUTAS."
|
"toda duplicidade é enviada para quarentena visual."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ap.add_argument("--review-root", required=True)
|
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("--labelmap", default="", help=argparse.SUPPRESS)
|
||||||
|
|
||||||
ap.add_argument("--report", default="")
|
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(
|
ap.add_argument(
|
||||||
"--apply",
|
"--apply",
|
||||||
action="store_true",
|
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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
|
@ -356,15 +457,24 @@ def main() -> int:
|
||||||
config_path = Path(args.config).resolve()
|
config_path = Path(args.config).resolve()
|
||||||
|
|
||||||
if not review_root.is_dir():
|
if not review_root.is_dir():
|
||||||
raise SystemExit(
|
raise SystemExit(f"[ERRO] review-root não encontrado: {review_root}")
|
||||||
f"[ERRO] review-root não encontrado: {review_root}"
|
|
||||||
)
|
|
||||||
if not brutas_root.is_dir():
|
if not brutas_root.is_dir():
|
||||||
raise SystemExit(
|
raise SystemExit(f"[ERRO] brutas-root não encontrado: {brutas_root}")
|
||||||
f"[ERRO] brutas-root não encontrado: {brutas_root}"
|
|
||||||
)
|
|
||||||
|
|
||||||
sizes = load_sizes(config_path)
|
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("[CONFIG]")
|
||||||
print(
|
print(
|
||||||
|
|
@ -375,9 +485,9 @@ def main() -> int:
|
||||||
f" raw_size : {sizes.raw_w}x{sizes.raw_h} "
|
f" raw_size : {sizes.raw_w}x{sizes.raw_h} "
|
||||||
"(brutas)"
|
"(brutas)"
|
||||||
)
|
)
|
||||||
print(
|
print(" resize : cv2.INTER_NEAREST")
|
||||||
" 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)
|
masks = discover_review_masks(review_root)
|
||||||
if not masks:
|
if not masks:
|
||||||
|
|
@ -394,34 +504,33 @@ def main() -> int:
|
||||||
"encontrados nas BRUTAS."
|
"encontrados nas BRUTAS."
|
||||||
)
|
)
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
print("=" * 100)
|
||||||
report_path = (
|
print("APPLY REVIEW MASKS - SAFE / QUARANTINE")
|
||||||
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(f"Modo : {'APPLY' if args.apply else 'DRY-RUN'}")
|
print(f"Modo : {'APPLY' if args.apply else 'DRY-RUN'}")
|
||||||
print(f"Review root : {review_root}")
|
print(f"Review root : {review_root}")
|
||||||
print(f"Brutas root : {brutas_root}")
|
print(f"Brutas root : {brutas_root}")
|
||||||
print(f"Arquivos review : {len(masks)}")
|
print(f"Arquivos review : {len(masks)}")
|
||||||
print(f"Bases review : {len(reviews_by_base)}")
|
print(f"Bases review : {len(reviews_by_base)}")
|
||||||
print("=" * 96)
|
print("=" * 100)
|
||||||
|
|
||||||
rows: List[dict] = []
|
rows: List[dict] = []
|
||||||
logical_reviews: List[LogicalReview] = []
|
logical_reviews: List[LogicalReview] = []
|
||||||
|
|
||||||
not_found = 0
|
not_found = 0
|
||||||
duplicate_review_ok = 0
|
quarantine_bases = 0
|
||||||
duplicate_review_conflict = 0
|
quarantine_pairs = 0
|
||||||
multi_target_bases = 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
|
total_target_files = 0
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# PREFLIGHT LEVE
|
# PREFLIGHT CONSERVADOR
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
for base in sorted(reviews_by_base):
|
for base in sorted(reviews_by_base):
|
||||||
review_paths = reviews_by_base[base]
|
review_paths = reviews_by_base[base]
|
||||||
|
|
@ -429,12 +538,11 @@ def main() -> int:
|
||||||
targets = unique_targets_from_previews(previews, base)
|
targets = unique_targets_from_previews(previews, base)
|
||||||
|
|
||||||
groups_review = sorted({review_group(p) for p in review_paths})
|
groups_review = sorted({review_group(p) for p in review_paths})
|
||||||
groups_preview = sorted(
|
groups_preview = sorted({preview_group(p) for p in previews})
|
||||||
{p.parent.parent.name for p in previews}
|
|
||||||
)
|
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
"status": "",
|
"status": "",
|
||||||
|
"apply_eligible": "NAO",
|
||||||
"base": base,
|
"base": base,
|
||||||
"review_count": len(review_paths),
|
"review_count": len(review_paths),
|
||||||
"review_groups": " | ".join(groups_review),
|
"review_groups": " | ".join(groups_review),
|
||||||
|
|
@ -444,53 +552,101 @@ def main() -> int:
|
||||||
"previews": " | ".join(str(p) for p in previews),
|
"previews": " | ".join(str(p) for p in previews),
|
||||||
"target_count": len(targets),
|
"target_count": len(targets),
|
||||||
"targets": " | ".join(str(p) for p in targets),
|
"targets": " | ".join(str(p) for p in targets),
|
||||||
|
"quarantine_dir": "",
|
||||||
|
"quarantine_pairs": 0,
|
||||||
"message": "",
|
"message": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
not_found += 1
|
not_found += 1
|
||||||
row["status"] = "NAO_ENCONTRADO"
|
row["status"] = "NAO_ENCONTRADO"
|
||||||
row["message"] = (
|
row["message"] = "nenhum preview correspondente encontrado nas BRUTAS"
|
||||||
"nenhum preview correspondente encontrado nas BRUTAS"
|
|
||||||
)
|
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
continue
|
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(
|
equivalent, reason = duplicate_reviews_are_equivalent(
|
||||||
review_paths,
|
review_paths,
|
||||||
sizes,
|
sizes,
|
||||||
)
|
)
|
||||||
if not equivalent:
|
|
||||||
duplicate_review_conflict += 1
|
if multi_target:
|
||||||
row["status"] = "CONFLITO_REVIEW_DUPLICADO"
|
if equivalent:
|
||||||
row["message"] = reason
|
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)
|
rows.append(row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
duplicate_equivalent = True
|
# Só chega aqui quando é estritamente 1 review : 1 target.
|
||||||
duplicate_review_ok += 1
|
if len(review_paths) != 1 or len(targets) != 1:
|
||||||
|
raise RuntimeError(
|
||||||
if len(targets) > 1:
|
f"invariante de segurança quebrada no preflight: {base} "
|
||||||
multi_target_bases += 1
|
f"review_count={len(review_paths)} target_count={len(targets)}"
|
||||||
row["status"] = "MULTI_TARGET"
|
|
||||||
row["message"] = (
|
|
||||||
f"mesma revisão será aplicada em {len(targets)} "
|
|
||||||
"cópias das BRUTAS"
|
|
||||||
)
|
)
|
||||||
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(
|
logical_reviews.append(
|
||||||
LogicalReview(
|
LogicalReview(
|
||||||
base=base,
|
base=base,
|
||||||
|
|
@ -498,114 +654,112 @@ def main() -> int:
|
||||||
representative=review_paths[0],
|
representative=review_paths[0],
|
||||||
targets=targets,
|
targets=targets,
|
||||||
preview_paths=previews,
|
preview_paths=previews,
|
||||||
duplicate_review_equivalent=duplicate_equivalent,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
print("[PREFLIGHT]")
|
print("[PREFLIGHT]")
|
||||||
print(f" Bases prontas : {len(logical_reviews)}")
|
print(f" Bases elegíveis 1:1 : {len(logical_reviews)}")
|
||||||
print(f" Bases com múltiplos alvos : {multi_target_bases}")
|
print(f" Bases em quarentena : {quarantine_bases}")
|
||||||
print(f" Review duplicado idêntico : {duplicate_review_ok}")
|
print(f" Multi-target : {q_multi_target}")
|
||||||
print(f" Conflito review duplicado : {duplicate_review_conflict}")
|
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" 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)
|
write_report(report_path, rows)
|
||||||
|
|
||||||
|
if quarantine_bases:
|
||||||
|
print(f"[QUARENTENA] {quarantine_root}")
|
||||||
|
|
||||||
if not args.apply:
|
if not args.apply:
|
||||||
print(f"[DRY-RUN] Relatório: {report_path}")
|
print(f"[DRY-RUN] Relatório: {report_path}")
|
||||||
|
print("[DRY-RUN] Nenhuma mask dentro das BRUTAS foi alterada.")
|
||||||
print(
|
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(
|
print(
|
||||||
"[INFO] Bases com múltiplos previews NÃO são mais ambíguas: "
|
"[ATENÇÃO] Houve erro ao montar parte da quarentena. "
|
||||||
"a mesma mask revisada será aplicada em todas as cópias."
|
"Não rode --apply até revisar o relatório."
|
||||||
)
|
|
||||||
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."
|
|
||||||
)
|
)
|
||||||
|
return 2
|
||||||
return 0
|
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_bases = 0
|
||||||
applied_files = 0
|
applied_files = 0
|
||||||
image_errors = 0
|
image_errors = 0
|
||||||
|
|
||||||
for i, item in enumerate(logical_reviews, 1):
|
for item in logical_reviews:
|
||||||
try:
|
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)
|
img = load_and_resize_mask(item.representative, sizes)
|
||||||
|
target = item.targets[0]
|
||||||
# Aplica em TODAS as cópias existentes das BRUTAS.
|
|
||||||
for target in item.targets:
|
|
||||||
atomic_write_png(img, target)
|
atomic_write_png(img, target)
|
||||||
applied_files += 1
|
|
||||||
|
|
||||||
|
applied_files += 1
|
||||||
applied_bases += 1
|
applied_bases += 1
|
||||||
|
|
||||||
if (
|
if applied_bases <= 10 or applied_bases % 100 == 0:
|
||||||
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 ""
|
|
||||||
)
|
|
||||||
print(
|
print(
|
||||||
f"[{applied_bases}/{len(logical_reviews)}] "
|
f"[{applied_bases}/{len(logical_reviews)}] "
|
||||||
f"{item.base}{suffix}"
|
f"{item.base} -> 1 target"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
image_errors += 1
|
image_errors += 1
|
||||||
print(f"[ERRO] {item.base}: {e}")
|
print(f"[ERRO] {item.base}: {e}")
|
||||||
|
|
||||||
# Atualiza a linha correspondente no relatório.
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if row["base"] == item.base:
|
if row["base"] == item.base:
|
||||||
row["status"] = "ERRO_APPLY"
|
row["status"] = "ERRO_APPLY"
|
||||||
|
row["apply_eligible"] = "NAO"
|
||||||
row["message"] = str(e)
|
row["message"] = str(e)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Atualiza status dos itens que chegaram ao fim sem erro.
|
|
||||||
error_bases = {
|
error_bases = {
|
||||||
row["base"]
|
row["base"]
|
||||||
for row in rows
|
for row in rows
|
||||||
if row["status"] == "ERRO_APPLY"
|
if row["status"] == "ERRO_APPLY"
|
||||||
}
|
}
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if (
|
if row["base"] not in error_bases and row["status"] == "RESOLVIDO_1x1":
|
||||||
row["base"] not in error_bases
|
|
||||||
and row["status"]
|
|
||||||
in {"RESOLVIDO", "MULTI_TARGET", "REVIEW_DUPLICADO_OK"}
|
|
||||||
):
|
|
||||||
row["status"] = "APLICADO"
|
row["status"] = "APLICADO"
|
||||||
|
|
||||||
write_report(report_path, rows)
|
write_report(report_path, rows)
|
||||||
|
|
||||||
print("=" * 96)
|
print("=" * 100)
|
||||||
print(f"[OK] Bases aplicadas : {applied_bases}")
|
print(f"[OK] Bases aplicadas 1:1 : {applied_bases}")
|
||||||
print(f"[OK] Arquivos gravados : {applied_files}")
|
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] Não encontrados : {not_found}")
|
||||||
print(
|
print(f"[ERRO] Falhas imagem/escrita : {image_errors}")
|
||||||
f"[SKIP] Conflito review duplic.: "
|
|
||||||
f"{duplicate_review_conflict}"
|
|
||||||
)
|
|
||||||
print(f"[ERRO] Falhas de imagem/escrita: {image_errors}")
|
|
||||||
print(f"[OK] Relatório : {report_path}")
|
print(f"[OK] Relatório : {report_path}")
|
||||||
|
if quarantine_bases:
|
||||||
if duplicate_review_conflict:
|
print(f"[OK] Quarentena : {quarantine_root}")
|
||||||
print(
|
|
||||||
"[INFO] Conflitos de revisão duplicada foram deixados intactos "
|
|
||||||
"para resolução manual."
|
|
||||||
)
|
|
||||||
|
|
||||||
return 0 if image_errors == 0 else 2
|
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-train-only ^
|
||||||
--synthetic-respect-family-split ^
|
--synthetic-respect-family-split ^
|
||||||
--clear-dst
|
--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
|
from __future__ import annotations
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"camera": "oak-fcc-3",
|
"camera": "oak-fcc-3",
|
||||||
"modelo": "segformer_b1",
|
"modelo": "segformer_b1",
|
||||||
"model_name": "2026_09_08",
|
"model_name": "2026_09_18",
|
||||||
"main_class_name": "erva",
|
"main_class_name": "erva",
|
||||||
"es_classes": "",
|
"es_classes": "",
|
||||||
"model_to_use": "geral",
|
"model_to_use": "geral",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue