Compare commits

..

2 Commits

68 changed files with 77480 additions and 10333 deletions

View File

@ -36,6 +36,7 @@ namespace AgroBase.Forms.Direcional
this.lblTipoMovimento = new System.Windows.Forms.Label();
this.btnSalvarSentido = new System.Windows.Forms.Button();
this.gpbDF = new System.Windows.Forms.GroupBox();
this.chbDFUsar = new System.Windows.Forms.CheckBox();
this.label5 = new System.Windows.Forms.Label();
this.txtDFOffsetAngulo = new System.Windows.Forms.TextBox();
this.label2 = new System.Windows.Forms.Label();
@ -47,6 +48,7 @@ namespace AgroBase.Forms.Direcional
this.lblDFEsquerda = new System.Windows.Forms.Label();
this.lblDFDireita = new System.Windows.Forms.Label();
this.gpbEF = new System.Windows.Forms.GroupBox();
this.chbEFUsar = new System.Windows.Forms.CheckBox();
this.label6 = new System.Windows.Forms.Label();
this.txtEFOffsetAngulo = new System.Windows.Forms.TextBox();
this.label4 = new System.Windows.Forms.Label();
@ -58,6 +60,7 @@ namespace AgroBase.Forms.Direcional
this.lblEFEsquerda = new System.Windows.Forms.Label();
this.lblEFDireita = new System.Windows.Forms.Label();
this.gpbDT = new System.Windows.Forms.GroupBox();
this.chbDTUsar = new System.Windows.Forms.CheckBox();
this.label8 = new System.Windows.Forms.Label();
this.txtDTOffsetAngulo = new System.Windows.Forms.TextBox();
this.label3 = new System.Windows.Forms.Label();
@ -69,6 +72,7 @@ namespace AgroBase.Forms.Direcional
this.lblDTEsquerda = new System.Windows.Forms.Label();
this.lblDTDireita = new System.Windows.Forms.Label();
this.gpbET = new System.Windows.Forms.GroupBox();
this.chbETUsar = new System.Windows.Forms.CheckBox();
this.label7 = new System.Windows.Forms.Label();
this.txtETOffsetAngulo = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
@ -79,10 +83,14 @@ namespace AgroBase.Forms.Direcional
this.cmbETEsquerda = new System.Windows.Forms.ComboBox();
this.lblETEsquerda = new System.Windows.Forms.Label();
this.lblETDireita = new System.Windows.Forms.Label();
this.chbDFUsar = new System.Windows.Forms.CheckBox();
this.chbDTUsar = new System.Windows.Forms.CheckBox();
this.chbEFUsar = new System.Windows.Forms.CheckBox();
this.chbETUsar = new System.Windows.Forms.CheckBox();
this.txtDTOffsetRef = new System.Windows.Forms.TextBox();
this.label9 = new System.Windows.Forms.Label();
this.label10 = new System.Windows.Forms.Label();
this.txtETOffsetRef = new System.Windows.Forms.TextBox();
this.label11 = new System.Windows.Forms.Label();
this.txtEFOffsetRef = new System.Windows.Forms.TextBox();
this.label12 = new System.Windows.Forms.Label();
this.txtDFOffsetRef = new System.Windows.Forms.TextBox();
this.gpbSentido.SuspendLayout();
this.gpbDF.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.nudDFOffset)).BeginInit();
@ -109,7 +117,7 @@ namespace AgroBase.Forms.Direcional
this.gpbSentido.Margin = new System.Windows.Forms.Padding(2);
this.gpbSentido.Name = "gpbSentido";
this.gpbSentido.Padding = new System.Windows.Forms.Padding(2);
this.gpbSentido.Size = new System.Drawing.Size(376, 427);
this.gpbSentido.Size = new System.Drawing.Size(376, 463);
this.gpbSentido.TabIndex = 80;
this.gpbSentido.TabStop = false;
this.gpbSentido.Text = "Sentido de giro";
@ -117,7 +125,7 @@ namespace AgroBase.Forms.Direcional
// lblIP
//
this.lblIP.AutoSize = true;
this.lblIP.Location = new System.Drawing.Point(9, 378);
this.lblIP.Location = new System.Drawing.Point(9, 422);
this.lblIP.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.lblIP.Name = "lblIP";
this.lblIP.Size = new System.Drawing.Size(17, 13);
@ -126,7 +134,7 @@ namespace AgroBase.Forms.Direcional
//
// txtIP
//
this.txtIP.Location = new System.Drawing.Point(12, 395);
this.txtIP.Location = new System.Drawing.Point(12, 439);
this.txtIP.Margin = new System.Windows.Forms.Padding(2);
this.txtIP.Name = "txtIP";
this.txtIP.Size = new System.Drawing.Size(85, 20);
@ -154,7 +162,7 @@ namespace AgroBase.Forms.Direcional
//
// btnSalvarSentido
//
this.btnSalvarSentido.Location = new System.Drawing.Point(275, 391);
this.btnSalvarSentido.Location = new System.Drawing.Point(275, 435);
this.btnSalvarSentido.Margin = new System.Windows.Forms.Padding(2);
this.btnSalvarSentido.Name = "btnSalvarSentido";
this.btnSalvarSentido.Size = new System.Drawing.Size(88, 24);
@ -165,6 +173,8 @@ namespace AgroBase.Forms.Direcional
//
// gpbDF
//
this.gpbDF.Controls.Add(this.label12);
this.gpbDF.Controls.Add(this.txtDFOffsetRef);
this.gpbDF.Controls.Add(this.chbDFUsar);
this.gpbDF.Controls.Add(this.label5);
this.gpbDF.Controls.Add(this.txtDFOffsetAngulo);
@ -180,11 +190,21 @@ namespace AgroBase.Forms.Direcional
this.gpbDF.Margin = new System.Windows.Forms.Padding(2);
this.gpbDF.Name = "gpbDF";
this.gpbDF.Padding = new System.Windows.Forms.Padding(2);
this.gpbDF.Size = new System.Drawing.Size(168, 152);
this.gpbDF.Size = new System.Drawing.Size(168, 175);
this.gpbDF.TabIndex = 9;
this.gpbDF.TabStop = false;
this.gpbDF.Text = "M4 - Direito Frente (DF)";
//
// chbDFUsar
//
this.chbDFUsar.AutoSize = true;
this.chbDFUsar.Location = new System.Drawing.Point(127, 124);
this.chbDFUsar.Name = "chbDFUsar";
this.chbDFUsar.Size = new System.Drawing.Size(32, 17);
this.chbDFUsar.TabIndex = 19;
this.chbDFUsar.Text = "L";
this.chbDFUsar.UseVisualStyleBackColor = true;
//
// label5
//
this.label5.AutoSize = true;
@ -294,6 +314,8 @@ namespace AgroBase.Forms.Direcional
//
// gpbEF
//
this.gpbEF.Controls.Add(this.label11);
this.gpbEF.Controls.Add(this.txtEFOffsetRef);
this.gpbEF.Controls.Add(this.chbEFUsar);
this.gpbEF.Controls.Add(this.label6);
this.gpbEF.Controls.Add(this.txtEFOffsetAngulo);
@ -309,11 +331,21 @@ namespace AgroBase.Forms.Direcional
this.gpbEF.Margin = new System.Windows.Forms.Padding(2);
this.gpbEF.Name = "gpbEF";
this.gpbEF.Padding = new System.Windows.Forms.Padding(2);
this.gpbEF.Size = new System.Drawing.Size(168, 152);
this.gpbEF.Size = new System.Drawing.Size(168, 175);
this.gpbEF.TabIndex = 9;
this.gpbEF.TabStop = false;
this.gpbEF.Text = "M2 - Esquerdo Frente (EF)";
//
// chbEFUsar
//
this.chbEFUsar.AutoSize = true;
this.chbEFUsar.Location = new System.Drawing.Point(127, 124);
this.chbEFUsar.Name = "chbEFUsar";
this.chbEFUsar.Size = new System.Drawing.Size(32, 17);
this.chbEFUsar.TabIndex = 21;
this.chbEFUsar.Text = "L";
this.chbEFUsar.UseVisualStyleBackColor = true;
//
// label6
//
this.label6.AutoSize = true;
@ -423,6 +455,8 @@ namespace AgroBase.Forms.Direcional
//
// gpbDT
//
this.gpbDT.Controls.Add(this.label9);
this.gpbDT.Controls.Add(this.txtDTOffsetRef);
this.gpbDT.Controls.Add(this.chbDTUsar);
this.gpbDT.Controls.Add(this.label8);
this.gpbDT.Controls.Add(this.txtDTOffsetAngulo);
@ -434,15 +468,25 @@ namespace AgroBase.Forms.Direcional
this.gpbDT.Controls.Add(this.cmbDTEsquerda);
this.gpbDT.Controls.Add(this.lblDTEsquerda);
this.gpbDT.Controls.Add(this.lblDTDireita);
this.gpbDT.Location = new System.Drawing.Point(195, 216);
this.gpbDT.Location = new System.Drawing.Point(195, 239);
this.gpbDT.Margin = new System.Windows.Forms.Padding(2);
this.gpbDT.Name = "gpbDT";
this.gpbDT.Padding = new System.Windows.Forms.Padding(2);
this.gpbDT.Size = new System.Drawing.Size(168, 152);
this.gpbDT.Size = new System.Drawing.Size(168, 175);
this.gpbDT.TabIndex = 9;
this.gpbDT.TabStop = false;
this.gpbDT.Text = "M3 - Direito Trás (DT)";
//
// chbDTUsar
//
this.chbDTUsar.AutoSize = true;
this.chbDTUsar.Location = new System.Drawing.Point(125, 124);
this.chbDTUsar.Name = "chbDTUsar";
this.chbDTUsar.Size = new System.Drawing.Size(32, 17);
this.chbDTUsar.TabIndex = 23;
this.chbDTUsar.Text = "L";
this.chbDTUsar.UseVisualStyleBackColor = true;
//
// label8
//
this.label8.AutoSize = true;
@ -552,6 +596,8 @@ namespace AgroBase.Forms.Direcional
//
// gpbET
//
this.gpbET.Controls.Add(this.label10);
this.gpbET.Controls.Add(this.txtETOffsetRef);
this.gpbET.Controls.Add(this.chbETUsar);
this.gpbET.Controls.Add(this.label7);
this.gpbET.Controls.Add(this.txtETOffsetAngulo);
@ -563,15 +609,25 @@ namespace AgroBase.Forms.Direcional
this.gpbET.Controls.Add(this.cmbETEsquerda);
this.gpbET.Controls.Add(this.lblETEsquerda);
this.gpbET.Controls.Add(this.lblETDireita);
this.gpbET.Location = new System.Drawing.Point(13, 216);
this.gpbET.Location = new System.Drawing.Point(13, 239);
this.gpbET.Margin = new System.Windows.Forms.Padding(2);
this.gpbET.Name = "gpbET";
this.gpbET.Padding = new System.Windows.Forms.Padding(2);
this.gpbET.Size = new System.Drawing.Size(168, 152);
this.gpbET.Size = new System.Drawing.Size(168, 175);
this.gpbET.TabIndex = 0;
this.gpbET.TabStop = false;
this.gpbET.Text = "M1 - Esquerdo Trás (ET)";
//
// chbETUsar
//
this.chbETUsar.AutoSize = true;
this.chbETUsar.Location = new System.Drawing.Point(127, 124);
this.chbETUsar.Name = "chbETUsar";
this.chbETUsar.Size = new System.Drawing.Size(32, 17);
this.chbETUsar.TabIndex = 23;
this.chbETUsar.Text = "L";
this.chbETUsar.UseVisualStyleBackColor = true;
//
// label7
//
this.label7.AutoSize = true;
@ -679,52 +735,88 @@ namespace AgroBase.Forms.Direcional
this.lblETDireita.TabIndex = 2;
this.lblETDireita.Text = "Direita";
//
// chbDFUsar
// txtDTOffsetRef
//
this.chbDFUsar.AutoSize = true;
this.chbDFUsar.Location = new System.Drawing.Point(127, 124);
this.chbDFUsar.Name = "chbDFUsar";
this.chbDFUsar.Size = new System.Drawing.Size(32, 17);
this.chbDFUsar.TabIndex = 19;
this.chbDFUsar.Text = "L";
this.chbDFUsar.UseVisualStyleBackColor = true;
this.txtDTOffsetRef.Location = new System.Drawing.Point(65, 146);
this.txtDTOffsetRef.Margin = new System.Windows.Forms.Padding(2);
this.txtDTOffsetRef.Name = "txtDTOffsetRef";
this.txtDTOffsetRef.Size = new System.Drawing.Size(57, 20);
this.txtDTOffsetRef.TabIndex = 24;
this.txtDTOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// chbDTUsar
// label9
//
this.chbDTUsar.AutoSize = true;
this.chbDTUsar.Location = new System.Drawing.Point(125, 124);
this.chbDTUsar.Name = "chbDTUsar";
this.chbDTUsar.Size = new System.Drawing.Size(32, 17);
this.chbDTUsar.TabIndex = 23;
this.chbDTUsar.Text = "L";
this.chbDTUsar.UseVisualStyleBackColor = true;
this.label9.AutoSize = true;
this.label9.Location = new System.Drawing.Point(10, 149);
this.label9.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.label9.Name = "label9";
this.label9.Size = new System.Drawing.Size(55, 13);
this.label9.TabIndex = 25;
this.label9.Text = "Offset Ref";
//
// chbEFUsar
// label10
//
this.chbEFUsar.AutoSize = true;
this.chbEFUsar.Location = new System.Drawing.Point(127, 124);
this.chbEFUsar.Name = "chbEFUsar";
this.chbEFUsar.Size = new System.Drawing.Size(32, 17);
this.chbEFUsar.TabIndex = 21;
this.chbEFUsar.Text = "L";
this.chbEFUsar.UseVisualStyleBackColor = true;
this.label10.AutoSize = true;
this.label10.Location = new System.Drawing.Point(10, 149);
this.label10.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.label10.Name = "label10";
this.label10.Size = new System.Drawing.Size(55, 13);
this.label10.TabIndex = 27;
this.label10.Text = "Offset Ref";
//
// chbETUsar
// txtETOffsetRef
//
this.chbETUsar.AutoSize = true;
this.chbETUsar.Location = new System.Drawing.Point(127, 124);
this.chbETUsar.Name = "chbETUsar";
this.chbETUsar.Size = new System.Drawing.Size(32, 17);
this.chbETUsar.TabIndex = 23;
this.chbETUsar.Text = "L";
this.chbETUsar.UseVisualStyleBackColor = true;
this.txtETOffsetRef.Location = new System.Drawing.Point(65, 146);
this.txtETOffsetRef.Margin = new System.Windows.Forms.Padding(2);
this.txtETOffsetRef.Name = "txtETOffsetRef";
this.txtETOffsetRef.Size = new System.Drawing.Size(57, 20);
this.txtETOffsetRef.TabIndex = 26;
this.txtETOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// label11
//
this.label11.AutoSize = true;
this.label11.Location = new System.Drawing.Point(11, 149);
this.label11.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.label11.Name = "label11";
this.label11.Size = new System.Drawing.Size(55, 13);
this.label11.TabIndex = 29;
this.label11.Text = "Offset Ref";
//
// txtEFOffsetRef
//
this.txtEFOffsetRef.Location = new System.Drawing.Point(66, 146);
this.txtEFOffsetRef.Margin = new System.Windows.Forms.Padding(2);
this.txtEFOffsetRef.Name = "txtEFOffsetRef";
this.txtEFOffsetRef.Size = new System.Drawing.Size(57, 20);
this.txtEFOffsetRef.TabIndex = 28;
this.txtEFOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// label12
//
this.label12.AutoSize = true;
this.label12.Location = new System.Drawing.Point(10, 149);
this.label12.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
this.label12.Name = "label12";
this.label12.Size = new System.Drawing.Size(55, 13);
this.label12.TabIndex = 31;
this.label12.Text = "Offset Ref";
//
// txtDFOffsetRef
//
this.txtDFOffsetRef.Location = new System.Drawing.Point(65, 146);
this.txtDFOffsetRef.Margin = new System.Windows.Forms.Padding(2);
this.txtDFOffsetRef.Name = "txtDFOffsetRef";
this.txtDFOffsetRef.Size = new System.Drawing.Size(57, 20);
this.txtDFOffsetRef.TabIndex = 30;
this.txtDFOffsetRef.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
//
// frmDirConfig
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.ClientSize = new System.Drawing.Size(398, 447);
this.ClientSize = new System.Drawing.Size(398, 485);
this.Controls.Add(this.gpbSentido);
this.Margin = new System.Windows.Forms.Padding(2);
this.Name = "frmDirConfig";
@ -805,5 +897,13 @@ namespace AgroBase.Forms.Direcional
private System.Windows.Forms.CheckBox chbEFUsar;
private System.Windows.Forms.CheckBox chbDTUsar;
private System.Windows.Forms.CheckBox chbETUsar;
private System.Windows.Forms.Label label12;
private System.Windows.Forms.TextBox txtDFOffsetRef;
private System.Windows.Forms.Label label11;
private System.Windows.Forms.TextBox txtEFOffsetRef;
private System.Windows.Forms.Label label9;
private System.Windows.Forms.TextBox txtDTOffsetRef;
private System.Windows.Forms.Label label10;
private System.Windows.Forms.TextBox txtETOffsetRef;
}
}

View File

@ -72,6 +72,9 @@ namespace AgroBase.Forms.Direcional
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
txtOffsetAngulo.Text = Modulo.DirMotor.OffsetAnguloReal.ToString();
TextBox txtOffsetRef = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetRef");
txtOffsetRef.Text = Modulo.DirMotor.OffsetReferenciamento.ToString();
CheckBox chbUsar = FuncoesGlobais.FindControlRecursive<CheckBox>(gpbSentido, "chb" + Modulo.Modulo_ID + "Usar");
chbUsar.Checked = Modulo.DirMotor.UsarAnguloSensor;
}
@ -119,6 +122,9 @@ namespace AgroBase.Forms.Direcional
TextBox txtOffsetAngulo = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetAngulo");
Modulo.DirMotor.OffsetAnguloReal = Convert.ToDouble(txtOffsetAngulo.Text);
TextBox txtOffsetRef = FuncoesGlobais.FindControlRecursive<TextBox>(gpbSentido, "txt" + Modulo.Modulo_ID + "OffsetRef");
Modulo.DirMotor.OffsetReferenciamento = Convert.ToDouble(txtOffsetRef.Text);
CheckBox chbUsar = FuncoesGlobais.FindControlRecursive<CheckBox>(gpbSentido, "chb" + Modulo.Modulo_ID + "Usar");
Modulo.DirMotor.UsarAnguloSensor = chbUsar.Checked;
}

View File

@ -339,13 +339,28 @@ namespace AgroBase.Models.Modules
{
var op = Variaveis.OperacaoEmAndamento;
if (op.CalibrandoRuntime)
if (op == null)
return;
long tickSolicitado = Stopwatch.GetTimestamp();
lock (_metricasLock)
{
long agoraTicks = Stopwatch.GetTimestamp();
DateTime agora = DateTime.Now;
double esperaLockSeg = (agoraTicks - tickSolicitado) / (double)Stopwatch.Frequency;
double tempoMovimentoAtualSeg = op.Sensoriamento?.Movimentacao?.TempoMovimentoSegs ?? 0.0;
if (op.CalibrandoRuntime)
{
/*
* Durante a calibragem não integra, mas mantém
* o relógio ancorado no instante atual.
*/
ReancorarMetricas(agoraTicks, tempoMovimentoAtualSeg, encerrarTrechos: true);
return;
}
bool operacaoConcluida = op.Sensoriamento?.Operacao?.StatusOperacaoAtual == StatusOperacao.Concluido;
bool operacaoAtiva = op.Sensoriamento?.Operacao?.OperacaoIniciada == true && !operacaoConcluida;
@ -411,10 +426,11 @@ namespace AgroBase.Models.Modules
: new HashSet<int>();
double vazaoAtualMLs = operacaoAtiva ? ObterVazaoAtualConfiavelMLs(agora) : 0.0;
double tempoMovimentoAtualSeg = op.Sensoriamento?.Movimentacao?.TempoMovimentoSegs ?? 0.0;
if (_ultimoTickMetricas != 0)
{
bool haviaAtuacaoNoIntervaloAnterior = _bicosAtivosIntervaloAnterior.Count > 0 || _bombasAtivasIntervaloAnterior.Count > 0 || _vazaoIntervaloAnteriorMLs > 0.01;
double dt = (agoraTicks - _ultimoTickMetricas) / (double)Stopwatch.Frequency;
if (dt > 0 && dt <= IntervaloMaximoIntegracaoSeg)
@ -443,8 +459,26 @@ namespace AgroBase.Models.Modules
if (dt > IntervaloMaximoIntegracaoSeg)
{
Variaveis.MostrarLog("[ATU/METRICAS] Integração ignorada por dt alto: " + dt.ToString("0.000") + "s");
op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, "[ATU/METRICAS] Integração ignorada por dt alto: " + dt.ToString("0.000") + "s");
string mensagem =
"[ATU/METRICAS] Integração ignorada por dt alto: " +
$"{dt:0.000}s; " +
$"opAtiva={operacaoAtiva}; " +
$"atuacaoAnterior={haviaAtuacaoNoIntervaloAnterior}; " +
$"bicosAnterior={_bicosAtivosIntervaloAnterior.Count}; " +
$"bombasAnterior={_bombasAtivasIntervaloAnterior.Count}; " +
$"vazaoAnterior={_vazaoIntervaloAnteriorMLs:0.00}mL/s; " +
$"esperaLock={esperaLockSeg * 1000.0:0.0}ms";
Variaveis.MostrarLog(mensagem);
/*
* Só vira evento operacional se efetivamente
* perdemos um intervalo de pulverização.
*/
if (operacaoAtiva && haviaAtuacaoNoIntervaloAnterior)
{
op.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 80, mensagem);
}
}
}
}
@ -494,6 +528,24 @@ namespace AgroBase.Models.Modules
}
}
private void ReancorarMetricas(long agoraTicks, double tempoMovimentoAtualSeg, bool encerrarTrechos)
{
if (encerrarTrechos)
{
foreach (var bico in BicosPulverizadores ?? new List<AtuadorBicoModel>())
{
bico?.AtualizarEstadoTrecho(false);
}
}
_ultimoTickMetricas = agoraTicks;
_tempoMovimentoAnteriorSeg = tempoMovimentoAtualSeg;
_bicosAtivosIntervaloAnterior = new HashSet<int>();
_bombasAtivasIntervaloAnterior = new HashSet<int>();
_potenciaBombasIntervaloAnterior = new Dictionary<int, double>();
_vazaoIntervaloAnteriorMLs = 0.0;
ZerarVazoesInstantaneas();
}
private void IntegrarIntervaloAnterior(double dt, double tempoMovimentoIntervaloSeg)
{
var bicosDoIntervalo = (BicosPulverizadores ??

View File

@ -24,6 +24,7 @@ namespace AgroBase.Models.Modules
public Sentido Sentido_SP { get; set; } = Sentido.Parado;
public double Angulo_SP { get; set; } = 0;
public double FolgaMecanica { get; set; } = 0;
public double OffsetReferenciamento { get; set; } = 0;
public double Velocidade_Max { get; set; } = 600;
public int Velocidade
{
@ -149,11 +150,16 @@ namespace AgroBase.Models.Modules
return !Comandar || Sentido_SP == Sentido.Parado ? 0 : Angulo_SP;
}
}
public double ErroAnguloSP
public double ErroAngulo
{
get
{
return Math.Abs(AnguloLeitura - AnguloSPAtual);
double anguloSp =
!Comandar || Sentido_SP == Sentido.Parado
? 0
: Angulo_SP;
return anguloSp - AnguloLeitura;
}
}
public bool RetornandoAzero
@ -165,13 +171,7 @@ namespace AgroBase.Models.Modules
return foraZero && parar;
}
}
public bool EmMovimento
{
get
{
return Math.Abs(RPM) > 1;
}
}
public bool EmMovimento { get; set; }
public bool AtingiuAnguloSP
{
get
@ -207,6 +207,109 @@ namespace AgroBase.Models.Modules
}
private DateTime _erroAngularDesde = DateTime.MinValue;
private DateTime _recuperacaoAngularDesde = DateTime.MinValue;
public DateTime UltimaMudancaAnguloSP { get; private set; }
public DateTime InicioMovimentoAngular { get; private set; }
public bool ErroSeguimentoAngularPersistente { get; private set; }
public void AtualizarSaudeSeguimentoAngular(DateTime agora)
{
const double toleranciaFalhaGraus = 3.0;
const double toleranciaRecuperacaoGraus = 2.0;
TimeSpan tempoGracaSP = TimeSpan.FromMilliseconds(750);
TimeSpan persistenciaFalha = TimeSpan.FromSeconds(2);
TimeSpan persistenciaRecuperacao = TimeSpan.FromMilliseconds(500);
TimeSpan timeoutMovimento = TimeSpan.FromSeconds(15);
if (!Comandar)
{
ResetarSaudeSeguimentoAngular();
return;
}
bool emGracaSP =
UltimaMudancaAnguloSP != DateTime.MinValue &&
agora - UltimaMudancaAnguloSP <= tempoGracaSP;
bool movimentoDentroDoPrazo =
EmMovimento &&
InicioMovimentoAngular != DateTime.MinValue &&
agora - InicioMovimentoAngular <= timeoutMovimento;
bool movimentoExcedeuTimeout =
EmMovimento &&
InicioMovimentoAngular != DateTime.MinValue &&
agora - InicioMovimentoAngular > timeoutMovimento;
double ErroAnguloAbs = Math.Abs(ErroAngulo);
bool candidatoFalha =
ErroAnguloAbs > toleranciaFalhaGraus &&
!emGracaSP &&
(!EmMovimento || movimentoExcedeuTimeout);
if (candidatoFalha)
{
_recuperacaoAngularDesde = DateTime.MinValue;
if (_erroAngularDesde == DateTime.MinValue)
_erroAngularDesde = agora;
if (agora - _erroAngularDesde >= persistenciaFalha)
ErroSeguimentoAngularPersistente = true;
return;
}
_erroAngularDesde = DateTime.MinValue;
bool recuperado =
ErroAnguloAbs <= toleranciaRecuperacaoGraus ||
movimentoDentroDoPrazo ||
emGracaSP;
if (!recuperado)
{
_recuperacaoAngularDesde = DateTime.MinValue;
return;
}
if (_recuperacaoAngularDesde == DateTime.MinValue)
_recuperacaoAngularDesde = agora;
if (agora - _recuperacaoAngularDesde >= persistenciaRecuperacao)
ErroSeguimentoAngularPersistente = false;
}
private void ResetarSaudeSeguimentoAngular()
{
_erroAngularDesde = DateTime.MinValue;
_recuperacaoAngularDesde = DateTime.MinValue;
ErroSeguimentoAngularPersistente = false;
}
public void AtualizarEmMovimento(bool emMovimentoAgora)
{
DateTime agora = DateTime.Now;
if (emMovimentoAgora && !EmMovimento)
{
InicioMovimentoAngular = agora;
}
if (!emMovimentoAgora)
{
InicioMovimentoAngular = DateTime.MinValue;
}
EmMovimento = emMovimentoAgora;
}
public Dictionary<TipoMovimentoDirecional, ConfiguracaoSentidoMotor> ConfigSentidos { get; set; }
public List<FuncoesPinout> Funcoes { get; set; }
@ -313,38 +416,41 @@ namespace AgroBase.Models.Modules
var _Controle = op.Controle;
int AnguloTipoMovimento = VariaveisEquipamento.AnguloMovimento[_Controle.TipoMovimento];
Sentido novoSentido = Sentido.Parado;
double novoAngulo = 0;
if (!Comandar)
{
Sentido_SP = Sentido.Parado;
novoSentido = Sentido.Parado;
}
else if (Direcao == Direcao.Direita)
{
Sentido_SP = ConfigSentidos[_Controle.TipoMovimento].Direita;
novoSentido = ConfigSentidos[_Controle.TipoMovimento].Direita;
}
else if (Direcao == Direcao.Esquerda)
{
Sentido_SP = ConfigSentidos[_Controle.TipoMovimento].Esquerda;
novoSentido = ConfigSentidos[_Controle.TipoMovimento].Esquerda;
}
else
{
Sentido_SP = Sentido.Parado;
novoSentido = Sentido.Parado;
}
UltimaDirecao = Direcao;
int mx = Sentido_SP == Sentido.Parado ? 0 : Sentido_SP == Sentido.Antihorario ? -1 : 1;
int mx = novoSentido == Sentido.Parado ? 0 : novoSentido == Sentido.Antihorario ? -1 : 1;
if (Sentido_SP == Sentido.Parado)
if (novoSentido == Sentido.Parado)
{
Angulo_SP = 0;
novoAngulo = 0;
UltimaDirecao = Direcao.Parado;
}
else if (AnguloTipoMovimento != -1)
{
Angulo_SP = AnguloTipoMovimento;
novoAngulo = AnguloTipoMovimento;
}
else
{
Angulo_SP = _Controle.Angulo;
novoAngulo = _Controle.Angulo;
}
if (Comandar)
@ -353,8 +459,16 @@ namespace AgroBase.Models.Modules
//Console.WriteLine($"[{Mod_ID}] Angulo Atualizado para {Variaveis.OperacaoEmAndamento.SimulacaoAnguloControle}");
}
Angulo_SP = Math.Abs(Angulo_SP) * mx;
Angulo_SP += AnguloFolgaCompensar;
novoAngulo = Math.Abs(novoAngulo) * mx;
novoAngulo += AnguloFolgaCompensar;
if (Math.Abs(novoAngulo - Angulo_SP) >= 0.05)
{
UltimaMudancaAnguloSP = DateTime.Now;
}
Angulo_SP = novoAngulo;
Sentido_SP = novoSentido;
if (_Simulando)
{
@ -473,9 +587,7 @@ namespace AgroBase.Models.Modules
_sentido = _sentido == Sentido.Horario ? Sentido.Antihorario : Sentido.Horario;
Variaveis.MostrarLog($"[DirecionalModel] [ReferenciaMotorRelativo] [DIR {Mod_ID}] - Invertendo Sentido de Giro para {_sentido.ToString()}");
double offset = Mod_ID == "DT" ? -2 : 0;
_anguloSP = CalcularCentroCompensado(AnguloEntrada, AnguloSaida, _sentido, offset);
_anguloSP = CalcularCentroCompensado(AnguloEntrada, AnguloSaida, _sentido, OffsetReferenciamento);
}
verificacoesAngulo++;

View File

@ -494,6 +494,8 @@ namespace AgroBase.Models.Modules
modulo.DirMotor.EnviarComandoControle(controleDir.UltimaDirecao);
enviouControle = true;
}
modulo.DirMotor.AtualizarSaudeSeguimentoAngular(agora);
}
if (enviouControle)

View File

@ -625,9 +625,9 @@ namespace AgroBase.Models.Modules
posicao = CanMessagePosicaoDados.Dados1,
indice = 0,
analise_health = true,
maximo = 5.2,
minimo = 4.9,
nominal = 5.0,
maximo = 5.3,
minimo = 4.75,
nominal = 5.1,
unidade_medida = "V"
},
new SensorValoresLeituraModel()
@ -636,9 +636,9 @@ namespace AgroBase.Models.Modules
posicao = CanMessagePosicaoDados.Dados1,
indice = 1,
analise_health = true,
maximo = 1500,
maximo = 800,
minimo = 100,
nominal = 280,
nominal = 430,
unidade_medida = "mA"
},
new SensorValoresLeituraModel()

View File

@ -4094,6 +4094,21 @@ namespace AgroBase.Models
op.TempoIniciarOperacao = Math.Max(0, Convert.ToInt32(Math.Round(tempoAguardarInicio)));
JObject emergenciaSistema = RedisService.GetField<JObject>(CtxKey.DadosOperacao, "emergencia_sistema");
bool emergenciaSistemicaSolicitada = emergenciaSistema?["solicitada"]?.Value<bool>() ?? RedisService.GetField<bool>(CtxKey.DadosOperacao, "emergencia_sistema_solicitada", false);
bool emergenciaAnterior = Operacao.Emergencia;
bool emergencia = emergenciaAnterior || emergenciaSistemicaSolicitada;
if (emergenciaSistemicaSolicitada && !emergenciaAnterior)
{
string codigo = emergenciaSistema?["codigo"]?.Value<string>() ?? "INTERTRAVAMENTO_SISTEMA";
string motivo = emergenciaSistema?["motivo"]?.Value<string>() ?? "Intertravamento sistêmico solicitado";
int severidade = emergenciaSistema?["severidade"]?.Value<int>() ?? 90;
InserirLog(T_Code.Mov, StatusModulo.Falha, Math.Max(0, 100 - severidade), $"Emergência sistêmica acionada [{codigo}]: {motivo}");
}
var operacaoAtualizada = new OperacaoSensoriamentoLogModel()
{
Momento = agora,
@ -4102,7 +4117,7 @@ namespace AgroBase.Models
Status = op.Status,
StatusOperacaoAnterior = Operacao.StatusOperacaoAnterior,
OperacaoIniciada = Operacao.OperacaoIniciada,
Emergencia = Operacao.Emergencia,
Emergencia = emergencia,
Pausa = Operacao.Pausa,
Calibrando = Operacao.Calibrando,
Finalizando = Operacao.Finalizando,
@ -4889,7 +4904,7 @@ namespace AgroBase.Models
? (velocidade / velocidadeMax) * 100.0
: 0;
double erroAngulo = dir != null ? CalcularErroAngulo(dir) : 0;
double erroAngulo = dir?.ErroAngulo ?? 0;
return new DirecionalModuloLogModel
{
@ -4919,6 +4934,7 @@ namespace AgroBase.Models
ErroAngulo = Math.Round(erroAngulo, 2),
ErroAnguloAbs = Math.Round(Math.Abs(erroAngulo), 2),
ErroSeguimentoAngularPersistente = dir?.ErroSeguimentoAngularPersistente ?? false,
AtingiuAnguloSP = dir?.AtingiuAnguloSP ?? false,
RetornandoAzero = dir?.RetornandoAzero ?? false,
@ -4943,19 +4959,6 @@ namespace AgroBase.Models
};
}
double CalcularErroAngulo(DirecionalModel dir)
{
if (dir == null)
return 0;
double anguloSp =
!dir.Comandar || dir.Sentido_SP == Sentido.Parado
? 0
: dir.Angulo_SP;
return anguloSp - dir.AnguloLeitura;
}
DirecionalStatusLogModel CriarStatus((bool, List<string>) status)
{
return new DirecionalStatusLogModel
@ -5021,7 +5024,7 @@ namespace AgroBase.Models
model.AnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.AnguloLeitura));
model.AnguloSPMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.Angulo_SP));
model.ErroAnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(CalcularErroAngulo(x.DirMotor)));
model.ErroAnguloMedioAbs = MediaOuZero(controlando, x => Math.Abs(x.DirMotor.ErroAngulo));
model.Status =
dados != null && controle != null
@ -5999,11 +6002,9 @@ namespace AgroBase.Models
public DirecionalStatusLogModel Status { get; set; } = new DirecionalStatusLogModel();
public bool StatusGeralOk =>
Disponivel &&
Status.Ok &&
ModulosInicializados > 0 &&
!Modulos.Any(x => x.ErroAnguloAbs > 3.0 && x.Controlar);
public bool StatusEstruturalOk => Disponivel && Status.Ok && ModulosInicializados > 0;
public bool StatusGeralOk => StatusEstruturalOk && !Modulos.Any(x => x.Controlar && x.ErroSeguimentoAngularPersistente);
public List<DirecionalModuloLogModel> Modulos { get; set; } = new List<DirecionalModuloLogModel>();
@ -6078,6 +6079,7 @@ namespace AgroBase.Models
// Diagnóstico geométrico
public double ErroAngulo { get; set; }
public double ErroAnguloAbs { get; set; }
public bool ErroSeguimentoAngularPersistente { get; set; }
public bool AtingiuAnguloSP { get; set; }
public bool RetornandoAzero { get; set; }

View File

@ -1228,6 +1228,8 @@ namespace AgroBase.Services
short rpm = DecodeInt16BE(data, 1);
item.RPM.valor = rpm;
Variaveis.OperacaoEmAndamento?.DispMvd?.Dados?.Modulos?.FirstOrDefault(x => x.DirMotor._EnderecoCAN_Tx == item.EnderecoTx)?.DirMotor?.AtualizarEmMovimento(Math.Abs(rpm) > 1);
}
private void ProcessarPulsosRecebidos(MKS057DModel item, byte[] data)

View File

@ -1,4 +1,5 @@
using AgroBase.Models;
using AgroBase.Models.Modules;
using AgroBase.Models.Operadores;
using Newtonsoft.Json;
using System;
@ -788,15 +789,30 @@ namespace AgroBase.Services.Operadores
})
.ToList();
bool possuiComandoServo = servo.UltimoComandoEnviado != DateTime.MinValue;
DateTime ultimaRespostaAnguloServo =
servo.ValoresLeituras?
.Where(x =>x.funcao == FuncoesPinout.ServoAnguloLeitura)
.OrderByDescending(x =>x.atual?.lidoEm ?? DateTime.MinValue)
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
bool possuiRespostaServo = ultimaRespostaAnguloServo != DateTime.MinValue;
double idadeUltimoComandoServoMs =
servo.UltimoComandoEnviado == DateTime.MinValue
!possuiComandoServo
? 999999
: (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds;
: Math.Max(0, (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaServoMs =
servo.UltimaLeitura == DateTime.MinValue
!possuiRespostaServo
? 999999
: (agora_dt - servo.UltimaLeitura).TotalMilliseconds;
: Math.Max(0, (agora_dt - ultimaRespostaAnguloServo).TotalMilliseconds);
bool telemetriaPosComandoServo =
possuiComandoServo &&
possuiRespostaServo &&
ultimaRespostaAnguloServo >= servo.UltimoComandoEnviado;
var servoObj = new
{
@ -806,15 +822,21 @@ namespace AgroBase.Services.Operadores
tipo,
controlar = servo.Controlar,
mandatorio = servo.Mandatorio,
angulo_sp = servo._AnguloControle,
angulo_desejado = servo.UltimoAnguloDesejado,
angulo_leitura = servo._AnguloLeiutra,
possui_comando = possuiComandoServo,
telemetria_pos_comando = telemetriaPosComandoServo,
ultimo_comando_ms = idadeUltimoComandoServoMs,
ultima_resposta_ms = idadeUltimaRespostaServoMs,
dados,
timestamp = agora,
saude = saude ?? new { },
angulo_desejado = servo.UltimoAnguloDesejado,
angulo_leitura = servo._AnguloLeiutra,
ultimo_comando_ms = idadeUltimoComandoServoMs,
ultima_resposta_ms = idadeUltimaRespostaServoMs,
};
senServosAtivos.Add($"{tipo}.{id}");
@ -843,15 +865,30 @@ namespace AgroBase.Services.Operadores
})
.ToList();
bool possuiComandoRele = rele.UltimoComandoEnviado != DateTime.MinValue;
bool possuiRespostaRele = rele.UltimaLeitura != DateTime.MinValue;
double idadeUltimoComandoReleMs = !possuiComandoRele ? 999999 : Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaReleMs = !possuiRespostaRele ? 999999 : Math.Max(0, (agora_dt - rele.UltimaLeitura).TotalMilliseconds);
DateTime ultimaRespostaEstadoRele =
rele.ValoresLeituras?
.Where(x =>x.funcao == FuncoesPinout.EstadoLeitura)
.OrderByDescending(x =>x.atual?.lidoEm ?? DateTime.MinValue)
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
bool telemetriaPosComando =
bool possuiComandoRele = rele.UltimoComandoEnviado != DateTime.MinValue;
bool possuiRespostaRele = ultimaRespostaEstadoRele != DateTime.MinValue;
double idadeUltimoComandoReleMs =
!possuiComandoRele
? 999999
: Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaReleMs =
!possuiRespostaRele
? 999999
: Math.Max(0, (agora_dt - ultimaRespostaEstadoRele).TotalMilliseconds);
bool telemetriaPosComandoRele =
possuiComandoRele &&
possuiRespostaRele &&
rele.UltimaLeitura >= rele.UltimoComandoEnviado;
ultimaRespostaEstadoRele >= rele.UltimoComandoEnviado;
var releObj = new
{
@ -870,7 +907,7 @@ namespace AgroBase.Services.Operadores
estado_leitura = rele._EstadoLeitura,
possui_comando = possuiComandoRele,
telemetria_pos_comando = telemetriaPosComando,
telemetria_pos_comando = telemetriaPosComandoRele,
ultimo_comando_ms = idadeUltimoComandoReleMs,
ultima_resposta_ms = idadeUltimaRespostaReleMs,
@ -902,15 +939,50 @@ namespace AgroBase.Services.Operadores
})
.ToList();
DateTime ultimaRespostaLedId =
led.ValoresLeituras?
.Where(x =>x.funcao == FuncoesPinout.StatusLedID)
.OrderByDescending(x => x.atual?.lidoEm ?? DateTime.MinValue)
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
DateTime ultimaRespostaLedStatus =
led.ValoresLeituras?
.Where(x =>x.funcao == FuncoesPinout.StatusLedLeitura)
.OrderByDescending(x =>x.atual?.lidoEm ?? DateTime.MinValue)
.FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue;
bool possuiComandoLed = led.UltimoComandoEnviado != DateTime.MinValue;
bool possuiRespostaLed =
ultimaRespostaLedId != DateTime.MinValue &&
ultimaRespostaLedStatus != DateTime.MinValue;
/*
* Usa a mais antiga das duas respostas.
* Assim ultima_resposta_ms representa a idade do conjunto completo.
*/
DateTime ultimaRespostaLed =
possuiRespostaLed
? (ultimaRespostaLedId <= ultimaRespostaLedStatus ? ultimaRespostaLedId : ultimaRespostaLedStatus)
: DateTime.MinValue;
double idadeUltimoComandoLedMs =
led.UltimoComandoEnviado == DateTime.MinValue
!possuiComandoLed
? 999999
: (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds;
: Math.Max(0, (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaLedMs =
led.UltimaLeitura == DateTime.MinValue
!possuiRespostaLed
? 999999
: (agora_dt - led.UltimaLeitura).TotalMilliseconds;
: Math.Max(0, (agora_dt - ultimaRespostaLed).TotalMilliseconds);
bool telemetriaPosComandoLed =
possuiComandoLed &&
possuiRespostaLed &&
ultimaRespostaLedId >= led.UltimoComandoEnviado &&
ultimaRespostaLedStatus >= led.UltimoComandoEnviado;
int statusLedSp = led.UltimoComportamentoDesejado != null ? (int)led.UltimoComportamentoDesejado.statusLED : -1;
var ledObj = new
{
@ -920,16 +992,24 @@ namespace AgroBase.Services.Operadores
tipo,
controlar = led.Controlar,
mandatorio = led.Mandatorio,
status_led_id_sp = led.UltimoComportamentoDesejado?.id ?? -1,
status_led_sp = statusLedSp,
status_led_id = (int)led._StatusLedId,
status_led_leitura = (int)led._StatusLed,
possui_comando = possuiComandoLed,
telemetria_pos_comando = telemetriaPosComandoLed,
ultimo_comando_ms = idadeUltimoComandoLedMs,
ultima_resposta_ms = idadeUltimaRespostaLedMs,
dados,
timestamp = agora,
saude = saude ?? new { },
status_led_id_sp = led.UltimoComportamentoDesejado?.id ?? -1,
status_led_sp = led.UltimoComportamentoDesejado?.statusLED,
status_led_id = led._StatusLedId,
status_led_leitura = led._StatusLed,
ultimo_comando_ms = idadeUltimoComandoLedMs,
ultima_resposta_ms = idadeUltimaRespostaLedMs,
};
senLedsAtivos.Add($"{tipo}.{id}");

View File

@ -2,6 +2,7 @@ import json
import os
import time
import threading
import hashlib
from pathlib import Path
from datetime import datetime
@ -15,6 +16,9 @@ from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client
from camera_worker.camera_imu import IMUCamera
CAMERA_MULTISPECTRAL_VERSION = "production_v1_2026_08_24"
class CameraMultispectral:
"""
Adaptador do módulo OAK-FFC-3 multiespectral para o ecossistema do robô.
@ -28,6 +32,9 @@ class CameraMultispectral:
A classe NÃO expõe lógica radiométrica/fusão/flatfield para o worker.
Isso fica dentro do core OAK-FCC-3.
Em modo produto, hardware/resoluções/Bayer/target_size vêm exclusivamente
do module_params homologado e do hardware validado pelo Manager/Client.
"""
def __init__(
@ -41,6 +48,7 @@ class CameraMultispectral:
target_size=None,
cache_max_age_s=0.15,
timeout_s=1.0,
require_product_contract=False,
):
self.mostrar_log = mostrar_log
self.mx_id = str(mx_id) if mx_id else None
@ -61,11 +69,42 @@ class CameraMultispectral:
self.rodando = False
self.ultima_saude = {}
self.width = int(width)
self.height = int(height)
# width/height recebidos do caller são somente compatibilidade legado.
# Em produto, resolução nativa vem do module_params/hardware real.
self.legacy_width = int(width)
self.legacy_height = int(height)
self.requested_target_size = (
None
if target_size is None
else [int(target_size[0]), int(target_size[1])]
)
# Aliases históricos. São sobrescritos pelo contrato do Client e passam
# a significar exclusivamente a resolução RGB nativa de referência.
self.width = self.legacy_width
self.height = self.legacy_height
self.fps = int(fps)
self.target_size = target_size
self.target_size = self.requested_target_size
self.tensor_size = self.requested_target_size
self.timeout_s = float(timeout_s)
self.require_product_contract = bool(require_product_contract)
self.product_contract = False
self.client_contract = {}
self.sensor_size_by_role = {
"rgb": [self.legacy_width, self.legacy_height],
"re": [self.legacy_width, self.legacy_height],
"nir": [self.legacy_width, self.legacy_height],
}
self.camera_hardware = {}
self.rgb_native_size = [self.legacy_width, self.legacy_height]
self.spectral_native_size = {
"re": [self.legacy_width, self.legacy_height],
"nir": [self.legacy_width, self.legacy_height],
}
self.bayer_pattern = None
self.module_params_sha256 = None
self._cache_max_age_s = float(cache_max_age_s)
self._lock = threading.Lock()
@ -115,6 +154,9 @@ class CameraMultispectral:
module_calibration_json = self._resolver_module_params_default()
self.module_calibration_json = module_calibration_json
self.module_params_sha256 = self._sha256_file_safe(
self.module_calibration_json
)
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.CamKey(self.mx_id),
@ -137,8 +179,10 @@ class CameraMultispectral:
try:
self.client = OakFcc3Client(
mx_id=self.mx_id,
width=self.width,
height=self.height,
# Fallbacks legado. O Client produto ignora estes valores como
# autoridade física e usa o module_params homologado.
width=self.legacy_width,
height=self.legacy_height,
bayer="BGGR",
fps=self.fps,
frame_type="RAW_BRUTO",
@ -146,15 +190,31 @@ class CameraMultispectral:
capture_mode="TRIPLE",
raw_policy="require_triple",
module_calibration_json=self.module_calibration_json,
sync_mode="best",
sync_tolerance_ms=25.0,
sync_mode="strict",
hardware_sync_enabled=False,
frame_sync_master="CAM_A",
sync_tolerance_ms=15.0,
imu_modo=self.imu_modo,
imu_freq_hz=self.imu_freq_hz,
# Runtime de campo: não executa a auditoria estatística
# evaluate_frame_quality() em todo frame.
# O tensor continua passando por todo o processamento científico
# necessário (decode, radiometria, flat-field, fusão e resize).
evaluate_quality=False,
require_product_contract=self.require_product_contract,
)
# O contrato já está disponível antes do start porque o Client
# carrega/valida module_params no construtor.
self._adotar_contrato_client()
resp = self.client.start(print_debug=False)
# Reconfirma depois do Manager abrir e validar o hardware físico.
self._adotar_contrato_client()
# Se mx_id veio None, pega o ID real aberto pelo core antes de criar IMU.
try:
self.mx_id = str(getattr(self.client, "mx_id", None) or self.mx_id)
@ -235,6 +295,11 @@ class CameraMultispectral:
self.mostrar_log(f"[CameraMultispectral] Erro ao iniciar módulo: {e}")
# Produto é fail-closed: o CameraManager precisa receber o motivo
# real da falha, e não um objeto parcialmente inicializado.
if self.require_product_contract:
raise
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.CamKey(self.mx_id),
mx_id=self.mx_id,
@ -267,40 +332,204 @@ class CameraMultispectral:
# Devolve o default mesmo se não existir, para o erro ficar claro no core.
return "calibration/module_params.json"
def _sha256_file_safe(self, path):
try:
if not path or not os.path.isfile(path):
return None
h = hashlib.sha256()
with open(path, "rb") as f:
while True:
chunk = f.read(1024 * 1024)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
except Exception:
return None
@staticmethod
def _normalize_size(value, field_name):
if not isinstance(value, (list, tuple)) or len(value) != 2:
raise RuntimeError(f"{field_name} inválido: {value!r}")
w = int(value[0])
h = int(value[1])
if w <= 0 or h <= 0:
raise RuntimeError(f"{field_name} inválido: {value!r}")
return [w, h]
def _adotar_contrato_client(self):
if self.client is None:
raise RuntimeError("OakFcc3Client não inicializado")
getter = getattr(self.client, "get_contract", None)
if not callable(getter):
if self.require_product_contract:
raise RuntimeError(
"OakFcc3Client sem get_contract(); versão produto obrigatória."
)
return
contract = getter() or {}
if not isinstance(contract, dict):
raise RuntimeError(
f"Contrato do OakFcc3Client inválido: {type(contract)}"
)
product = bool(contract.get("product_contract", False))
if self.require_product_contract and not product:
raise RuntimeError(
"CameraMultispectral exige module_params de produção homologado."
)
sizes_raw = contract.get("sensor_size_by_role") or {}
sizes = {}
for role in ("rgb", "re", "nir"):
if role not in sizes_raw:
if self.require_product_contract:
raise RuntimeError(
f"Contrato do Client sem sensor_size_by_role.{role}"
)
continue
sizes[role] = self._normalize_size(
sizes_raw[role],
f"sensor_size_by_role.{role}",
)
if "rgb" in sizes:
self.rgb_native_size = list(sizes["rgb"])
self.width = int(self.rgb_native_size[0])
self.height = int(self.rgb_native_size[1])
if "re" in sizes:
self.spectral_native_size["re"] = list(sizes["re"])
if "nir" in sizes:
self.spectral_native_size["nir"] = list(sizes["nir"])
if sizes:
self.sensor_size_by_role = {
role: list(size)
for role, size in sizes.items()
}
camera_hardware = contract.get("camera_hardware") or {}
if isinstance(camera_hardware, dict):
self.camera_hardware = json.loads(
json.dumps(camera_hardware, default=str)
)
bayer = contract.get("bayer_pattern")
if bayer:
bayer = str(bayer).upper()
if bayer not in ("RGGB", "BGGR", "GRBG", "GBRG"):
raise RuntimeError(f"Bayer inválido no contrato: {bayer!r}")
self.bayer_pattern = bayer
default_target = contract.get("default_target_size")
if default_target is not None:
default_target = self._normalize_size(
default_target,
"client.default_target_size",
)
if (
self.requested_target_size is not None
and default_target is not None
and list(self.requested_target_size) != list(default_target)
):
raise RuntimeError(
"Resolução IA divergente do module_params: "
f"caller={self.requested_target_size}, "
f"module_params={default_target}"
)
self.target_size = (
list(default_target)
if default_target is not None
else (
list(self.requested_target_size)
if self.requested_target_size is not None
else list(self.rgb_native_size)
)
)
self.tensor_size = list(self.target_size)
self.product_contract = product
self.client_contract = json.loads(
json.dumps(contract, default=str)
)
def _montar_parametros(self, status=None, start_resp=None):
status = status or {}
altura_camera = 111.0
# Valores medidos por você anteriormente:
# FOV prático: 96 x 157 cm a 111 cm de altura.
# FOV prático histórico medido no conjunto mecânico.
# É mantido como referência operacional, mas não como calibração óptica.
largura_real = 157.0
altura_real = 96.0
cm_por_px_x = largura_real / float(self.width)
cm_por_px_y = altura_real / float(self.height)
tensor_w = int(self.tensor_size[0])
tensor_h = int(self.tensor_size[1])
# Para o worker, pixel operacional significa pixel do tensor entregue à IA,
# não pixel do sensor bruto. A homografia/crop pode reduzir discretamente
# a FOV real, então estes valores continuam sendo NOMINAIS.
cm_por_px_x = largura_real / float(tensor_w)
cm_por_px_y = altura_real / float(tensor_h)
parametros = {
"tipo": "multispectral",
"adapter_version": CAMERA_MULTISPECTRAL_VERSION,
"product_contract": bool(self.product_contract),
"module_calibration_json": self.module_calibration_json,
"module_params_sha256": self.module_params_sha256,
"frame_type": "RAW_BRUTO",
"channels": ["R", "G", "B", "RE", "NIR"],
"output_layout": "CHW",
"dtype": "float32",
"tensor_min": 0.0,
"tensor_max": 1.0,
"rgb_width": self.width,
"rgb_height": self.height,
"tensor_width": self.width if self.target_size is None else int(self.target_size[0]),
"tensor_height": self.height if self.target_size is None else int(self.target_size[1]),
"reference_camera": "rgb",
"rgb_native_size": list(self.rgb_native_size),
"sensor_size_by_role": {
role: list(size)
for role, size in self.sensor_size_by_role.items()
},
"camera_hardware": json.loads(
json.dumps(self.camera_hardware, default=str)
),
"bayer_pattern": self.bayer_pattern,
# Aliases compatíveis, agora explicitamente RGB nativo.
"rgb_width": int(self.rgb_native_size[0]),
"rgb_height": int(self.rgb_native_size[1]),
"tensor_size": list(self.tensor_size),
"tensor_width": tensor_w,
"tensor_height": tensor_h,
"fps": self.fps,
"altura_camera": altura_camera,
"largura_real": largura_real,
"altura_real": altura_real,
"cm_por_px_x": cm_por_px_x,
"cm_por_px_y": cm_por_px_y,
"cm_por_px_nominal": True,
"status": status,
"start_resp": start_resp,
"client_contract": json.loads(
json.dumps(self.client_contract, default=str)
),
}
return parametros
@ -557,6 +786,15 @@ class CameraMultispectral:
f"Tensor multiespectral deveria ter 5 canais, veio shape={tensor.shape}"
)
if self.target_size is not None:
expected_w = int(self.target_size[0])
expected_h = int(self.target_size[1])
if tuple(tensor.shape[1:]) != (expected_h, expected_w):
raise RuntimeError(
"Tensor final diverge do contrato de resolução: "
f"shape={tensor.shape}, esperado=(5,{expected_h},{expected_w})"
)
t_validate_ms = (time.perf_counter() - t0) * 1000.0
# ============================================================
@ -595,6 +833,13 @@ class CameraMultispectral:
"shape": list(tensor.shape),
"dtype": str(tensor.dtype),
"channels": ["R", "G", "B", "RE", "NIR"],
"product_contract": bool(self.product_contract),
"tensor_size": list(self.tensor_size),
"sensor_size_by_role": {
role: list(size)
for role, size in self.sensor_size_by_role.items()
},
"bayer_pattern": self.bayer_pattern,
"frame_id": int(meta.get("frame_id", 0) or 0),
"async_packet_seq": int(
@ -615,6 +860,22 @@ class CameraMultispectral:
},
}
#capture_perf = dict(meta.get("capture_perf", {}) or {})
#agora_log = time.monotonic()
#if not hasattr(self, "_ultimo_log_sync"):
# self._ultimo_log_sync = 0.0
#if agora_log - self._ultimo_log_sync >= 1.0:
# print(
# "[OAK_SYNC] "
# f"frame={meta.get('frame_id')} | "
# f"dt={float(meta.get('sync_dt_ms', 0.0) or 0.0):.3f}ms | "
# f"ok={bool(meta.get('sync_ok', False))} | "
# f"tol={float(meta.get('sync_tolerance_ms', 0.0) or 0.0):.3f}ms | "
# f"seq={capture_perf.get('selected_seq_by_cam')} | "
# f"ts={capture_perf.get('selected_ts_by_cam')}"
# )
# self._ultimo_log_sync = agora_log
with self._lock:
self.ultimo_tensor_multispec = tensor
self.ultimo_meta = meta
@ -674,7 +935,10 @@ class CameraMultispectral:
def requisitar_frame_rgb(self, force: bool = False, max_age_s: float = None):
"""
Retorna preview BGR uint8 do tensor multiespectral.
Retorna preview BGR uint8 gerado sob demanda a partir do Raw5 cacheado.
O hot path de inferência não paga custo de preview. O preview só é
construído quando stream/debug realmente pede um frame RGB.
"""
try:
agora = time.time()
@ -682,27 +946,58 @@ class CameraMultispectral:
max_age_s = self._cache_max_age_s
with self._lock:
cache_ok = (
preview_ok = (
self.ultimo_frame_rgb is not None
and self.timestamp_ultimo_frame_rgb is not None
and (agora - self.timestamp_ultimo_frame_rgb) < max_age_s
)
if cache_ok and not force:
if preview_ok and not force:
return self.ultimo_frame_rgb, dict(self._ultimo_resultado_rgb)
# Atualiza tensor, que por consequência atualiza preview RGB.
tensor, res = self.requisitar_tensor_multispec(force=force, max_age_s=max_age_s)
tensor_cache = self.ultimo_tensor_multispec
ts_tensor = self.timestamp_ultimo_tensor
tensor_fresco = (
tensor_cache is not None
and ts_tensor is not None
and (agora - ts_tensor) < max_age_s
)
if not tensor_fresco or force:
tensor_cache, res = self.requisitar_tensor_multispec(
force=force,
max_age_s=max_age_s,
)
if tensor_cache is None:
return None, {
"erro": res.get("erro") or "tensor indisponível para preview",
"duracao": res.get("duracao", 0.0),
"frame_valido": False,
}
t0 = time.perf_counter()
preview = self._tensor_to_preview_bgr(tensor_cache)
dur = time.perf_counter() - t0
if preview is None or preview.size <= 0:
raise RuntimeError("Falha ao gerar preview RGB a partir do Raw5.")
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": True,
"origem": "raw5_cache_on_demand",
"shape": list(preview.shape),
}
with self._lock:
if self.ultimo_frame_rgb is not None:
return self.ultimo_frame_rgb, dict(self._ultimo_resultado_rgb)
self.ultimo_frame_rgb = preview
self.timestamp_ultimo_frame_rgb = time.time()
self._ultimo_resultado_rgb = resultado
return None, {
"erro": res.get("erro") or "preview RGB indisponível",
"duracao": res.get("duracao", 0.0),
"frame_valido": False,
}
return preview, resultado
except Exception as e:
self.mostrar_log(f"[CameraMultispectral] Erro ao requisitar RGB: {e}")
@ -1341,8 +1636,12 @@ class CameraMultispectral:
core = getattr(self.client, "core", None)
if core is not None and hasattr(core, "fuse_multispec_cameras"):
t0 = time.perf_counter()
out = core.fuse_multispec_cameras(decoded_fixed, meta, 5)
out = core.resize_tensor_chw(out, target_size=self.target_size)
out = core.fuse_multispec_cameras(
decoded_fixed,
meta,
5,
target_size=self.target_size,
)
#t_core_build_ms = (time.perf_counter() - t0) * 1000.0
#t_total_ms = (time.perf_counter() - t_total0) * 1000.0
@ -1407,6 +1706,16 @@ class CameraMultispectral:
meta_item.setdefault("role", role)
meta_item.setdefault("socket", info.get("socket", cam_id))
meta_item.setdefault("sensor", info.get("sensor"))
meta_item.setdefault("width", info.get("width"))
meta_item.setdefault("height", info.get("height"))
meta_item.setdefault("native_size", info.get("native_size"))
meta_item.setdefault("bit_depth", info.get("bit_depth"))
meta_item.setdefault("raw_format", info.get("raw_format"))
if role == "rgb":
meta_item.setdefault(
"bayer_pattern",
info.get("bayer_pattern") or self.bayer_pattern,
)
fixed["meta"] = meta_item
out[cam_id] = fixed
@ -1423,6 +1732,16 @@ class CameraMultispectral:
"role": role,
"socket": info.get("socket", cam_id),
"sensor": info.get("sensor"),
"width": info.get("width"),
"height": info.get("height"),
"native_size": info.get("native_size"),
"bit_depth": info.get("bit_depth"),
"raw_format": info.get("raw_format"),
"bayer_pattern": (
info.get("bayer_pattern") or self.bayer_pattern
if role == "rgb"
else None
),
"timestamp": (meta.get("timestamps") or {}).get(cam_id) if isinstance(meta, dict) else None,
"shape": list(item.shape),
"dtype": str(item.dtype),
@ -1441,6 +1760,28 @@ class CameraMultispectral:
# Salvamento científico / pós-processamento
# ============================================================
def _processing_diagnostics_json_safe(self):
core = getattr(self.client, "core", None) if self.client is not None else None
if core is None:
return None
payload = {
"fusion_result": getattr(core, "last_fusion_result", None),
"radiometric_normalization_result": getattr(
core,
"last_radiometric_normalization_result",
None,
),
"flatfield_result": getattr(core, "last_flatfield_result", None),
"frame_quality": getattr(core, "last_frame_quality_result", None),
}
try:
return json.loads(json.dumps(payload, default=str))
except Exception:
return None
def _ts_name(self) -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
@ -1466,6 +1807,20 @@ class CameraMultispectral:
with self._lock:
raw_meta = dict(self.ultimo_meta or {})
# Reforça o contrato científico no próprio stream_meta salvo.
raw_meta.setdefault("frame_type", "RAW_BRUTO")
raw_meta.setdefault("reference_camera", "rgb")
raw_meta.setdefault("bayer_pattern", self.bayer_pattern)
raw_meta.setdefault(
"sensor_size_by_role",
{role: list(size) for role, size in self.sensor_size_by_role.items()},
)
raw_meta.setdefault(
"camera_hardware",
json.loads(json.dumps(self.camera_hardware, default=str)),
)
raw_meta.setdefault("product_contract", bool(self.product_contract))
required = {"CAM_A", "CAM_B", "CAM_C"}
presentes = set(raw_frame.keys())
faltando = sorted(required - presentes)
@ -1526,9 +1881,9 @@ class CameraMultispectral:
preview = self.client.build_save_preview_from_cam_a(
packed_raw_by_camera=raw_frame,
meta_stream=raw_meta,
sensor_width=self.width,
sensor_height=self.height,
bayer_pattern="BGGR",
sensor_width=int(self.rgb_native_size[0]),
sensor_height=int(self.rgb_native_size[1]),
bayer_pattern=self.bayer_pattern,
)
if preview is not None:
@ -1641,9 +1996,22 @@ class CameraMultispectral:
"mx_id": self.mx_id,
"module_calibration_json": self.module_calibration_json,
"sensor_width": self.width,
"sensor_height": self.height,
"bayer_pattern": "BGGR",
# Compatibilidade root: sensor_width/height representam CAM_A/RGB.
"reference_camera": "rgb",
"sensor_width": int(self.rgb_native_size[0]),
"sensor_height": int(self.rgb_native_size[1]),
"sensor_size_by_role": {
role: list(size)
for role, size in self.sensor_size_by_role.items()
},
"camera_hardware": json.loads(
json.dumps(self.camera_hardware, default=str)
),
"bayer_pattern": self.bayer_pattern,
"tensor_size": list(self.tensor_size),
"product_contract": bool(self.product_contract),
"camera_adapter_version": CAMERA_MULTISPECTRAL_VERSION,
"module_params_sha256": self.module_params_sha256,
"fps_target": self.fps,
"frame_type": "RAW_BRUTO",
@ -1662,6 +2030,10 @@ class CameraMultispectral:
"stream_meta": raw_meta,
"actual_camera_controls": self.client.get_current_camera_controls() if self.client else None,
"radiometric_last_result": self.client.get_radiometric_last_result() if self.client else None,
"client_contract": json.loads(
json.dumps(self.client_contract, default=str)
),
"processing_diagnostics": self._processing_diagnostics_json_safe(),
"resultado": resultado,
}

View File

@ -11,6 +11,9 @@ from .radiometric_controller import RadiometricController
PHYSICAL_CHANNEL_NAMES = ("R", "G", "B", "RE", "NIR")
PHYSICAL_CHANNEL_COUNT = len(PHYSICAL_CHANNEL_NAMES)
OAK_FCC3_CLIENT_VERSION = "production_v1_2026_08_24"
PRODUCT_SCHEMA = "multispec_module_params_v3"
ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
class OakFcc3Client:
@ -46,58 +49,105 @@ class OakFcc3Client:
mx_id=None,
imu_modo="rotation_vector",
imu_freq_hz=200,
evaluate_quality=True,
require_product_contract=False,
**kwargs,
):
self.width = width
self.height = height
self.bayer = bayer
self.fps = fps
self.frame_type = frame_type
self.output_dtype = output_dtype
self.capture_mode = capture_mode
self.raw_policy = raw_policy
# width/height/bayer recebidos do caller ficam apenas como fallback legado.
# Em module_params de produção, hardware real + MP são a autoridade.
self.legacy_width = int(width)
self.legacy_height = int(height)
self.legacy_bayer = str(bayer or "BGGR").upper()
self.fps = float(fps)
self.frame_type = str(frame_type).upper()
self.output_dtype = str(output_dtype).lower()
self.capture_mode = str(capture_mode).upper()
self.raw_policy = str(raw_policy).lower()
self.module_calibration_json = module_calibration_json
self.module_params = self._load_module_params(module_calibration_json)
self.require_product_contract = bool(require_product_contract)
self.module_params = self._load_module_params(
module_calibration_json,
required=self.require_product_contract,
)
self.product_contract = self._is_product_module_params(self.module_params)
if self.require_product_contract and not self.product_contract:
raise RuntimeError(
"Contrato de produção obrigatório, mas module_params não foi "
"gerado pelo assembler oficial."
)
self.fusion_config = self.module_params.get("fusion_config", {}) or {}
self.camera_hardware = self._resolve_camera_hardware()
self.sensor_size_by_role = self._resolve_sensor_size_by_role()
rgb_size = self.sensor_size_by_role.get(
"rgb",
[self.legacy_width, self.legacy_height],
)
self.rgb_native_width = int(rgb_size[0])
self.rgb_native_height = int(rgb_size[1])
# Mantidos por compatibilidade, mas agora significam RGB de referência.
self.width = self.rgb_native_width
self.height = self.rgb_native_height
self.bayer = self._resolve_bayer_pattern()
self.default_target_size = self._resolve_default_target_size()
self.imu_modo = str(imu_modo).strip().lower()
self.imu_freq_hz = int(imu_freq_hz)
self.evaluate_quality = bool(evaluate_quality)
self.mx_id = str(mx_id) if mx_id else None
# Validação antecipada do modo produto. O Manager também repete estes
# checks antes de abrir o hardware, de propósito.
self._validate_static_product_contract()
self.svc = OakFcc3Service(
timeout=10,
fps=fps,
width=width,
height=height,
frame_type=frame_type,
output_dtype=output_dtype,
capture_mode=capture_mode,
raw_policy=raw_policy,
fps=self.fps,
width=self.width,
height=self.height,
frame_type=self.frame_type,
output_dtype=self.output_dtype,
capture_mode=self.capture_mode,
raw_policy=self.raw_policy,
sync_mode=sync_mode,
sync_tolerance_ms=sync_tolerance_ms,
mx_id=self.mx_id,
module_calibration_json=module_calibration_json,
module_params=self.module_params,
require_product_contract=self.require_product_contract,
imu_modo=self.imu_modo,
imu_freq_hz=self.imu_freq_hz,
**kwargs,
)
self.applied_camera_controls = {}
self.radiometric_controller = None
self.radiometric_controller_enabled = False
self.core = RawProcessorCore(
sensor_width=width,
sensor_height=height,
bayer_pattern=bayer,
sensor_width=self.rgb_native_width,
sensor_height=self.rgb_native_height,
bayer_pattern=self.bayer,
calibration_json_path=module_calibration_json,
)
# Preview é apenas visual, porém também precisa usar o Bayer/raster
# reais do RGB para não mentir sobre a AR0234.
self.preview = RawProcessorPreview(
sensor_width=width,
sensor_height=height,
bayer_pattern=bayer,
sensor_width=self.rgb_native_width,
sensor_height=self.rgb_native_height,
bayer_pattern=self.bayer,
)
self._preview_cache = {
(self.rgb_native_width, self.rgb_native_height, self.bayer): self.preview,
}
def __enter__(self):
self.start()
@ -106,19 +156,247 @@ class OakFcc3Client:
def __exit__(self, exc_type, exc, tb):
self.stop()
def _load_module_params(self, path):
if not path or not os.path.isfile(path):
def _load_module_params(self, path, required=False):
if not path:
if required:
raise FileNotFoundError(
"module_params obrigatório no contrato de produção."
)
return {}
if not os.path.isfile(path):
if required:
raise FileNotFoundError(
f"module_params não encontrado: {path}"
)
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
data = json.load(f)
if not isinstance(data, dict):
raise RuntimeError(
f"module_params root deve ser dict/object: {path}"
)
return data
@staticmethod
def _is_product_module_params(module_params):
mp = module_params or {}
assembly = mp.get("assembly_metadata", {}) or {}
return bool(
mp.get("schema") == PRODUCT_SCHEMA
and assembly.get("schema") == ASSEMBLY_SCHEMA
and isinstance(mp.get("camera_hardware"), dict)
and isinstance(mp.get("sensor_size_by_role"), dict)
and isinstance(mp.get("calibration_provenance"), dict)
)
@staticmethod
def _normalize_size(value, label):
if not (isinstance(value, (list, tuple)) and len(value) == 2):
raise RuntimeError(f"{label} deve ser [W,H], recebido={value!r}")
w = int(value[0])
h = int(value[1])
if w <= 0 or h <= 0:
raise RuntimeError(f"{label} inválido: {value!r}")
return [w, h]
def _resolve_camera_hardware(self):
raw = self.module_params.get("camera_hardware", {}) or {}
if not self.product_contract:
return {}
out = {}
expected = {
"rgb": ("CAM_A", ("OV9782", "AR0234")),
"re": ("CAM_B", ("OV9282",)),
"nir": ("CAM_C", ("OV9282",)),
}
for role, (expected_socket, allowed_sensors) in expected.items():
item = raw.get(role)
if not isinstance(item, dict):
raise RuntimeError(f"camera_hardware sem role={role}")
socket = str(item.get("socket") or item.get("socket_name") or "").upper()
sensor = str(item.get("sensor") or item.get("sensor_name") or "").upper()
size = self._normalize_size(item.get("size"), f"camera_hardware.{role}.size")
if socket != expected_socket:
raise RuntimeError(
f"camera_hardware.{role}.socket={socket!r}, esperado={expected_socket!r}"
)
if sensor not in allowed_sensors:
raise RuntimeError(
f"camera_hardware.{role}.sensor={sensor!r}, permitidos={allowed_sensors}"
)
out[role] = {
"role": role,
"socket": socket,
"sensor": sensor,
"size": size,
}
return out
def _resolve_sensor_size_by_role(self):
if not self.product_contract:
return {
"rgb": [self.legacy_width, self.legacy_height],
"re": [self.legacy_width, self.legacy_height],
"nir": [self.legacy_width, self.legacy_height],
}
raw = self.module_params.get("sensor_size_by_role", {}) or {}
out = {}
for role in ("rgb", "re", "nir"):
size = self._normalize_size(
raw.get(role),
f"sensor_size_by_role.{role}",
)
expected = self.camera_hardware[role]["size"]
if size != expected:
raise RuntimeError(
f"sensor_size_by_role.{role}={size} != camera_hardware={expected}"
)
out[role] = size
return out
def _resolve_bayer_pattern(self):
value = self.module_params.get("bayer_pattern") if self.product_contract else None
bayer = str(value or self.legacy_bayer).upper()
if bayer not in ("RGGB", "BGGR", "GRBG", "GBRG"):
raise RuntimeError(f"bayer_pattern inválido: {bayer!r}")
return bayer
def _resolve_default_target_size(self):
value = (self.fusion_config or {}).get("target_size")
if value is None:
return None
return self._normalize_size(value, "fusion_config.target_size")
def _resolve_target_size(self, target_size):
value = target_size if target_size is not None else self.default_target_size
if value is None:
return None
size = self._normalize_size(value, "target_size")
if self.product_contract and self.default_target_size is not None:
if size != self.default_target_size:
raise RuntimeError(
"target_size solicitado diverge do module_params homologado: "
f"requested={size}, calibrated_runtime={self.default_target_size}"
)
return size
def _validate_static_product_contract(self):
if not self.product_contract:
return
if self.frame_type != "RAW_BRUTO":
raise RuntimeError(
"OakFcc3Client de produção aceita somente frame_type='RAW_BRUTO'."
)
if self.raw_policy != "require_triple":
raise RuntimeError(
"OakFcc3Client de produção exige raw_policy='require_triple'."
)
if self.capture_mode not in ("TRIPLE", "AUTO"):
raise RuntimeError(
"OakFcc3Client de produção exige capture_mode TRIPLE/AUTO."
)
expected_mx = (
(self.module_params.get("calibration_provenance", {}) or {})
.get("device_mx_id")
)
if expected_mx and self.mx_id and str(expected_mx) != self.mx_id:
raise RuntimeError(
"MX ID solicitado diverge da calibração homologada: "
f"requested={self.mx_id}, calibrated={expected_mx}"
)
def _sync_contract_from_manager_status(self, status):
if not isinstance(status, dict):
return
if self.product_contract and not bool(status.get("product_contract", False)):
raise RuntimeError(
"Client carregou module_params produto, mas Manager não reconheceu o contrato."
)
sizes = status.get("sensor_size_by_role")
if isinstance(sizes, dict):
normalized = {
role: self._normalize_size(sizes.get(role), f"manager.sensor_size_by_role.{role}")
for role in ("rgb", "re", "nir")
}
if self.product_contract and normalized != self.sensor_size_by_role:
raise RuntimeError(
"Manager e Client discordam sobre sensor_size_by_role: "
f"manager={normalized}, client={self.sensor_size_by_role}"
)
self.sensor_size_by_role = normalized
bayer = status.get("bayer_pattern")
if bayer:
bayer = str(bayer).upper()
if self.product_contract and bayer != self.bayer:
raise RuntimeError(
f"Manager Bayer={bayer} != Client/module_params={self.bayer}"
)
def get_contract(self):
return {
"client_version": OAK_FCC3_CLIENT_VERSION,
"product_contract": bool(self.product_contract),
"require_product_contract": bool(self.require_product_contract),
"sensor_size_by_role": {
role: list(size)
for role, size in self.sensor_size_by_role.items()
},
"camera_hardware": json.loads(json.dumps(self.camera_hardware)),
"bayer_pattern": self.bayer,
"default_target_size": (
None if self.default_target_size is None
else list(self.default_target_size)
),
"evaluate_quality": bool(self.evaluate_quality),
"radiometric_controller_enabled": bool(self.radiometric_controller_enabled),
}
def apply_module_camera_settings(self):
camera_settings = self.module_params.get("camera_settings", {}) or {}
applied = {}
if self.product_contract:
missing = [
role
for role in ("rgb", "re", "nir")
if not isinstance(camera_settings.get(role), dict)
]
if missing:
raise RuntimeError(
f"module_params produto sem camera_settings para: {missing}"
)
for role, settings in camera_settings.items():
applied = {}
roles = ("rgb", "re", "nir") if self.product_contract else tuple(camera_settings.keys())
for role in roles:
settings = camera_settings.get(role)
if not isinstance(settings, dict):
continue
@ -137,17 +415,43 @@ class OakFcc3Client:
}
self.applied_camera_controls = applied
if self.product_contract:
failed = {
role: value
for role, value in applied.items()
if not bool((value or {}).get("ok", False))
}
if failed:
raise RuntimeError(
f"Falha reaplicando camera_settings homologado: {failed}"
)
return applied
def enable_radiometric_controller(self):
cfg = self.module_params.get("radiometric_config", {}) or {}
enabled = bool(cfg.get("enabled", False))
# No produto atual este controller de exposição em campo é OFF.
# Não instanciamos um segundo piloto para ficar parado dentro do loop.
if not enabled:
self.radiometric_controller = None
self.radiometric_controller_enabled = False
return None
self.radiometric_controller = RadiometricController(
client=self,
config_json_path=self.module_calibration_json,
)
self.radiometric_controller.sync_from_camera_controls(self.applied_camera_controls)
self.radiometric_controller.sync_from_camera_controls(
self.applied_camera_controls
)
self.radiometric_controller.sync_from_actual_camera_controls()
self.radiometric_controller_enabled = bool(
getattr(self.radiometric_controller, "enabled", True)
)
return self.radiometric_controller
def update_radiometry(self, decoded, meta=None):
@ -187,17 +491,45 @@ class OakFcc3Client:
)
try:
self.mx_id = self.svc.manager.mx_id
status = self.svc.get_status()
self.mx_id = str(status.get("mx_id") or self.mx_id or "") or None
self._sync_contract_from_manager_status(status)
except Exception:
pass
# Se a validação de contrato falhar, fecha hardware antes de propagar.
try:
self.svc.disconnect()
except Exception:
pass
raise
# O Manager de produção já aplicou estes controles via initialControl.
# Reaplicamos após start como confirmação operacional e para manter
# compatibilidade com Managers legados durante a migração.
applied = self.apply_module_camera_settings()
self.enable_radiometric_controller()
if print_debug:
print("[OAK CLIENT] START:", resp)
print("[OAK CLIENT] CONTRACT:", self.get_contract())
print("[OAK CLIENT] APPLIED CAMERA SETTINGS:", applied)
self.enable_radiometric_controller()
if isinstance(resp, dict):
resp = dict(resp)
resp["client_version"] = OAK_FCC3_CLIENT_VERSION
resp["product_contract"] = bool(self.product_contract)
resp["sensor_size_by_role"] = {
role: list(size)
for role, size in self.sensor_size_by_role.items()
}
resp["bayer_pattern"] = self.bayer
resp["default_target_size"] = (
None if self.default_target_size is None
else list(self.default_target_size)
)
resp["radiometric_controller_enabled"] = bool(
self.radiometric_controller_enabled
)
return resp
@ -209,7 +541,29 @@ class OakFcc3Client:
return self.svc.get_device_metrics()
def get_status(self):
return self.svc.get_status()
status = self.svc.get_status()
if not isinstance(status, dict):
status = {}
else:
status = dict(status)
status.update({
"client_version": OAK_FCC3_CLIENT_VERSION,
"client_product_contract": bool(self.product_contract),
"client_require_product_contract": bool(self.require_product_contract),
"client_sensor_size_by_role": {
role: list(size)
for role, size in self.sensor_size_by_role.items()
},
"client_bayer_pattern": self.bayer,
"client_default_target_size": (
None if self.default_target_size is None
else list(self.default_target_size)
),
"evaluate_quality": bool(self.evaluate_quality),
"radiometric_controller_enabled": bool(self.radiometric_controller_enabled),
})
return status
def get_next_raw_frame(self, timeout=1.0):
return self.svc.capture_frame(timeout=timeout)
@ -224,6 +578,12 @@ class OakFcc3Client:
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
meta = dict(raw_meta)
if self.product_contract and frame_type != "RAW_BRUTO":
raise RuntimeError(
"OakFcc3Client produto recebeu frame_type não canônico: "
f"{frame_type!r}. Esperado='RAW_BRUTO'."
)
if frame_type == "RAW_BRUTO":
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
@ -320,6 +680,25 @@ class OakFcc3Client:
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
channels_expected = self._validate_physical_channel_count(channels_expected)
target_size = self._resolve_target_size(target_size)
frame_type = str(
(meta or {}).get("frame_type", self.frame_type)
if isinstance(meta, dict)
else self.frame_type
).upper()
# No runtime quente evitamos o quality audit pesado. Decodificamos e
# usamos exatamente o mesmo caminho do CameraMultispectral.
if not self.evaluate_quality and frame_type == "RAW_BRUTO":
decoded = self.core.decode_stream_cameras(frame, meta)
return self.build_infer_tensor_from_decoded(
decoded=decoded,
meta=meta,
channels_expected=channels_expected,
target_size=target_size,
)
return self.core.build_infer_tensor_from_stream(
frame,
meta,
@ -327,19 +706,57 @@ class OakFcc3Client:
target_size=target_size,
)
def build_infer_tensor_from_decoded(self, decoded, meta, channels_expected, target_size=None):
def build_infer_tensor_from_decoded(
self,
decoded,
meta,
channels_expected,
target_size=None,
evaluate_quality=None,
):
channels_expected = self._validate_physical_channel_count(channels_expected)
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
tensor = self.core.resize_tensor_chw(tensor, target_size=target_size)
target_size = self._resolve_target_size(target_size)
if evaluate_quality is None:
evaluate_quality = self.evaluate_quality
evaluate_quality = bool(evaluate_quality)
# Core novo faz a geometria source->target em uma única etapa.
# NÃO redimensionar novamente depois da fusão.
tensor = self.core.fuse_multispec_cameras(
decoded,
meta,
channels_expected,
target_size=target_size,
)
patch_cfg = getattr(
self.core,
"patch_normalization_config",
{},
) or {}
# Mantém paridade com build_infer_tensor_from_stream: se a calibração
# habilitar patch normalization, ela também vale no caminho decoded.
patch_cfg = getattr(self.core, "patch_normalization_config", {}) or {}
if bool(patch_cfg.get("enabled", False)):
if self.product_contract:
raise RuntimeError(
"patch_normalization não é permitido no contrato produto."
)
tensor = self.core.apply_patch_normalization_to_tensor(tensor)
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
return tensor
if evaluate_quality:
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
else:
# Evita deixar resultado antigo no objeto e evita percentis no hot path.
self.core.last_frame_quality_result = {
"status": "skipped",
"usable_for_training": None,
"reason": "disabled_by_oak_fcc3_client",
"client_version": OAK_FCC3_CLIENT_VERSION,
}
return np.ascontiguousarray(
tensor.astype(np.float32, copy=False)
)
def decode_stream_cameras(self, frame, meta):
if str(meta.get("frame_type", self.frame_type)).upper() == "PREVIEW":
@ -382,12 +799,19 @@ class OakFcc3Client:
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
def build_multispec_tensor(self, decoded, meta=None, target_size=None):
def build_multispec_tensor(
self,
decoded,
meta=None,
target_size=None,
evaluate_quality=None,
):
tensor = self.build_infer_tensor_from_decoded(
decoded=decoded,
meta=meta,
channels_expected=5,
target_size=target_size,
evaluate_quality=evaluate_quality,
)
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
@ -448,10 +872,14 @@ class OakFcc3Client:
arr = arr[:, :, 0]
if bit_depth == 10 and arr.ndim == 2:
role_size = self.sensor_size_by_role.get(
str(role).lower(),
[self.rgb_native_width, self.rgb_native_height],
)
raw16 = self.core.unpack_raw10_packed(
arr,
sensor_width=int(info.get("width", self.width)),
sensor_height=int(info.get("height", self.height)),
sensor_width=int(info.get("width", role_size[0])),
sensor_height=int(info.get("height", role_size[1])),
)
if role == "rgb":
@ -513,7 +941,7 @@ class OakFcc3Client:
or cam_meta.get("bayer")
or stream_meta.get("bayer_pattern")
or bayer_pattern
or "RGGB"
or self.bayer
)
bayer = str(bayer).upper()
@ -545,19 +973,17 @@ class OakFcc3Client:
real_w = int(cam_meta.get("width", sensor_width))
real_h = int(cam_meta.get("height", sensor_height))
core = RawProcessorCore(
sensor_width=real_w,
sensor_height=real_h,
bayer_pattern=bayer,
)
cache_key = (real_w, real_h, bayer)
preview = self._preview_cache.get(cache_key)
if preview is None:
preview = RawProcessorPreview(
sensor_width=real_w,
sensor_height=real_h,
bayer_pattern=bayer,
)
self._preview_cache[cache_key] = preview
preview = RawProcessorPreview(
sensor_width=real_w,
sensor_height=real_h,
bayer_pattern=bayer,
)
raw16 = core.unpack_raw10_packed(
raw16 = self.core.unpack_raw10_packed(
packed,
sensor_width=real_w,
sensor_height=real_h,
@ -624,6 +1050,11 @@ class OakFcc3Client:
def decode_oak_aligned_multispec(self, frame, meta):
if self.product_contract:
raise RuntimeError(
"MULTISPEC alinhado pela OAK é legado e não faz parte do contrato produto."
)
"""
Decodifica frames já alinhados pela OAK.
@ -672,6 +1103,11 @@ class OakFcc3Client:
return decoded
def build_multispec_tensor_from_oak_aligned(self, decoded, meta=None):
if self.product_contract:
raise RuntimeError(
"Tensor MULTISPEC pré-alinhado pela OAK é legado no contrato produto."
)
"""
Monta CHW [R,G,B,RE,NIR] sem reaplicar homografia.
"""

View File

@ -1,95 +1,634 @@
# camera_worker/oak_fcc3_core/raw_processor_preview.py
# -*- coding: utf-8 -*-
"""
RawProcessorPreview - Production
================================
Conversor EXCLUSIVAMENTE visual para RAW Bayer da câmera RGB.
Este módulo:
- NÃO participa do tensor científico;
- NÃO altera RAW salvo;
- NÃO aplica Flat-Field;
- NÃO aplica Radiometric Normalization;
- NÃO aplica Homography;
- NÃO deve ser usado para treino ou inferência.
Contrato oficial:
CAM_A = RGB
OV9782 -> 1280x800
AR0234 -> 1920x1200
A resolução e o Bayer devem vir do module_params/Client já validados.
Pipeline de preview:
RAW Bayer uint16
-> robust display levels
-> uint8
-> demosaic BGR
-> gray-world opcional
-> contraste opcional
-> JPEG/preview
Importante:
O resultado desta classe é BGR, compatível com OpenCV.
"""
from __future__ import annotations
from typing import Optional
import cv2
import numpy as np
from typing import Optional
RAW_PROCESSOR_PREVIEW_VERSION = "production_v1_2026_08_24"
SUPPORTED_BAYER_PATTERNS = (
"RGGB",
"BGGR",
"GRBG",
"GBRG",
)
SUPPORTED_BIT_DEPTHS = (
8,
10,
12,
16,
)
class RawProcessorPreview:
def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"):
self.sensor_width = sensor_width
self.sensor_height = sensor_height
self.bayer_pattern = bayer_pattern.upper()
"""
Preview RAW Bayer para debug/stream/salvamento visual.
Parâmetros
----------
sensor_width:
Largura NATIVA do RGB de referência.
sensor_height:
Altura NATIVA do RGB de referência.
bayer_pattern:
Bayer real da CAM_A, vindo do contrato homologado.
sensor_name:
Opcional, somente rastreabilidade.
strict_shape:
Se True, RAW recebido deve possuir exatamente HxW nativo.
Recomendado e default no produto.
"""
def __init__(
self,
sensor_width: int,
sensor_height: int,
bayer_pattern: str = "GBRG",
sensor_name: Optional[str] = None,
strict_shape: bool = True,
):
self.sensor_width = int(sensor_width)
self.sensor_height = int(sensor_height)
if self.sensor_width <= 0 or self.sensor_height <= 0:
raise ValueError(
"Resolução inválida para preview: "
f"{self.sensor_width}x{self.sensor_height}"
)
self.bayer_pattern = str(
bayer_pattern
).strip().upper()
if self.bayer_pattern not in SUPPORTED_BAYER_PATTERNS:
raise ValueError(
f"Padrão Bayer não suportado: {self.bayer_pattern}. "
f"Suportados={SUPPORTED_BAYER_PATTERNS}"
)
self.sensor_name = (
str(sensor_name).strip().upper()
if sensor_name
else None
)
self.strict_shape = bool(
strict_shape
)
# ============================================================
# Contrato
# ============================================================
def get_contract(self) -> dict:
return {
"preview_version": RAW_PROCESSOR_PREVIEW_VERSION,
"sensor_name": self.sensor_name,
"sensor_width": int(
self.sensor_width
),
"sensor_height": int(
self.sensor_height
),
"sensor_size": [
int(
self.sensor_width
),
int(
self.sensor_height
),
],
"bayer_pattern": (
self.bayer_pattern
),
"strict_shape": bool(
self.strict_shape
),
"output_color_order": "BGR",
"scientific_effect": "none",
}
# ============================================================
# Validação
# ============================================================
def _validate_raw(
self,
raw16: np.ndarray,
bit_depth: int,
) -> np.ndarray:
if raw16 is None:
raise ValueError(
"RAW de preview é None."
)
raw = np.asarray(
raw16
)
if raw.ndim != 2:
raise ValueError(
"RAW Bayer deve ser 2D HxW; "
f"shape={raw.shape}"
)
if self.strict_shape:
expected = (
self.sensor_height,
self.sensor_width,
)
if raw.shape != expected:
raise ValueError(
"RAW Bayer com shape incompatível: "
f"recebido={raw.shape}, esperado={expected}"
)
bit_depth = int(
bit_depth
)
if bit_depth not in SUPPORTED_BIT_DEPTHS:
raise ValueError(
f"bit_depth não suportado: {bit_depth}. "
f"Suportados={SUPPORTED_BIT_DEPTHS}"
)
if not np.issubdtype(
raw.dtype,
np.integer,
):
raise ValueError(
"RAW Bayer para preview deve ser inteiro; "
f"dtype={raw.dtype}"
)
return raw
@staticmethod
def _validate_levels(
black_level,
white_level,
max_val,
):
black = (
None
if black_level is None
else float(
black_level
)
)
white = (
None
if white_level is None
else float(
white_level
)
)
if (
black is not None
and not np.isfinite(
black
)
):
raise ValueError(
f"black_level inválido: {black_level}"
)
if (
white is not None
and not np.isfinite(
white
)
):
raise ValueError(
f"white_level inválido: {white_level}"
)
if black is not None:
black = float(
np.clip(
black,
0.0,
max_val,
)
)
if white is not None:
white = float(
np.clip(
white,
0.0,
max_val,
)
)
return black, white
# ============================================================
# RAW -> display uint8
# ============================================================
def raw16_to_vis8(
self, raw16: np.ndarray,
black_level: Optional[int] = None,
white_level: Optional[int] = None,
gamma: float = 2.2,
bit_depth: int = 10
self,
raw16: np.ndarray,
black_level: Optional[float] = None,
white_level: Optional[float] = None,
gamma: float = 2.2,
bit_depth: int = 10,
auto_low_percentile: float = 0.5,
auto_high_percentile: float = 99.5,
) -> np.ndarray:
"""
Conversão para visualização:
- auto-level
- gamma
Converte RAW Bayer para uint8 de DISPLAY.
Quando níveis não são informados, usa percentis robustos em vez de
min/max para evitar que hot pixels ou pequenos pontos saturados lavem
todo o preview.
Isso é somente estética visual.
"""
max_val = float((1 << bit_depth) - 1)
raw = self._validate_raw(
raw16,
bit_depth,
)
raw = raw16.astype(np.float32)
max_val = float(
(1 << int(bit_depth))
- 1
)
if black_level is None:
black_level = float(raw.min())
if white_level is None:
white_level = float(raw.max())
black, white = (
self._validate_levels(
black_level,
white_level,
max_val,
)
)
if white_level <= black_level:
norm = raw / max_val
low_p = float(
auto_low_percentile
)
high_p = float(
auto_high_percentile
)
if not (
0.0 <= low_p
< high_p
<= 100.0
):
raise ValueError(
"Percentis automáticos inválidos: "
f"{low_p}, {high_p}"
)
raw_f = raw.astype(
np.float32,
copy=False,
)
# Amostragem determinística para previews grandes.
# Evita percentil full-frame de 2.3M pixels em todo snapshot.
total = raw_f.size
max_samples = 250_000
if total > max_samples:
step = max(
1,
total // max_samples,
)
sample = raw_f.reshape(
-1
)[::step]
else:
norm = (raw - black_level) / (white_level - black_level)
sample = raw_f.reshape(
-1
)
norm = np.clip(norm, 0.0, 1.0)
if black is None:
black = float(
np.percentile(
sample,
low_p,
)
)
if gamma is not None and gamma > 0:
norm = np.power(norm, 1.0 / gamma)
if white is None:
white = float(
np.percentile(
sample,
high_p,
)
)
return (norm * 255.0).clip(0, 255).astype(np.uint8)
if white <= black:
# Caso frame praticamente constante.
if max_val <= 0.0:
norm = np.zeros_like(
raw_f,
dtype=np.float32,
)
else:
norm = raw_f / max_val
else:
norm = (
raw_f - black
) / (
white - black
)
norm = np.clip(
norm,
0.0,
1.0,
)
if gamma is not None:
gamma = float(
gamma
)
if not np.isfinite(
gamma
) or gamma <= 0.0:
raise ValueError(
f"gamma inválido: {gamma}"
)
norm = np.power(
norm,
1.0 / gamma,
)
return np.clip(
norm * 255.0,
0.0,
255.0,
).astype(
np.uint8
)
# ============================================================
# Bayer
# ============================================================
def _debayer_code(self):
"""
Retorna código OpenCV que produz BGR.
A versão antiga usava COLOR_Bayer*2RGB_EA e em seguida tratava o
resultado como BGR, podendo trocar vermelho/azul no preview.
"""
# IMPORTANTE:
# Os aliases Bayer do OpenCV são contraintuitivos em relação ao
# nome físico 2x2 que usamos no produto. O mapeamento abaixo foi
# validado com mosaicos sintéticos de cor conhecida e produz BGR:
#
# físico RGGB -> OpenCV BayerBG2BGR
# físico BGGR -> OpenCV BayerRG2BGR
# físico GRBG -> OpenCV BayerGB2BGR
# físico GBRG -> OpenCV BayerGR2BGR
#
# Não "simplifique" este mapa pela semelhança dos nomes.
mapping = {
"RGGB": cv2.COLOR_BayerRG2RGB_EA,
"BGGR": cv2.COLOR_BayerBG2RGB_EA,
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
}
"RGGB": (
cv2.COLOR_BayerBG2BGR_EA
),
"BGGR": (
cv2.COLOR_BayerRG2BGR_EA
),
"GRBG": (
cv2.COLOR_BayerGB2BGR_EA
),
"GBRG": (
cv2.COLOR_BayerGR2BGR_EA
),
}
if self.bayer_pattern not in mapping:
raise ValueError(f"Padrão Bayer não suportado: {self.bayer_pattern}")
return mapping[
self.bayer_pattern
]
return mapping[self.bayer_pattern]
# ============================================================
# Ajustes VISUAIS
# ============================================================
def apply_preview_white_balance(self, bgr: np.ndarray, strength: float = 1.0) -> np.ndarray:
@staticmethod
def apply_preview_white_balance(
bgr: np.ndarray,
strength: float = 1.0,
) -> np.ndarray:
"""
Gray-world simples para deixar o preview mais agradável.
Não usar no raw de treino.
Gray-world simples somente para deixar o preview legível.
Não usar no RAW científico.
"""
img = bgr.astype(np.float32)
img = np.asarray(
bgr
)
mean_b = float(img[:, :, 0].mean())
mean_g = float(img[:, :, 1].mean())
mean_r = float(img[:, :, 2].mean())
if (
img.ndim != 3
or img.shape[2] != 3
):
raise ValueError(
"Preview WB exige BGR HxWx3; "
f"shape={img.shape}"
)
mean_gray = (mean_b + mean_g + mean_r) / 3.0
strength = float(
strength
)
if not np.isfinite(
strength
):
raise ValueError(
f"strength inválido: {strength}"
)
strength = float(
np.clip(
strength,
0.0,
1.0,
)
)
work = img.astype(
np.float32,
copy=True,
)
# Amostragem reduz custo em 1920x1200.
h, w = work.shape[:2]
sample_step = max(
1,
int(
np.sqrt(
(h * w)
/ 200_000.0
)
),
)
sample = work[
::sample_step,
::sample_step,
]
mean_b = float(
sample[:, :, 0].mean()
)
mean_g = float(
sample[:, :, 1].mean()
)
mean_r = float(
sample[:, :, 2].mean()
)
mean_gray = (
mean_b
+ mean_g
+ mean_r
) / 3.0
eps = 1e-6
gain_b = mean_gray / max(mean_b, eps)
gain_g = mean_gray / max(mean_g, eps)
gain_r = mean_gray / max(mean_r, eps)
# strength=1 aplica total, strength=0 não aplica
gain_b = 1.0 + (gain_b - 1.0) * strength
gain_g = 1.0 + (gain_g - 1.0) * strength
gain_r = 1.0 + (gain_r - 1.0) * strength
gain_b = mean_gray / max(
mean_b,
eps,
)
img[:, :, 0] *= gain_b
img[:, :, 1] *= gain_g
img[:, :, 2] *= gain_r
gain_g = mean_gray / max(
mean_g,
eps,
)
return np.clip(img, 0, 255).astype(np.uint8)
gain_r = mean_gray / max(
mean_r,
eps,
)
def apply_preview_contrast(self, bgr: np.ndarray, alpha: float = 1.08, beta: float = 0.0) -> np.ndarray:
gain_b = 1.0 + (
gain_b - 1.0
) * strength
gain_g = 1.0 + (
gain_g - 1.0
) * strength
gain_r = 1.0 + (
gain_r - 1.0
) * strength
work[:, :, 0] *= gain_b
work[:, :, 1] *= gain_g
work[:, :, 2] *= gain_r
return np.clip(
work,
0.0,
255.0,
).astype(
np.uint8
)
@staticmethod
def apply_preview_contrast(
bgr: np.ndarray,
alpha: float = 1.08,
beta: float = 0.0,
) -> np.ndarray:
"""
Ajuste leve de contraste/brilho para preview.
Contraste/brilho de DISPLAY.
"""
out = cv2.convertScaleAbs(bgr, alpha=alpha, beta=beta)
return out
alpha = float(
alpha
)
beta = float(
beta
)
if (
not np.isfinite(
alpha
)
or alpha <= 0.0
):
raise ValueError(
f"alpha inválido: {alpha}"
)
if not np.isfinite(
beta
):
raise ValueError(
f"beta inválido: {beta}"
)
return cv2.convertScaleAbs(
bgr,
alpha=alpha,
beta=beta,
)
# ============================================================
# Preview completo
# ============================================================
def raw16_to_preview_bgr(
self,
@ -99,28 +638,89 @@ class RawProcessorPreview:
apply_wb: bool = True,
apply_contrast: bool = True,
bit_depth: int = 10,
black_level: Optional[float] = None,
white_level: Optional[float] = None,
) -> np.ndarray:
"""
Pipeline de preview bonito:
1. auto-level + gamma no mosaico
2. demosaic
3. white balance simples
4. leve contraste final
Pipeline visual:
1. robust levels + gamma no mosaico
2. demosaic BGR
3. gray-world opcional
4. contraste opcional
"""
vis8 = self.raw16_to_vis8(raw16, gamma=gamma, bit_depth=bit_depth)
bgr = cv2.cvtColor(vis8, self._debayer_code())
vis8 = self.raw16_to_vis8(
raw16,
black_level=black_level,
white_level=white_level,
gamma=gamma,
bit_depth=bit_depth,
)
bgr = cv2.cvtColor(
vis8,
self._debayer_code(),
)
if apply_wb:
bgr = self.apply_preview_white_balance(bgr, strength=wb_strength)
bgr = (
self.apply_preview_white_balance(
bgr,
strength=wb_strength,
)
)
if apply_contrast:
bgr = self.apply_preview_contrast(bgr, alpha=1.08, beta=0.0)
bgr = (
self.apply_preview_contrast(
bgr,
alpha=1.08,
beta=0.0,
)
)
return bgr
return np.ascontiguousarray(
bgr,
dtype=np.uint8,
)
def raw16_to_preview_jpg_bytes(
self,
raw16: np.ndarray,
jpeg_quality: int = 95,
**preview_kwargs,
) -> bytes:
quality = int(
jpeg_quality
)
if not (
1 <= quality <= 100
):
raise ValueError(
f"jpeg_quality inválido: {quality}"
)
bgr = (
self.raw16_to_preview_bgr(
raw16,
**preview_kwargs,
)
)
ok, enc = cv2.imencode(
".jpg",
bgr,
[
int(
cv2.IMWRITE_JPEG_QUALITY
),
quality,
],
)
def raw16_to_preview_jpg_bytes(self, raw16: np.ndarray, jpeg_quality: int = 95) -> bytes:
bgr = self.raw16_to_preview_bgr(raw16)
ok, enc = cv2.imencode(".jpg", bgr, [int(cv2.IMWRITE_JPEG_QUALITY), int(jpeg_quality)])
if not ok:
raise RuntimeError("Falha ao codificar preview JPG")
raise RuntimeError(
"Falha ao codificar preview JPG."
)
return enc.tobytes()

View File

@ -149,7 +149,7 @@ class SegformerNavRunner:
"trt_fp16_enable": bool(self.config.get("trt_fp16_enable", True)),
"trt_engine_cache_enable": bool(self.config.get("trt_engine_cache_enable", True)),
"trt_engine_cache_path": str(
self.config.get("trt_engine_cache_path", "./trt_cache_visual_worker")
self.config.get("trt_engine_cache_path", "./trt_cache_weed_worker")
),
}

View File

@ -1402,13 +1402,17 @@ class ModuloAtuador(ModuloDiagnosticoBase):
calibrando = ctx["calibrando"]
bomba_principal = ctx["bomba_principal"]
autonomia_corredor_aplicavel = bool(
auto
and ctx["operacao_iniciada"]
and not calibrando
and not ctx["finalizando"]
)
herbicida_pode_bloquear = bool(
auto and
ctx["operacao_iniciada"] and
not calibrando and
not ctx["finalizando"] and
not ctx["pausa"] and
not ctx["emergencia"]
autonomia_corredor_aplicavel
and not ctx["pausa"]
and not ctx["emergencia"]
)
em_transicao = self._pulverizacao_em_transicao(ctx)
@ -1491,7 +1495,7 @@ class ModuloAtuador(ModuloDiagnosticoBase):
})
score = min(score, 90)
if not ctx["herbicida_ok_corredor"]:
if (autonomia_corredor_aplicavel and not ctx["herbicida_ok_corredor"]):
motivo = self._texto_ou_padrao(
ctx.get("autonomia_motivo"),
ctx.get("motivo_herbicida_corredor"),

View File

@ -172,6 +172,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
motivos_gerais = []
condicoes_gerais = []
drivers_com_travamento = []
saude_total_ativos = 0.0
total_drivers_em_uso = 0
algum_conectado = False
@ -209,6 +210,17 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
conectado = self._bool(saude_mod.get("conectado_efetivo", False))
em_uso = self._bool(saude_mod.get("em_uso", True))
detalhes_valores = self._as_dict(saude_mod.get("detalhes_valores", {}))
if self._bool(detalhes_valores.get("travamento_confirmado", False)):
drivers_com_travamento.append({
"id": endereco_str,
"label": saude_mod.get("label"),
"rpm": detalhes_valores.get("rpm", 0),
"rpm_sp": detalhes_valores.get("rpm_sp", 0),
"corrente_motor": detalhes_valores.get("corrente_motor", 0),
"nivel": detalhes_valores.get("travamento_nivel"),
})
if conectado:
algum_conectado = True
@ -253,6 +265,9 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
"drivers_ativos": ativos,
"drivers_em_uso": total_drivers_em_uso,
"modelo": "movimentacao_inteligente_por_resposta_comando_valores_e_telemetria",
"travamento_confirmado": len(drivers_com_travamento) > 0,
"drivers_com_travamento": drivers_com_travamento
},
)
@ -924,6 +939,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
"temperatura_motor": temperatura_motor,
"temperatura_driver": temperatura_driver,
"score_valores": int(max(0, min(100, round(score)))),
# Novo contrato
"travamento_detectado": possivel_travamento,
"travamento_confirmado": estado_travamento["nivel"] == "confirmado",
"travamento_nivel": estado_travamento["nivel"],
"travamento_duracao_s": estado_travamento.get("duracao_s", 0.0),
}
return {

View File

@ -25,6 +25,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
COMANDO_GRACE_MS = 2000.0
COMANDO_ALERTA_MS = 3000.0
COMANDO_FALHA_MS = 7000.0
COMANDO_RECUPERACAO_MS = 1500.0
COMPONENTE_RECUPERACAO_GRACE_MS = 3000.0
MODULO_RECUPERACAO_GRACE_MS = 10000.0
@ -82,7 +83,10 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
updates[f"sensores.{item['key']}.saude"] = s
for item in servos:
s = self._avaliar_servo(item)
s = self._avaliar_servo(
item,
modulo_disponivel=sen_disponivel,
)
saude_individual.append(s)
updates[f"servos.{item['key']}.saude"] = s
@ -92,7 +96,10 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
updates[f"reles.{item['key']}.saude"] = s
for item in sinaleiros:
s = self._avaliar_sinaleiro(item)
s = self._avaliar_sinaleiro(
item,
modulo_disponivel=sen_disponivel,
)
saude_individual.append(s)
updates[f"sinaleiros.{item['key']}.saude"] = s
@ -239,12 +246,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
corrente_36v = self._valor_sensor(sensores, "AB36V", "Corrente")
temp_bateria = self._valor_sensor(sensores, "TBAT", "Temperatura")
temps_motor = []
for s in sensores:
label = str(s.get("dados", {}).get("label", ""))
if label.startswith("TMV"):
temps_motor.append(self._float(self._campo_atual(s["dados"], "Temperatura", 0), 0))
return {
"bms_ligado": bms_ligado,
"percent_bateria": percent_bateria,
@ -257,7 +258,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
"tensao_36v": tensao_36v,
"corrente_36v": corrente_36v,
"temp_bateria": temp_bateria,
"temperaturas_motor": temps_motor,
"debug": {
"bms_ligado": bms_ligado,
"percent_bateria": percent_bateria,
@ -267,7 +267,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
"sensor_ab36v_em_uso": ab36v_em_uso,
"tensao_36v": tensao_36v,
"corrente_36v": corrente_36v,
"temp_motor_max": max(temps_motor) if temps_motor else 0,
"temp_bateria": temp_bateria,
},
}
@ -281,7 +280,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
conectado = self._bool(dados.get("conectado", False))
aferir = self._bool(dados.get("aferir", False))
mandatorio = self._bool(dados.get("mandatorio", False))
em_uso = aferir
delegado_ao_mov = str(dados.get("label", "")).upper().startswith("TMV")
em_uso = aferir and not delegado_ao_mov
motivos = []
condicoes = []
@ -298,7 +298,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
saude = int(max(0, min(100, round(saude))))
status = self._resolver_status_item(conectado, saude, condicoes)
debug = {"resposta": resposta_score, "valores": valor_debug, "telemetria": telemetria["parametros"]}
debug = {
"resposta": resposta_score,
"valores": valor_debug,
"telemetria": telemetria["parametros"],
"avaliacao_delegada_ao_mov": delegado_ao_mov,
}
return self._item(item, status, saude, motivos, condicoes, em_uso, debug, "sensor")
def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes):
@ -403,8 +408,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
label = str(dados.get("label", ""))
if label == "AB36V" and ctx["sensor_ab36v_em_uso"]:
return ["Tensao", "Corrente", "Temperatura"]
if label.startswith("TMV"):
return ["Temperatura"]
if label == "TBAT" and not ctx["bms_ligado"]:
return ["Temperatura"]
if mandatorio:
@ -415,73 +418,254 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
# Atuadores auxiliares
# ============================================================
def _avaliar_servo(self, item):
def _avaliar_servo(self, item, modulo_disponivel=True):
dados = item["dados"]
conectado = self._bool(dados.get("conectado", False))
controlar = self._bool(dados.get("controlar", False))
if not conectado:
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], controlar, {}, "servo")
runtime, suspenso = self._preparar_atuador_auxiliar(
item=item,
grupo="servo",
nome="Servo SEN",
modulo_disponivel=modulo_disponivel,
)
if suspenso is not None:
return suspenso
motivos, condicoes = [], []
resposta = self._score_resposta(dados, motivos, condicoes, "Servo SEN")
comando, dbg_cmd = self._avaliar_comando_servo(item, motivos, condicoes)
telem = self._avaliar_telemetria(dados, motivos, ["ServoAnguloLeitura"] if controlar else [])
comando, dbg_cmd = self._avaliar_comando_servo(
item,
motivos,
condicoes,
runtime,
)
telem = self._avaliar_telemetria(
dados,
motivos,
["ServoAnguloLeitura"] if controlar else [],
)
saude = resposta * 0.25 + comando * 0.60 + telem["score"] * 0.15
if any(self._is_falha_hardware(c) for c in condicoes):
saude = min(saude, 45)
saude = int(max(0, min(100, round(saude))))
return self._item(item, self._resolver_status_item(conectado, saude, condicoes), saude, motivos, condicoes, controlar, {"resposta": resposta, "comando": dbg_cmd, "telemetria": telem["parametros"]}, "servo")
def _avaliar_comando_servo(self, item, motivos, condicoes):
return self._item(
item,
self._resolver_status_item(True, saude, condicoes),
saude,
motivos,
condicoes,
controlar,
{
"resposta": resposta,
"comando": dbg_cmd,
"telemetria": telem["parametros"],
},
"servo",
)
def _avaliar_comando_servo(self, item, motivos, condicoes, runtime):
dados = item["dados"]
label = dados.get("label")
controlar = self._bool(dados.get("controlar", False))
angulo_sp = self._float(dados.get("angulo_desejado", dados.get("angulo_sp", 0)), 0)
angulo = self._float(dados.get("angulo_leitura", self._campo_atual(dados, "ServoAnguloLeitura", -1)), -1)
ultimo_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999)
tem_idade = ultimo_ms < 900000
if angulo < 0:
motivos.append("Servo sem leitura de ângulo")
condicoes.append({
"label": "Servo SEN",
"valor": angulo,
"severidade": 70 if controlar else 35,
"classe": "processo",
"descricao": f"Servo {label} sem leitura de ângulo válida",
"acoes": ["Verificar retorno de ServoAnguloLeitura."],
})
return (60 if controlar else 85), {"angulo": angulo, "angulo_sp": angulo_sp, "sem_leitura": True}
desejado_presente = (
"angulo_desejado" in dados or
"angulo_sp" in dados
)
leitura_presente = (
"angulo_leitura" in dados or
self._campo_existe(dados, "ServoAnguloLeitura")
)
angulo_sp = self._float(
dados.get("angulo_desejado", dados.get("angulo_sp", 0.0)),
0.0,
)
angulo = self._float(
dados.get(
"angulo_leitura",
self._campo_atual(dados, "ServoAnguloLeitura", -1.0),
),
-1.0,
)
evidencia = self._evidencia_comando(dados)
debug = {
"controlar": controlar,
"angulo": angulo,
"angulo_sp": angulo_sp,
**evidencia,
}
if not controlar:
self._resetar_estado_comando(runtime, manter_desejado=False)
return 100, debug
if not desejado_presente or not leitura_presente:
self._resetar_estado_comando(runtime, manter_desejado=False)
debug["contrato_comando_completo"] = False
return 92, debug
token_desejado = round(angulo_sp, 3)
self._registrar_desejado(runtime, token_desejado)
erro = abs(angulo - angulo_sp)
if not controlar or erro <= self.SERVO_TOL_GRAUS:
return 100, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "controlar": controlar}
if tem_idade and ultimo_ms < self.COMANDO_GRACE_MS:
return 96, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
if erro >= self.SERVO_FALHA_GRAUS and tem_idade and ultimo_ms >= self.COMANDO_FALHA_MS:
motivos.append(f"Servo {label} não convergiu para o SP ({angulo:.1f}/{angulo_sp:.1f})")
condicoes.append({
"label": "Servo SEN",
"valor": angulo,
"severidade": 92,
"classe": "falha_hardware",
"descricao": f"Servo {label} não acompanhou o comando de ângulo",
"acoes": ["Verificar travamento mecânico ou alimentação do servo."],
})
return 25, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
if erro >= self.SERVO_ALERTA_GRAUS:
motivos.append(f"Servo {label} distante do SP ({angulo:.1f}/{angulo_sp:.1f})")
condicoes.append({
"label": "Servo SEN",
"valor": angulo,
"severidade": min(89, max(45, int(erro * 4))),
"classe": "processo",
"descricao": f"Servo {label} distante do ângulo desejado",
"acoes": ["Acompanhar se o erro persiste."],
})
return 70, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
motivos.append(f"Servo {label} fora da tolerância momentaneamente ({erro:.1f}°)")
return 88, {"angulo": angulo, "angulo_sp": angulo_sp, "erro": erro, "ultimo_comando_ms": ultimo_ms}
debug["erro"] = erro
if erro <= self.SERVO_TOL_GRAUS:
return self._confirmar_recuperacao_servo(
runtime,
label,
angulo,
angulo_sp,
erro,
motivos,
condicoes,
debug,
)
runtime["coerencia_desde"] = None
if not evidencia["possui_comando"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug
if not evidencia["telemetria_pos_comando"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
return 96, debug
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "período de resposta ao comando"
return 96, debug
agora = time.monotonic()
tempo_retorno_ms = (
agora - runtime["componente_recuperado_em"]
) * 1000.0
if tempo_retorno_ms < self.COMPONENTE_RECUPERACAO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "estabilização após reconexão"
debug["tempo_desde_retorno_componente_ms"] = tempo_retorno_ms
return 96, debug
if runtime["divergencia_desde"] is None:
runtime["divergencia_desde"] = agora
tempo_divergente_ms = (
agora - runtime["divergencia_desde"]
) * 1000.0
debug["tempo_divergente_ms"] = tempo_divergente_ms
if runtime["nivel_divergencia"] == "falha":
return self._falha_servo(
label, angulo, angulo_sp, erro,
motivos, condicoes, debug,
)
if tempo_divergente_ms < self.COMANDO_ALERTA_MS:
return 96, debug
if (
erro >= self.SERVO_FALHA_GRAUS and
tempo_divergente_ms >= self.COMANDO_FALHA_MS
):
runtime["nivel_divergencia"] = "falha"
return self._falha_servo(
label, angulo, angulo_sp, erro,
motivos, condicoes, debug,
)
runtime["nivel_divergencia"] = "alerta"
motivos.append(
f"Servo {label} distante do SP ({angulo:.1f}/{angulo_sp:.1f})"
)
condicoes.append({
"label": "Servo SEN",
"valor": angulo,
"severidade": min(89, max(55, int(erro * 4))),
"classe": "processo",
"descricao": f"Servo {label} não convergiu para o ângulo desejado",
"acoes": [
"Acompanhar a convergência do servo.",
"Verificar esforço mecânico se o erro permanecer.",
],
})
return (68 if erro >= self.SERVO_ALERTA_GRAUS else 82), debug
def _falha_servo(
self,
label,
angulo,
angulo_sp,
erro,
motivos,
condicoes,
debug,
):
motivos.append(
f"Servo {label} não convergiu para o SP "
f"({angulo:.1f}/{angulo_sp:.1f})"
)
condicoes.append({
"label": "Servo SEN",
"valor": angulo,
"severidade": 92,
"classe": "falha_hardware",
"descricao": f"Servo {label} não acompanhou o comando de ângulo",
"acoes": [
"Verificar travamento mecânico.",
"Verificar alimentação e retorno do servo.",
],
})
debug["erro"] = erro
return 25, debug
def _confirmar_recuperacao_servo(
self,
runtime,
label,
angulo,
angulo_sp,
erro,
motivos,
condicoes,
debug,
):
runtime["divergencia_desde"] = None
if runtime["nivel_divergencia"] is None:
runtime["coerencia_desde"] = None
return 100, debug
agora = time.monotonic()
if runtime["coerencia_desde"] is None:
runtime["coerencia_desde"] = agora
tempo_coerente_ms = (
agora - runtime["coerencia_desde"]
) * 1000.0
debug["recuperacao_em_confirmacao"] = True
debug["tempo_coerente_ms"] = tempo_coerente_ms
if tempo_coerente_ms >= self.COMANDO_RECUPERACAO_MS:
runtime["nivel_divergencia"] = None
runtime["coerencia_desde"] = None
return 100, debug
motivos.append(f"Servo {label} recuperou o SP; confirmando estabilidade")
condicoes.append({
"label": "Servo SEN",
"valor": angulo,
"severidade": 55,
"classe": "processo",
"descricao": f"Servo {label} voltou ao SP e está em confirmação",
"acoes": ["Aguardar confirmação da estabilidade do retorno."],
})
return 78, debug
def _avaliar_rele(self, item, modulo_disponivel=True):
return self._avaliar_saida_binaria(
@ -493,8 +677,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
modulo_disponivel=modulo_disponivel,
)
def _avaliar_sinaleiro(self, item):
return self._avaliar_sinaleiro_impl(item)
def _avaliar_sinaleiro(self, item, modulo_disponivel=True):
return self._avaliar_sinaleiro_impl(
item,
modulo_disponivel=modulo_disponivel,
)
def _avaliar_saida_binaria(
self,
@ -672,8 +859,155 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
"componente_recuperado_em": 0.0,
"divergencia_desde": None,
"coerencia_desde": None,
"nivel_divergencia": None,
"ultimo_desejado": None,
})
def _preparar_atuador_auxiliar(
self,
item,
grupo,
nome,
modulo_disponivel,
):
dados = item["dados"]
controlar = self._bool(dados.get("controlar", False))
conectado = self._bool(dados.get("conectado", False))
agora = time.monotonic()
runtime = self._runtime_saida_binaria(item)
if not modulo_disponivel:
runtime["modulo_disponivel_anterior"] = False
runtime["componente_disponivel_anterior"] = False
self._resetar_estado_comando(runtime, manter_desejado=False)
return runtime, self._item(
item,
StatusModulo.DESCONECTADO,
0,
[],
[],
False,
{
"avaliacao_suspensa": True,
"motivo_suspensao": "Módulo SEN indisponível",
},
grupo,
)
if not runtime["modulo_disponivel_anterior"]:
runtime["modulo_recuperado_em"] = agora
runtime["componente_disponivel_anterior"] = False
self._resetar_estado_comando(runtime, manter_desejado=False)
runtime["modulo_disponivel_anterior"] = True
tempo_retorno_modulo_ms = (
agora - runtime["modulo_recuperado_em"]
) * 1000.0
if not conectado:
runtime["componente_disponivel_anterior"] = False
self._resetar_estado_comando(runtime, manter_desejado=False)
if tempo_retorno_modulo_ms < self.MODULO_RECUPERACAO_GRACE_MS:
return runtime, self._item(
item,
StatusModulo.DESCONECTADO,
0,
[],
[],
False,
{
"avaliacao_suspensa": True,
"motivo_suspensao": (
"Aguardando reinicialização após retorno do SEN"
),
"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms,
},
grupo,
)
return runtime, self._item(
item,
StatusModulo.DESCONECTADO,
0,
["desconectado após período de recuperação"],
[{
"label": nome,
"valor": 0,
"severidade": 92,
"classe": "falha_hardware",
"descricao": (
f"{dados.get('label')} não reinicializou após o retorno do SEN"
),
"acoes": [
"Verificar configuração do componente.",
"Verificar retorno de telemetria.",
"Verificar firmware do SEN.",
],
}],
controlar,
{
"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms,
},
grupo,
)
if not runtime["componente_disponivel_anterior"]:
runtime["componente_recuperado_em"] = agora
self._resetar_estado_comando(runtime, manter_desejado=False)
runtime["componente_disponivel_anterior"] = True
return runtime, None
def _evidencia_comando(self, dados):
ultimo_comando_ms = self._float(
dados.get("ultimo_comando_ms", 999999),
999999,
)
ultima_resposta_ms = self._float(
dados.get("ultima_resposta_ms", 999999),
999999,
)
possui_comando = self._bool(
dados.get(
"possui_comando",
ultimo_comando_ms < 900000,
)
)
if "telemetria_pos_comando" in dados:
telemetria_pos_comando = self._bool(
dados.get("telemetria_pos_comando", False)
)
else:
telemetria_pos_comando = (
possui_comando and
ultima_resposta_ms <= ultimo_comando_ms + 100.0
)
return {
"possui_comando": possui_comando,
"telemetria_pos_comando": telemetria_pos_comando,
"ultimo_comando_ms": ultimo_comando_ms,
"ultima_resposta_ms": ultima_resposta_ms,
}
def _registrar_desejado(self, runtime, token_desejado):
if runtime.get("ultimo_desejado") == token_desejado:
return
self._resetar_estado_comando(runtime, manter_desejado=False)
runtime["ultimo_desejado"] = token_desejado
@staticmethod
def _resetar_estado_comando(runtime, manter_desejado=True):
runtime["divergencia_desde"] = None
runtime["coerencia_desde"] = None
runtime["nivel_divergencia"] = None
if not manter_desejado:
runtime["ultimo_desejado"] = None
def _avaliar_comando_binario(
self,
item,
@ -874,57 +1208,213 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
return 30, debug
def _avaliar_sinaleiro_impl(self, item):
def _avaliar_sinaleiro_impl(self, item, modulo_disponivel=True):
dados = item["dados"]
conectado = self._bool(dados.get("conectado", False))
controlar = self._bool(dados.get("controlar", False))
if not conectado:
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], controlar, {}, "sinaleiro")
runtime, suspenso = self._preparar_atuador_auxiliar(
item=item,
grupo="sinaleiro",
nome="Sinaleiro SEN",
modulo_disponivel=modulo_disponivel,
)
if suspenso is not None:
return suspenso
motivos, condicoes = [], []
resposta = self._score_resposta(dados, motivos, condicoes, "Sinaleiro SEN")
comando, dbg_cmd = self._avaliar_comando_sinaleiro(item, motivos, condicoes)
resposta = self._score_resposta(
dados,
motivos,
condicoes,
"Sinaleiro SEN",
)
comando, dbg_cmd = self._avaliar_comando_sinaleiro(
item,
motivos,
condicoes,
runtime,
)
telem = self._avaliar_telemetria(dados, motivos, [])
# Sinaleiro é diagnóstico visual: não derruba operação sozinho.
# Sinaleiro é diagnóstico visual: uma divergência confirmada
# gera alerta, mas não transforma o SEN inteiro em falha.
saude = resposta * 0.45 + comando * 0.45 + telem["score"] * 0.10
saude = int(max(0, min(100, round(saude))))
return self._item(item, self._resolver_status_item(conectado, saude, condicoes), saude, motivos, condicoes, controlar, {"resposta": resposta, "comando": dbg_cmd, "telemetria": telem["parametros"]}, "sinaleiro")
def _avaliar_comando_sinaleiro(self, item, motivos, condicoes):
return self._item(
item,
self._resolver_status_item(True, saude, condicoes),
saude,
motivos,
condicoes,
controlar,
{
"resposta": resposta,
"comando": dbg_cmd,
"telemetria": telem["parametros"],
},
"sinaleiro",
)
def _avaliar_comando_sinaleiro(
self,
item,
motivos,
condicoes,
runtime,
):
dados = item["dados"]
label = dados.get("label")
controlar = self._bool(dados.get("controlar", False))
id_sp = self._safe_int(dados.get("status_led_id_sp", dados.get("led_id_sp", -1)), -1)
st_sp = self._safe_int(dados.get("status_led_sp", dados.get("led_status_sp", -1)), -1)
id_lido = self._safe_int(dados.get("status_led_id", self._campo_atual(dados, "StatusLedID", -1)), -1)
st_lido = self._safe_int(dados.get("status_led_leitura", self._campo_atual(dados, "StatusLedLeitura", -1)), -1)
ultimo_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999)
id_sp = self._safe_int(
dados.get("status_led_id_sp", dados.get("led_id_sp", -1)),
-1,
)
st_sp = self._safe_int(
dados.get("status_led_sp", dados.get("led_status_sp", -1)),
-1,
)
id_lido = self._safe_int(
dados.get(
"status_led_id",
self._campo_atual(dados, "StatusLedID", -1),
),
-1,
)
# O publicador C# envia as propriedades em todos os ciclos.
# Portanto, presença lógica é determinada pelo valor válido,
# não apenas pela existência da chave no dicionário.
id_sp_presente = id_sp >= 0
status_sp_presente = st_sp >= 0
st_lido = self._safe_int(
dados.get(
"status_led_leitura",
self._campo_atual(dados, "StatusLedLeitura", -1),
),
-1,
)
evidencia = self._evidencia_comando(dados)
debug = {
"controlar": controlar,
"status_lido": st_lido,
"status_sp": st_sp,
"id_lido": id_lido,
"id_sp": id_sp,
**evidencia,
}
if not controlar:
return 100, {"controlar": False, "status_lido": st_lido, "id_lido": id_lido}
if id_sp < 0 and st_sp < 0:
return 94, {"contrato_comando_completo": False, "status_lido": st_lido, "id_lido": id_lido}
self._resetar_estado_comando(runtime, manter_desejado=False)
return 100, debug
if not id_sp_presente and not status_sp_presente:
self._resetar_estado_comando(runtime, manter_desejado=False)
debug["contrato_comando_completo"] = False
return 94, debug
token_desejado = (
id_sp if id_sp_presente else None,
st_sp if status_sp_presente else None,
)
self._registrar_desejado(runtime, token_desejado)
# Se o SP existe, uma leitura ausente (-1) também é divergência.
coerente_id = (
not id_sp_presente or
(id_lido >= 0 and id_lido == id_sp)
)
coerente_status = (
not status_sp_presente or
(st_lido >= 0 and st_lido == st_sp)
)
coerente = coerente_id and coerente_status
debug["divergente"] = not coerente
coerente = True
if id_sp >= 0 and id_lido >= 0 and id_sp != id_lido:
coerente = False
if st_sp >= 0 and st_lido >= 0 and st_sp != st_lido:
coerente = False
if coerente:
return 100, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp}
if ultimo_ms < self.COMANDO_GRACE_MS:
return 96, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp, "ultimo_comando_ms": ultimo_ms}
runtime["divergencia_desde"] = None
motivos.append(f"Sinaleiro {label} diferente do comportamento desejado")
if runtime["nivel_divergencia"] is None:
runtime["coerencia_desde"] = None
return 100, debug
agora = time.monotonic()
if runtime["coerencia_desde"] is None:
runtime["coerencia_desde"] = agora
tempo_coerente_ms = (
agora - runtime["coerencia_desde"]
) * 1000.0
debug["recuperacao_em_confirmacao"] = True
debug["tempo_coerente_ms"] = tempo_coerente_ms
if tempo_coerente_ms >= self.COMANDO_RECUPERACAO_MS:
runtime["nivel_divergencia"] = None
runtime["coerencia_desde"] = None
return 100, debug
motivos.append(
f"Sinaleiro {label} recuperou o estado; confirmando estabilidade"
)
return 88, debug
runtime["coerencia_desde"] = None
if not evidencia["possui_comando"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug
if not evidencia["telemetria_pos_comando"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
return 96, debug
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "período de resposta ao comando"
return 96, debug
agora = time.monotonic()
tempo_retorno_ms = (
agora - runtime["componente_recuperado_em"]
) * 1000.0
if tempo_retorno_ms < self.COMPONENTE_RECUPERACAO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "estabilização após reconexão"
debug["tempo_desde_retorno_componente_ms"] = tempo_retorno_ms
return 96, debug
if runtime["divergencia_desde"] is None:
runtime["divergencia_desde"] = agora
tempo_divergente_ms = (
agora - runtime["divergencia_desde"]
) * 1000.0
debug["tempo_divergente_ms"] = tempo_divergente_ms
if tempo_divergente_ms < self.COMANDO_ALERTA_MS:
return 96, debug
runtime["nivel_divergencia"] = "alerta"
motivos.append(
f"Sinaleiro {label} diferente do comportamento desejado"
)
condicoes.append({
"label": "Sinaleiro SEN",
"valor": st_lido,
"severidade": 45,
"severidade": 55,
"classe": "processo",
"descricao": f"Sinaleiro {label} não está refletindo o comportamento desejado",
"acoes": ["Conferir LED/sinaleiro físico e comando pelo SEN."],
"descricao": (
f"Sinaleiro {label} não está refletindo o comportamento desejado"
),
"acoes": [
"Conferir LED/sinaleiro físico.",
"Conferir comando e retorno publicados pelo SEN.",
],
})
return 82, {"status_lido": st_lido, "status_sp": st_sp, "id_lido": id_lido, "id_sp": id_sp, "ultimo_comando_ms": ultimo_ms}
return 78, debug
# ============================================================
# Processo geral
@ -1013,30 +1503,6 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
score = min(score, 60)
energia_critica = True
temp_motor_max = max(ctx["temperaturas_motor"]) if ctx["temperaturas_motor"] else 0
if temp_motor_max >= 90:
motivos.append(f"Temperatura máxima dos motores crítica ({temp_motor_max:.1f} °C)")
condicoes.append({
"label": "Temperatura motores",
"valor": temp_motor_max,
"severidade": 95,
"classe": "falha_hardware",
"descricao": f"Temperatura de motor crítica ({temp_motor_max:.1f} °C)",
"acoes": ["Reduzir carga/parar movimentação e verificar refrigeração."],
})
score = min(score, 45)
elif temp_motor_max >= 80:
motivos.append(f"Temperatura máxima dos motores elevada ({temp_motor_max:.1f} °C)")
condicoes.append({
"label": "Temperatura motores",
"valor": temp_motor_max,
"severidade": 75,
"classe": "risco_operacional",
"descricao": f"Temperatura de motor elevada ({temp_motor_max:.1f} °C)",
"acoes": ["Reduzir esforço se persistir."],
})
score = min(score, 75)
if ctx["temp_bateria"] >= 60:
motivos.append(f"Temperatura da bateria crítica ({ctx['temp_bateria']:.1f} °C)")
condicoes.append({

View File

@ -29,6 +29,9 @@ class RegrasTaticas:
# Pulverizador automático.
"atu_indisponivel": self._nova_persistencia(),
"bomba_cavitada": self._nova_persistencia(),
# MOV / possível atolamento ou bloqueio mecânico.
"mov_travamento": self._nova_persistencia(),
}
# ============================================================
@ -51,6 +54,13 @@ class RegrasTaticas:
motivos.extend(novos_motivos)
freio_necessario = freio_necessario or freio
novos_motivos, freio = self._avaliar_travamento_mov(
controle=controle,
operacao=operacao
)
motivos.extend(novos_motivos)
freio_necessario = freio_necessario or freio
novos_motivos, freio = self._avaliar_oak_parada_por_bloqueio(
controle=controle,
operacao=operacao
@ -126,6 +136,92 @@ class RegrasTaticas:
return motivos, freio_necessario
def _avaliar_travamento_mov(self, controle: dict, operacao: dict):
movimento_automatico = self._bool(
controle.get("movimento_automatico", False)
)
controle_manual = self._bool(
controle.get("controle_manual_acionado", False)
)
status_operacao = self._enum_or(
StatusOperacao,
operacao.get(
"status",
StatusOperacao.NaoIniciado.value
),
StatusOperacao.NaoIniciado
)
pausa = self._bool(operacao.get("pausa", False))
emergencia = self._bool(operacao.get("emergencia", False))
calibrando = self._bool(operacao.get("calibrando", False))
finalizando = self._bool(operacao.get("finalizando", False))
deve_avaliar = (
status_operacao == StatusOperacao.EmAndamento
and movimento_automatico
and not controle_manual
and not pausa
and not emergencia
and not calibrando
and not finalizando
)
if not deve_avaliar:
self._resetar_persistencia("mov_travamento")
return [], False
mov, _, operante = self._modulo_operante(
T_Code.Mov,
aceitar_alerta=True
)
if not operante:
# Indisponibilidade do módulo não deve ser confundida com atolamento.
self._resetar_persistencia("mov_travamento")
return [], False
saude = self._dict(mov.get("saude", {}))
detalhes = self._dict(saude.get("detalhes", {}))
travamento_health = self._bool(
detalhes.get("travamento_confirmado", False)
)
# O health já exige aproximadamente 2 segundos e 3 amostras.
# Esta persistência curta apenas protege a regra contra troca de
# snapshot ou inconsistência momentânea no Redis.
travamento_tatico = self._avaliar_persistencia(
nome="mov_travamento",
estado_bruto=travamento_health,
tempo_acionar=0.30,
tempo_liberar=2.00
)
if not travamento_tatico:
return [], False
drivers = detalhes.get("drivers_com_travamento", []) or []
ids = [
str(driver.get("label") or driver.get("id"))
for driver in drivers
if isinstance(driver, dict)
]
complemento = (
f" | motores: {', '.join(ids)}"
if ids
else ""
)
return [
"Possível atolamento ou bloqueio mecânico detectado"
f"{complemento}. Operação pausada para intervenção do operador."
], True
def _avaliar_oak_parada_por_bloqueio(self, controle: dict, operacao: dict):
"""
Bloqueia somente se a função de parada por bloqueio estiver ativa.

View File

@ -630,6 +630,90 @@ class ContextoGlobalRedis:
"motivos": motivos_controle_runtime,
}
# ------------------------------------------------------------
# 5.1) Solicitação de intertravamento sistêmico
# ------------------------------------------------------------
parada_seguranca = cls._dict(
controle.get("parada_seguranca", {})
)
parada_seguranca_ativa = cls._bool(
parada_seguranca.get("ativa", False)
)
codigo_parada_seguranca = str(
parada_seguranca.get("codigo", "") or ""
).strip().upper()
codigos_que_solicitam_emergencia = {
"MOV_TRAVAMENTO",
}
codigo_valido_emergencia = (
codigo_parada_seguranca
in codigos_que_solicitam_emergencia
)
solicitacao_da_regra = cls._bool(
parada_seguranca.get(
"solicitar_emergencia",
False
)
)
emergencia_sistema_solicitada = bool(
parada_seguranca_ativa
and codigo_valido_emergencia
and solicitacao_da_regra
)
motivo_emergencia_sistema = (
str(
parada_seguranca.get(
"motivo",
"Intertravamento sistêmico solicitado"
)
)
if emergencia_sistema_solicitada
else None
)
origem_emergencia_sistema = (
parada_seguranca.get("origem")
if emergencia_sistema_solicitada
else None
)
severidade_emergencia_sistema = (
cls._int(
parada_seguranca.get("severidade", 90),
90
)
if emergencia_sistema_solicitada
else 0
)
ts_evento_emergencia_sistema = (
parada_seguranca.get("ts_evento")
if emergencia_sistema_solicitada
else None
)
detalhes_regras["intertravamento_sistema"] = {
"ativo": emergencia_sistema_solicitada,
"codigo": (
codigo_parada_seguranca
if emergencia_sistema_solicitada
else None
),
"origem": origem_emergencia_sistema,
"severidade": severidade_emergencia_sistema,
"motivo": motivo_emergencia_sistema,
"ts_evento": ts_evento_emergencia_sistema,
"drivers": parada_seguranca.get("drivers", []),
}
# ------------------------------------------------------------
# 6) Decisão final
# ------------------------------------------------------------
@ -667,6 +751,22 @@ class ContextoGlobalRedis:
liberado=liberou,
motivo_nao_liberado="\n\n".join(motivos) if not liberou else None,
motivos_nao_liberado_lista=motivos,
emergencia_sistema_solicitada=emergencia_sistema_solicitada,
emergencia_sistema={
"solicitada": emergencia_sistema_solicitada,
"codigo": (
codigo_parada_seguranca
if emergencia_sistema_solicitada
else None
),
"origem": origem_emergencia_sistema,
"severidade": severidade_emergencia_sistema,
"motivo": motivo_emergencia_sistema,
"ts_evento": ts_evento_emergencia_sistema,
"drivers": parada_seguranca.get("drivers", []),
},
operacao_liberacao_debug={
"ts": time.time(),
"liberado": liberou,

View File

@ -1,7 +1,9 @@
import datetime
import json
import os
import threading
import time
from pathlib import Path
import cv2
import numpy as np
@ -24,6 +26,13 @@ from visual_worker.utils import converter_valores_numpy
from shared.perf_monitor import VisualPerfMonitor
CAMERA_MANAGER_VERSION = "production_v1_2026_08_24"
PRODUCT_ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
PRODUCT_MODULE_SCHEMA = "multispec_module_params_v3"
PHYSICAL_RAW5_CHANNELS = 5
class CameraManager:
"""
CameraManager v1 do Weed Worker.
@ -61,6 +70,12 @@ class CameraManager:
self.weed_detector = None
self.seg_config = None
# Contrato imutável da sessão produto.
# Não faz parte do reset operacional porque descreve modelo + MP + hardware.
self._pipeline_contract = {}
self._module_params_snapshot = {}
self._module_params_path = None
self.operante = False
self.iniciando = False
self.debug_visual = False
@ -226,6 +241,488 @@ class CameraManager:
self._proxima_inicializacao_monotonic = time.monotonic() + atraso
return atraso
@staticmethod
def _normalizar_size_wh(value, field_name):
if not isinstance(value, (list, tuple)) or len(value) != 2:
raise RuntimeError(
f"{field_name} deve ser [W,H]; recebido={value!r}"
)
w = int(value[0])
h = int(value[1])
if w <= 0 or h <= 0:
raise RuntimeError(
f"{field_name} inválido: {value!r}"
)
return [w, h]
@staticmethod
def _path_normalizado(value):
if not value:
return None
try:
p = Path(str(value))
if p.is_file():
return str(p.resolve())
# Mesmo quando ainda não existe, normaliza sem exigir existência.
return str(p.expanduser().absolute())
except Exception:
return str(value)
@staticmethod
def _carregar_json_obrigatorio(path_value, field_name):
if not path_value:
raise RuntimeError(f"{field_name} não definido.")
p = Path(str(path_value))
if not p.is_file():
raise FileNotFoundError(
f"{field_name} não encontrado: {p}"
)
with p.open("r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise RuntimeError(
f"{field_name} deve conter JSON object/dict."
)
return p.resolve(), data
def _obter_tamanho_entrada_modelo(self):
if self.model_svc is None:
raise RuntimeError("Modelo ONNX ainda não inicializado.")
shape = list(
getattr(self.model_svc, "onnx_input_shape", None)
or []
)
if len(shape) != 4:
raise RuntimeError(
f"ONNX input deve ser BCHW 4D; shape={shape}"
)
h = shape[2]
w = shape[3]
if not isinstance(h, (int, np.integer)) or not isinstance(w, (int, np.integer)):
raise RuntimeError(
"Produto exige resolução ONNX estática em H/W; "
f"shape={shape}"
)
if int(w) <= 0 or int(h) <= 0:
raise RuntimeError(
f"Resolução ONNX inválida: {shape}"
)
return [int(w), int(h)]
def _validar_module_params_produto(self, seg_config, mx_id):
module_path, mp = self._carregar_json_obrigatorio(
seg_config.get("module_calibration_json"),
"module_calibration_json",
)
require_product = bool(
seg_config.get("require_product_contract", True)
)
if mp.get("schema") != PRODUCT_MODULE_SCHEMA:
raise RuntimeError(
f"module_params schema inválido: {mp.get('schema')!r}. "
f"Esperado={PRODUCT_MODULE_SCHEMA!r}"
)
assembly = mp.get("assembly_metadata", {}) or {}
assembly_schema = assembly.get("schema")
product_contract = assembly_schema == PRODUCT_ASSEMBLY_SCHEMA
if require_product and not product_contract:
raise RuntimeError(
"CameraManager produto exige module_params gerado pelo "
"module_params_assembler_production.py. "
f"assembly_metadata.schema={assembly_schema!r}"
)
fusion = mp.get("fusion_config", {}) or {}
mp_target = self._normalizar_size_wh(
fusion.get("target_size"),
"module_params.fusion_config.target_size",
)
sizes_raw = mp.get("sensor_size_by_role", {}) or {}
hardware = mp.get("camera_hardware", {}) or {}
sizes = {}
for role in ("rgb", "re", "nir"):
size = sizes_raw.get(role)
if size is None:
hw_role = hardware.get(role, {}) or {}
size = hw_role.get("size")
if require_product and size is None:
raise RuntimeError(
f"module_params sem sensor_size_by_role.{role}"
)
if size is not None:
sizes[role] = self._normalizar_size_wh(
size,
f"module_params.sensor_size_by_role.{role}",
)
if require_product and set(sizes) != {"rgb", "re", "nir"}:
raise RuntimeError(
f"module_params incompleto em sensor_size_by_role: {sizes}"
)
provenance = mp.get("calibration_provenance", {}) or {}
calibrated_mx = provenance.get("device_mx_id")
if calibrated_mx and str(calibrated_mx) != str(mx_id):
raise RuntimeError(
"MX ID solicitado diverge do módulo calibrado: "
f"solicitado={mx_id} calibrado={calibrated_mx}"
)
return {
"path": str(module_path),
"module_params": mp,
"product_contract": product_contract,
"require_product_contract": require_product,
"target_size": mp_target,
"sensor_size_by_role": sizes,
"camera_hardware": hardware,
"calibrated_mx_id": calibrated_mx,
}
def _validar_contrato_pipeline(self, seg_config, mx_id):
"""
Fecha a autoridade da resolução antes de abrir a OAK:
ONNX input [W,H]
==
module_params.fusion_config.target_size
==
seg_config.ia_resolution
camera_width/camera_height deixam de participar da decisão física.
"""
model_target = self._obter_tamanho_entrada_modelo()
mp_info = self._validar_module_params_produto(
seg_config,
mx_id,
)
mp_target = list(mp_info["target_size"])
if model_target != mp_target:
raise RuntimeError(
"Contrato de resolução incompatível entre ONNX e module_params: "
f"ONNX={model_target} module_params={mp_target}"
)
cfg_target_raw = seg_config.get("ia_resolution")
if cfg_target_raw is not None:
cfg_target = self._normalizar_size_wh(
cfg_target_raw,
"seg_config.ia_resolution",
)
if cfg_target != model_target:
raise RuntimeError(
"Contrato de resolução incompatível entre config, ONNX e MP: "
f"config={cfg_target} ONNX={model_target} MP={mp_target}"
)
else:
cfg_target = list(model_target)
# Canonicaliza o config em memória. A partir daqui quem consulta
# ia_resolution recebe o contrato já validado, não um default solto.
seg_config["ia_resolution"] = list(model_target)
seg_config["module_calibration_json"] = mp_info["path"]
model_channels = int(
getattr(self.model_svc, "channels", 0)
or 0
)
input_names = list(
getattr(
self.model_svc,
"input_channel_names",
[],
)
or []
)
if model_channels <= 0 or len(input_names) != model_channels:
raise RuntimeError(
"Contrato de canais do modelo inválido: "
f"C={model_channels} names={input_names}"
)
physical_allowed = {
"R", "G", "B", "RE", "NIR", "NDVI", "NDRE",
}
invalid = [
str(x)
for x in input_names
if str(x).upper() not in physical_allowed
]
if invalid:
raise RuntimeError(
f"Canais ONNX não suportados pelo pipeline: {invalid}"
)
rgb_native = (
list(mp_info["sensor_size_by_role"].get("rgb"))
if mp_info["sensor_size_by_role"].get("rgb") is not None
else None
)
if mp_info["require_product_contract"] and rgb_native is None:
raise RuntimeError(
"Produto exige resolução RGB nativa no module_params."
)
model_path = getattr(
self.model_svc,
"onnx_path",
None,
)
contract = {
"validated": True,
"camera_manager_version": CAMERA_MANAGER_VERSION,
"product_contract": bool(mp_info["product_contract"]),
"require_product_contract": bool(
mp_info["require_product_contract"]
),
"tensor_size": list(model_target),
"onnx_input_shape": list(
getattr(
self.model_svc,
"onnx_input_shape",
[],
)
or []
),
"onnx_input_channels": input_names,
"onnx_model_path": (
str(model_path)
if model_path is not None
else None
),
"module_params_path": mp_info["path"],
"sensor_size_by_role": dict(
mp_info["sensor_size_by_role"]
),
"camera_hardware": dict(
mp_info["camera_hardware"]
),
"rgb_native_size": rgb_native,
"calibrated_mx_id": mp_info[
"calibrated_mx_id"
],
}
self._pipeline_contract = contract
self._module_params_snapshot = mp_info[
"module_params"
]
self._module_params_path = mp_info[
"path"
]
self.mostrar_log(
"[weed][CONTRACT] "
f"tensor={contract['tensor_size']} "
f"channels={contract['onnx_input_channels']} "
f"product={contract['product_contract']} "
f"mp={contract['module_params_path']}"
)
return contract
def _validar_tensor_camera(self, tensor5, contexto="runtime"):
if not isinstance(tensor5, np.ndarray):
raise RuntimeError(
f"[{contexto}] tensor da câmera deve ser np.ndarray; "
f"recebido={type(tensor5)}"
)
if tensor5.ndim != 3:
raise RuntimeError(
f"[{contexto}] Raw5 deve ser CHW 3D; shape={tensor5.shape}"
)
if int(tensor5.shape[0]) != PHYSICAL_RAW5_CHANNELS:
raise RuntimeError(
f"[{contexto}] câmera deve entregar Raw5 com C=5; "
f"shape={tensor5.shape}"
)
expected = (
self._pipeline_contract.get(
"tensor_size"
)
or []
)
if len(expected) == 2:
expected_w = int(expected[0])
expected_h = int(expected[1])
if (
int(tensor5.shape[2]) != expected_w
or int(tensor5.shape[1]) != expected_h
):
raise RuntimeError(
f"[{contexto}] tensor H/W incompatível: "
f"recebido={tensor5.shape} "
f"esperado=(5,{expected_h},{expected_w})"
)
if tensor5.dtype != np.float32:
raise RuntimeError(
f"[{contexto}] Raw5 produto deve ser float32; "
f"dtype={tensor5.dtype}"
)
return True
def _validar_config_dinamica_contrato(self, cfg):
"""
Somente parâmetros operacionais podem mudar durante a sessão.
Modelo, MP, canais e resolução exigem reinicialização completa.
"""
if not self._pipeline_contract.get("validated", False):
return cfg
expected_size = list(
self._pipeline_contract[
"tensor_size"
]
)
requested_size = cfg.get(
"ia_resolution"
)
if requested_size is not None:
requested_size = self._normalizar_size_wh(
requested_size,
"config dinâmica ia_resolution",
)
if requested_size != expected_size:
raise RuntimeError(
"ia_resolution não pode mudar com a sessão ativa: "
f"atual={expected_size} solicitado={requested_size}. "
"Reinicie CameraManager com o novo modelo/MP."
)
cfg["ia_resolution"] = list(
expected_size
)
expected_mp = self._pipeline_contract.get(
"module_params_path"
)
requested_mp = self._path_normalizado(
cfg.get(
"module_calibration_json"
)
)
if (
expected_mp
and requested_mp
and requested_mp != self._path_normalizado(
expected_mp
)
):
raise RuntimeError(
"module_calibration_json não pode mudar com a sessão ativa. "
"Feche/reinicialize o CameraManager."
)
current_model = self._path_normalizado(
self._pipeline_contract.get(
"onnx_model_path"
)
)
requested_model = self._path_normalizado(
cfg.get(
"onnx_model_path"
)
or cfg.get(
"ia_model_path"
)
)
if (
current_model
and requested_model
and current_model != requested_model
):
raise RuntimeError(
"Modelo ONNX não pode ser trocado dinamicamente na sessão ativa. "
"Feche/reinicialize o CameraManager."
)
requested_channels = cfg.get(
"input_channels"
)
if isinstance(
requested_channels,
str,
):
requested_channels = [
x.strip().upper()
for x in requested_channels.split(",")
if x.strip()
]
elif requested_channels is not None:
requested_channels = [
str(x).upper()
for x in requested_channels
]
current_channels = [
str(x).upper()
for x in self._pipeline_contract.get(
"onnx_input_channels",
[]
)
]
if (
requested_channels is not None
and requested_channels != current_channels
):
raise RuntimeError(
"input_channels não pode mudar com ONNX ativo: "
f"atual={current_channels} solicitado={requested_channels}"
)
return cfg
def inicializar(self, mx_id):
if mx_id is None:
return False
@ -271,7 +768,20 @@ class CameraManager:
self.seg_config.get("posproc_intervalo_min_s", 5.0)
)
camera_nova = self._inicializar_camera(mx_id, self.seg_config)
# Produto: valida o modelo e o MP ANTES de abrir a OAK.
# Assim não ocupamos USB/pipeline para só depois descobrir
# que ONNX, module_params e ia_resolution são incompatíveis.
self._inicializar_modelo()
self._validar_contrato_pipeline(
self.seg_config,
mx_id,
)
camera_nova = self._inicializar_camera(
mx_id,
self.seg_config,
)
if camera_nova is None:
atraso = self._registrar_falha_inicializacao()
self.mostrar_log(
@ -294,7 +804,6 @@ class CameraManager:
self.debug_visual = bool(self.seg_config.get("debug_visual", False))
self.debug_perf = bool(self.seg_config.get("debug_perf", False))
self._inicializar_modelo()
self._inicializar_detector()
self.operante = False
@ -374,15 +883,56 @@ class CameraManager:
nova = None
try:
contract = self._pipeline_contract or {}
if not contract.get("validated", False):
raise RuntimeError(
"Contrato pipeline não validado antes da câmera."
)
target_size = list(
contract["tensor_size"]
)
rgb_native = (
contract.get(
"rgb_native_size"
)
or [1280, 800]
)
# camera_width/camera_height do config deixam de ser autoridade.
# Estes valores são apenas fallback de compatibilidade do construtor
# e vêm do MP homologado.
fallback_w = int(
rgb_native[0]
)
fallback_h = int(
rgb_native[1]
)
nova = CameraMultispectral(
self.mostrar_log,
mx_id=mx_id,
module_calibration_json=seg_config.get("module_calibration_json"),
width=int(seg_config.get("camera_width", 1280)),
height=int(seg_config.get("camera_height", 800)),
# Default conservador para campo. Se o JSON definir 40, ele será respeitado.
fps=int(seg_config.get("camera_fps", 20)),
target_size=seg_config.get("ia_resolution", [1024, 640]),
module_calibration_json=contract[
"module_params_path"
],
width=fallback_w,
height=fallback_h,
# FPS continua sendo política operacional.
fps=int(
seg_config.get(
"camera_fps",
20,
)
),
target_size=target_size,
require_product_contract=bool(
contract.get(
"require_product_contract",
True,
)
),
)
if not nova.iniciado:
@ -392,6 +942,62 @@ class CameraManager:
pass
return None
if bool(
contract.get(
"require_product_contract",
True,
)
):
if not bool(
getattr(
nova,
"product_contract",
False,
)
):
raise RuntimeError(
"CameraMultispectral iniciou sem contrato produto."
)
camera_tensor_size = list(
getattr(
nova,
"tensor_size",
[],
)
or []
)
if camera_tensor_size != target_size:
raise RuntimeError(
"CameraMultispectral divergiu do tamanho final: "
f"camera={camera_tensor_size} "
f"contrato={target_size}"
)
camera_sizes = dict(
getattr(
nova,
"sensor_size_by_role",
{},
)
or {}
)
expected_sizes = dict(
contract.get(
"sensor_size_by_role",
{},
)
or {}
)
if camera_sizes != expected_sizes:
raise RuntimeError(
"CameraMultispectral divergiu do hardware homologado: "
f"camera={camera_sizes} esperado={expected_sizes}"
)
return nova
except Exception as e:
@ -400,12 +1006,51 @@ class CameraManager:
nova.parar()
except Exception:
pass
self.mostrar_log(f"⚠️ Camera com ID {mx_id} não conectada: {e}")
self.mostrar_log(
f"⚠️ Camera com ID {mx_id} não conectada: {e}"
)
return None
def _inicializar_modelo(self):
requested = (
self.seg_config.get("onnx_model_path")
or self.seg_config.get("ia_model_path")
or self.seg_config.get("model_path")
or self.seg_config.get("onnx_path")
)
if self.model_svc is not None:
return
current = getattr(
self.model_svc,
"onnx_path",
None,
)
current_norm = self._path_normalizado(
current
)
requested_norm = self._path_normalizado(
requested
)
if (
current_norm
and requested_norm
and current_norm != requested_norm
):
self.mostrar_log(
"[weed][MODEL] modelo mudou entre sessões; "
f"recriando runtime | antigo={current_norm} "
f"novo={requested_norm}"
)
# Libera a referência. O ORT/TRT anterior será coletado
# quando não houver mais referências ao serviço.
self.model_svc = None
else:
return
self.model_svc = MultiSpecSegformerService(
model_config=self.seg_config,
@ -414,7 +1059,8 @@ class CameraManager:
self.mostrar_log(
f"[weed][MODEL] backend={getattr(self.model_svc, 'runtime_backend', 'onnx')} "
f"runtime_mode={getattr(self.model_svc, 'runtime_mode', None)}"
f"runtime_mode={getattr(self.model_svc, 'runtime_mode', None)} "
f"input_shape={getattr(self.model_svc, 'onnx_input_shape', None)}"
)
def _inicializar_detector(self):
@ -689,6 +1335,11 @@ class CameraManager:
time.sleep(0.05)
continue
self._validar_tensor_camera(
tensor5,
contexto="warmup",
)
frame_id = int(
res.get("frame_id")
or res.get("async_packet_seq")
@ -910,6 +1561,7 @@ class CameraManager:
from weed_worker.config import load_seg_config
cfg = load_seg_config()
cfg = self._validar_config_dinamica_contrato(cfg)
self.seg_config = cfg
self.qtd_bicos = int(cfg.get("qtd_bicos", self.qtd_bicos or 7) or 7)
self.posproc_intervalo_min_s = float(cfg.get("posproc_intervalo_min_s", self.posproc_intervalo_min_s))
@ -974,7 +1626,9 @@ class CameraManager:
motivos = []
if self.camera is None:
if not self._pipeline_contract.get("validated", False):
motivos.append("contrato ONNX/module_params não validado")
elif self.camera is None:
motivos.append("câmera indisponível")
elif self.iniciando:
motivos.append("câmera inicializando")
@ -1010,6 +1664,23 @@ class CameraManager:
"timeout_tensor_s": timeout_tensor,
"timeout_inferencia_s": timeout_inferencia,
"timeout_deteccao_s": timeout_deteccao,
"contract": {
"validated": bool(
self._pipeline_contract.get("validated", False)
),
"product_contract": bool(
self._pipeline_contract.get("product_contract", False)
),
"tensor_size": list(
self._pipeline_contract.get("tensor_size", []) or []
),
"onnx_input_channels": list(
self._pipeline_contract.get("onnx_input_channels", []) or []
),
"module_params_path": self._pipeline_contract.get(
"module_params_path"
),
},
}
self.pipeline_ia_ok = ok
@ -1017,6 +1688,20 @@ class CameraManager:
return resultado
def get_pipeline_contract(self):
"""Snapshot somente-leitura do contrato ONNX + MP da sessão atual."""
try:
return json.loads(
json.dumps(
self._pipeline_contract,
default=str,
)
)
except Exception:
return dict(
self._pipeline_contract
)
# ============================================================
# Caches
# ============================================================
@ -1415,6 +2100,11 @@ class CameraManager:
time.sleep(0.50 if reiniciou else 0.05)
continue
self._validar_tensor_camera(
tensor5,
contexto="capture",
)
if not self._sessao_camera_valida(camera_atual, generation):
self.perf.inc("tensor_descartado_geracao")
continue

View File

@ -0,0 +1 @@
Coloque seu labelmap.txt real ao lado do EXE.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
{
"group_root": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\dist\\MaskReviewer\\group",
"labelmap": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\dist\\MaskReviewer\\labelmap.txt"
}

View File

@ -0,0 +1,51 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = []
binaries = []
hiddenimports = []
tmp_ret = collect_all('cv2')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['MaskReviewer.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='MaskReviewer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='MaskReviewer',
)

View File

@ -0,0 +1,110 @@
MASKREVIEWER - PACOTE WINDOWS
================================
OBJETIVO
--------
Revisar, comparar e eventualmente editar máscaras já separadas pelo auditor.
Não carrega modelo, PyTorch, CUDA ou checkpoint.
PACOTE PARA O ANOTADOR
----------------------
A estrutura recomendada é:
MaskReviewer\
MaskReviewer.exe
_internal\
labelmap.txt
group\
chao\
previews\
masks\
predictions\
final_masks\
review_order.csv
chao_cana\
chao_cana_erva\
chao_erva\
...
Ao abrir MaskReviewer.exe:
1. Se "group" estiver ao lado do EXE, ele usa automaticamente.
2. Se "labelmap.txt" estiver ao lado do EXE, ele usa automaticamente.
3. Se algum deles não existir, abre o seletor do Windows.
4. Os caminhos escolhidos ficam lembrados em MaskReviewer.settings.json.
5. O estado do lote fica em group\review_choices.csv.
6. As escolhas finais ficam em cada:
group\<GRUPO>\final_masks\
NOVO LOTE
---------
Para mandar uma nova revisão à mesma pessoa:
1. Ela mantém:
MaskReviewer.exe
_internal\
labelmap.txt
2. Substitui a pasta:
group\
3. Abre o mesmo EXE.
Como review_choices.csv fica DENTRO de group\, a sessão nova começa limpa junto
com a nova pasta group.
CONTROLES
---------
Painel 2x2:
1 = máscara humana
2 = máscara do modelo
3 = REMAP
E = editar
A/D = anterior/próxima
S = pular
X = apagar decisão
Q/ESC = sair
A barra "Overlay %" controla a opacidade dos dois overlays ao mesmo tempo.
Editor:
E e depois:
1 = humana como base
2 = modelo como base
3 = começar de chão
Mouse esquerdo = adiciona ponto
Mouse direito = remove último ponto
ENTER = preenche polígono
C = próxima classe
0..9 = ID de classe
Z = desfazer
R = restaurar base
BACKSPACE/DEL = limpar pontos abertos
S = salvar
ESC/Q = cancelar edição
COMO GERAR O EXE
----------------
Execute com duplo clique:
build_windows.bat
Recomendado:
Windows 10/11
Python 3.11 ou 3.12 instalado SOMENTE na máquina de build
O script cria:
dist\MaskReviewer\MaskReviewer.exe
Depois do build, a máquina do anotador NÃO precisa ter Python.
POR QUE --ONEDIR?
-----------------
O build usa PyInstaller --onedir porque OpenCV/Numpy ficam mais confiáveis,
iniciam mais rápido e são mais fáceis de diagnosticar do que um EXE --onefile.
IMPORTANTE
----------
O MaskReviewer nunca altera o dataset bruto.
Ele só escreve final_masks e review_choices.csv dentro do lote enviado.

View File

@ -0,0 +1,77 @@
('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.exe',
False,
False,
True,
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-windowed.ico',
None,
False,
False,
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly xmlns='
b'"urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">\n <trustInfo x'
b'mlns="urn:schemas-microsoft-com:asm.v3">\n <security>\n <requested'
b'Privileges>\n <requestedExecutionLevel level="asInvoker" uiAccess='
b'"false"/>\n </requestedPrivileges>\n </security>\n </trustInfo>\n '
b'<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">\n <'
b'application>\n <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f'
b'0}"/>\n <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>\n '
b' <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>\n <s'
b'upportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>\n <supporte'
b'dOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>\n </application>\n <'
b'/compatibility>\n <application xmlns="urn:schemas-microsoft-com:asm.v3">'
b'\n <windowsSettings>\n <longPathAware xmlns="http://schemas.micros'
b'oft.com/SMI/2016/WindowsSettings">true</longPathAware>\n </windowsSett'
b'ings>\n </application>\n <dependency>\n <dependentAssembly>\n <ass'
b'emblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version='
b'"6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" langua'
b'ge="*"/>\n </dependentAssembly>\n </dependency>\n</assembly>',
True,
False,
None,
None,
None,
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.pkg',
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\PYZ-00.pyz',
'PYZ'),
('struct',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth__tkinter',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py',
'PYSOURCE'),
('pyi_rth_inspect',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
'PYSOURCE'),
('MaskReviewer',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\MaskReviewer.py',
'PYSOURCE')],
[],
False,
False,
1787944409,
[('runw.exe',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\runw.exe',
'EXECUTABLE')],
'C:\\Python312\\python312.dll')

View File

@ -0,0 +1,55 @@
('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.pkg',
{'BINARY': True,
'DATA': True,
'EXECUTABLE': True,
'EXTENSION': True,
'PYMODULE': True,
'PYSOURCE': True,
'PYZ': False,
'SPLASH': True,
'SYMLINK': False},
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\PYZ-00.pyz',
'PYZ'),
('struct',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth__tkinter',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py',
'PYSOURCE'),
('pyi_rth_inspect',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
'PYSOURCE'),
('MaskReviewer',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\MaskReviewer.py',
'PYSOURCE')],
'python312.dll',
True,
False,
False,
[],
None,
None,
None)

View File

@ -0,0 +1,815 @@
('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\PYZ-00.pyz',
[('__future__', 'C:\\Python312\\Lib\\__future__.py', 'PYMODULE'),
('_aix_support', 'C:\\Python312\\Lib\\_aix_support.py', 'PYMODULE'),
('_compat_pickle', 'C:\\Python312\\Lib\\_compat_pickle.py', 'PYMODULE'),
('_compression', 'C:\\Python312\\Lib\\_compression.py', 'PYMODULE'),
('_py_abc', 'C:\\Python312\\Lib\\_py_abc.py', 'PYMODULE'),
('_pydatetime', 'C:\\Python312\\Lib\\_pydatetime.py', 'PYMODULE'),
('_pydecimal', 'C:\\Python312\\Lib\\_pydecimal.py', 'PYMODULE'),
('_strptime', 'C:\\Python312\\Lib\\_strptime.py', 'PYMODULE'),
('_threading_local', 'C:\\Python312\\Lib\\_threading_local.py', 'PYMODULE'),
('argparse', 'C:\\Python312\\Lib\\argparse.py', 'PYMODULE'),
('ast', 'C:\\Python312\\Lib\\ast.py', 'PYMODULE'),
('asyncio', 'C:\\Python312\\Lib\\asyncio\\__init__.py', 'PYMODULE'),
('asyncio.base_events',
'C:\\Python312\\Lib\\asyncio\\base_events.py',
'PYMODULE'),
('asyncio.base_futures',
'C:\\Python312\\Lib\\asyncio\\base_futures.py',
'PYMODULE'),
('asyncio.base_subprocess',
'C:\\Python312\\Lib\\asyncio\\base_subprocess.py',
'PYMODULE'),
('asyncio.base_tasks',
'C:\\Python312\\Lib\\asyncio\\base_tasks.py',
'PYMODULE'),
('asyncio.constants',
'C:\\Python312\\Lib\\asyncio\\constants.py',
'PYMODULE'),
('asyncio.coroutines',
'C:\\Python312\\Lib\\asyncio\\coroutines.py',
'PYMODULE'),
('asyncio.events', 'C:\\Python312\\Lib\\asyncio\\events.py', 'PYMODULE'),
('asyncio.exceptions',
'C:\\Python312\\Lib\\asyncio\\exceptions.py',
'PYMODULE'),
('asyncio.format_helpers',
'C:\\Python312\\Lib\\asyncio\\format_helpers.py',
'PYMODULE'),
('asyncio.futures', 'C:\\Python312\\Lib\\asyncio\\futures.py', 'PYMODULE'),
('asyncio.locks', 'C:\\Python312\\Lib\\asyncio\\locks.py', 'PYMODULE'),
('asyncio.log', 'C:\\Python312\\Lib\\asyncio\\log.py', 'PYMODULE'),
('asyncio.mixins', 'C:\\Python312\\Lib\\asyncio\\mixins.py', 'PYMODULE'),
('asyncio.proactor_events',
'C:\\Python312\\Lib\\asyncio\\proactor_events.py',
'PYMODULE'),
('asyncio.protocols',
'C:\\Python312\\Lib\\asyncio\\protocols.py',
'PYMODULE'),
('asyncio.queues', 'C:\\Python312\\Lib\\asyncio\\queues.py', 'PYMODULE'),
('asyncio.runners', 'C:\\Python312\\Lib\\asyncio\\runners.py', 'PYMODULE'),
('asyncio.selector_events',
'C:\\Python312\\Lib\\asyncio\\selector_events.py',
'PYMODULE'),
('asyncio.sslproto', 'C:\\Python312\\Lib\\asyncio\\sslproto.py', 'PYMODULE'),
('asyncio.staggered',
'C:\\Python312\\Lib\\asyncio\\staggered.py',
'PYMODULE'),
('asyncio.streams', 'C:\\Python312\\Lib\\asyncio\\streams.py', 'PYMODULE'),
('asyncio.subprocess',
'C:\\Python312\\Lib\\asyncio\\subprocess.py',
'PYMODULE'),
('asyncio.taskgroups',
'C:\\Python312\\Lib\\asyncio\\taskgroups.py',
'PYMODULE'),
('asyncio.tasks', 'C:\\Python312\\Lib\\asyncio\\tasks.py', 'PYMODULE'),
('asyncio.threads', 'C:\\Python312\\Lib\\asyncio\\threads.py', 'PYMODULE'),
('asyncio.timeouts', 'C:\\Python312\\Lib\\asyncio\\timeouts.py', 'PYMODULE'),
('asyncio.transports',
'C:\\Python312\\Lib\\asyncio\\transports.py',
'PYMODULE'),
('asyncio.trsock', 'C:\\Python312\\Lib\\asyncio\\trsock.py', 'PYMODULE'),
('asyncio.unix_events',
'C:\\Python312\\Lib\\asyncio\\unix_events.py',
'PYMODULE'),
('asyncio.windows_events',
'C:\\Python312\\Lib\\asyncio\\windows_events.py',
'PYMODULE'),
('asyncio.windows_utils',
'C:\\Python312\\Lib\\asyncio\\windows_utils.py',
'PYMODULE'),
('base64', 'C:\\Python312\\Lib\\base64.py', 'PYMODULE'),
('bdb', 'C:\\Python312\\Lib\\bdb.py', 'PYMODULE'),
('bisect', 'C:\\Python312\\Lib\\bisect.py', 'PYMODULE'),
('bz2', 'C:\\Python312\\Lib\\bz2.py', 'PYMODULE'),
('calendar', 'C:\\Python312\\Lib\\calendar.py', 'PYMODULE'),
('cmd', 'C:\\Python312\\Lib\\cmd.py', 'PYMODULE'),
('code', 'C:\\Python312\\Lib\\code.py', 'PYMODULE'),
('codeop', 'C:\\Python312\\Lib\\codeop.py', 'PYMODULE'),
('concurrent', 'C:\\Python312\\Lib\\concurrent\\__init__.py', 'PYMODULE'),
('concurrent.futures',
'C:\\Python312\\Lib\\concurrent\\futures\\__init__.py',
'PYMODULE'),
('concurrent.futures._base',
'C:\\Python312\\Lib\\concurrent\\futures\\_base.py',
'PYMODULE'),
('concurrent.futures.process',
'C:\\Python312\\Lib\\concurrent\\futures\\process.py',
'PYMODULE'),
('concurrent.futures.thread',
'C:\\Python312\\Lib\\concurrent\\futures\\thread.py',
'PYMODULE'),
('contextlib', 'C:\\Python312\\Lib\\contextlib.py', 'PYMODULE'),
('contextvars', 'C:\\Python312\\Lib\\contextvars.py', 'PYMODULE'),
('copy', 'C:\\Python312\\Lib\\copy.py', 'PYMODULE'),
('csv', 'C:\\Python312\\Lib\\csv.py', 'PYMODULE'),
('ctypes', 'C:\\Python312\\Lib\\ctypes\\__init__.py', 'PYMODULE'),
('ctypes._endian', 'C:\\Python312\\Lib\\ctypes\\_endian.py', 'PYMODULE'),
('dataclasses', 'C:\\Python312\\Lib\\dataclasses.py', 'PYMODULE'),
('datetime', 'C:\\Python312\\Lib\\datetime.py', 'PYMODULE'),
('decimal', 'C:\\Python312\\Lib\\decimal.py', 'PYMODULE'),
('difflib', 'C:\\Python312\\Lib\\difflib.py', 'PYMODULE'),
('dis', 'C:\\Python312\\Lib\\dis.py', 'PYMODULE'),
('doctest', 'C:\\Python312\\Lib\\doctest.py', 'PYMODULE'),
('email', 'C:\\Python312\\Lib\\email\\__init__.py', 'PYMODULE'),
('email._encoded_words',
'C:\\Python312\\Lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'C:\\Python312\\Lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr', 'C:\\Python312\\Lib\\email\\_parseaddr.py', 'PYMODULE'),
('email._policybase',
'C:\\Python312\\Lib\\email\\_policybase.py',
'PYMODULE'),
('email.base64mime', 'C:\\Python312\\Lib\\email\\base64mime.py', 'PYMODULE'),
('email.charset', 'C:\\Python312\\Lib\\email\\charset.py', 'PYMODULE'),
('email.contentmanager',
'C:\\Python312\\Lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders', 'C:\\Python312\\Lib\\email\\encoders.py', 'PYMODULE'),
('email.errors', 'C:\\Python312\\Lib\\email\\errors.py', 'PYMODULE'),
('email.feedparser', 'C:\\Python312\\Lib\\email\\feedparser.py', 'PYMODULE'),
('email.generator', 'C:\\Python312\\Lib\\email\\generator.py', 'PYMODULE'),
('email.header', 'C:\\Python312\\Lib\\email\\header.py', 'PYMODULE'),
('email.headerregistry',
'C:\\Python312\\Lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators', 'C:\\Python312\\Lib\\email\\iterators.py', 'PYMODULE'),
('email.message', 'C:\\Python312\\Lib\\email\\message.py', 'PYMODULE'),
('email.parser', 'C:\\Python312\\Lib\\email\\parser.py', 'PYMODULE'),
('email.policy', 'C:\\Python312\\Lib\\email\\policy.py', 'PYMODULE'),
('email.quoprimime', 'C:\\Python312\\Lib\\email\\quoprimime.py', 'PYMODULE'),
('email.utils', 'C:\\Python312\\Lib\\email\\utils.py', 'PYMODULE'),
('fileinput', 'C:\\Python312\\Lib\\fileinput.py', 'PYMODULE'),
('fnmatch', 'C:\\Python312\\Lib\\fnmatch.py', 'PYMODULE'),
('fractions', 'C:\\Python312\\Lib\\fractions.py', 'PYMODULE'),
('ftplib', 'C:\\Python312\\Lib\\ftplib.py', 'PYMODULE'),
('getopt', 'C:\\Python312\\Lib\\getopt.py', 'PYMODULE'),
('getpass', 'C:\\Python312\\Lib\\getpass.py', 'PYMODULE'),
('gettext', 'C:\\Python312\\Lib\\gettext.py', 'PYMODULE'),
('glob', 'C:\\Python312\\Lib\\glob.py', 'PYMODULE'),
('gzip', 'C:\\Python312\\Lib\\gzip.py', 'PYMODULE'),
('hashlib', 'C:\\Python312\\Lib\\hashlib.py', 'PYMODULE'),
('hmac', 'C:\\Python312\\Lib\\hmac.py', 'PYMODULE'),
('html', 'C:\\Python312\\Lib\\html\\__init__.py', 'PYMODULE'),
('html.entities', 'C:\\Python312\\Lib\\html\\entities.py', 'PYMODULE'),
('http', 'C:\\Python312\\Lib\\http\\__init__.py', 'PYMODULE'),
('http.client', 'C:\\Python312\\Lib\\http\\client.py', 'PYMODULE'),
('http.cookiejar', 'C:\\Python312\\Lib\\http\\cookiejar.py', 'PYMODULE'),
('http.server', 'C:\\Python312\\Lib\\http\\server.py', 'PYMODULE'),
('importlib', 'C:\\Python312\\Lib\\importlib\\__init__.py', 'PYMODULE'),
('importlib._abc', 'C:\\Python312\\Lib\\importlib\\_abc.py', 'PYMODULE'),
('importlib._bootstrap',
'C:\\Python312\\Lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'C:\\Python312\\Lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc', 'C:\\Python312\\Lib\\importlib\\abc.py', 'PYMODULE'),
('importlib.machinery',
'C:\\Python312\\Lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'C:\\Python312\\Lib\\importlib\\metadata\\__init__.py',
'PYMODULE'),
('importlib.metadata._adapters',
'C:\\Python312\\Lib\\importlib\\metadata\\_adapters.py',
'PYMODULE'),
('importlib.metadata._collections',
'C:\\Python312\\Lib\\importlib\\metadata\\_collections.py',
'PYMODULE'),
('importlib.metadata._functools',
'C:\\Python312\\Lib\\importlib\\metadata\\_functools.py',
'PYMODULE'),
('importlib.metadata._itertools',
'C:\\Python312\\Lib\\importlib\\metadata\\_itertools.py',
'PYMODULE'),
('importlib.metadata._meta',
'C:\\Python312\\Lib\\importlib\\metadata\\_meta.py',
'PYMODULE'),
('importlib.metadata._text',
'C:\\Python312\\Lib\\importlib\\metadata\\_text.py',
'PYMODULE'),
('importlib.readers',
'C:\\Python312\\Lib\\importlib\\readers.py',
'PYMODULE'),
('importlib.resources',
'C:\\Python312\\Lib\\importlib\\resources\\__init__.py',
'PYMODULE'),
('importlib.resources._adapters',
'C:\\Python312\\Lib\\importlib\\resources\\_adapters.py',
'PYMODULE'),
('importlib.resources._common',
'C:\\Python312\\Lib\\importlib\\resources\\_common.py',
'PYMODULE'),
('importlib.resources._itertools',
'C:\\Python312\\Lib\\importlib\\resources\\_itertools.py',
'PYMODULE'),
('importlib.resources._legacy',
'C:\\Python312\\Lib\\importlib\\resources\\_legacy.py',
'PYMODULE'),
('importlib.resources.abc',
'C:\\Python312\\Lib\\importlib\\resources\\abc.py',
'PYMODULE'),
('importlib.resources.readers',
'C:\\Python312\\Lib\\importlib\\resources\\readers.py',
'PYMODULE'),
('importlib.util', 'C:\\Python312\\Lib\\importlib\\util.py', 'PYMODULE'),
('inspect', 'C:\\Python312\\Lib\\inspect.py', 'PYMODULE'),
('ipaddress', 'C:\\Python312\\Lib\\ipaddress.py', 'PYMODULE'),
('json', 'C:\\Python312\\Lib\\json\\__init__.py', 'PYMODULE'),
('json.decoder', 'C:\\Python312\\Lib\\json\\decoder.py', 'PYMODULE'),
('json.encoder', 'C:\\Python312\\Lib\\json\\encoder.py', 'PYMODULE'),
('json.scanner', 'C:\\Python312\\Lib\\json\\scanner.py', 'PYMODULE'),
('logging', 'C:\\Python312\\Lib\\logging\\__init__.py', 'PYMODULE'),
('lzma', 'C:\\Python312\\Lib\\lzma.py', 'PYMODULE'),
('mimetypes', 'C:\\Python312\\Lib\\mimetypes.py', 'PYMODULE'),
('multiprocessing',
'C:\\Python312\\Lib\\multiprocessing\\__init__.py',
'PYMODULE'),
('multiprocessing.connection',
'C:\\Python312\\Lib\\multiprocessing\\connection.py',
'PYMODULE'),
('multiprocessing.context',
'C:\\Python312\\Lib\\multiprocessing\\context.py',
'PYMODULE'),
('multiprocessing.dummy',
'C:\\Python312\\Lib\\multiprocessing\\dummy\\__init__.py',
'PYMODULE'),
('multiprocessing.dummy.connection',
'C:\\Python312\\Lib\\multiprocessing\\dummy\\connection.py',
'PYMODULE'),
('multiprocessing.forkserver',
'C:\\Python312\\Lib\\multiprocessing\\forkserver.py',
'PYMODULE'),
('multiprocessing.heap',
'C:\\Python312\\Lib\\multiprocessing\\heap.py',
'PYMODULE'),
('multiprocessing.managers',
'C:\\Python312\\Lib\\multiprocessing\\managers.py',
'PYMODULE'),
('multiprocessing.pool',
'C:\\Python312\\Lib\\multiprocessing\\pool.py',
'PYMODULE'),
('multiprocessing.popen_fork',
'C:\\Python312\\Lib\\multiprocessing\\popen_fork.py',
'PYMODULE'),
('multiprocessing.popen_forkserver',
'C:\\Python312\\Lib\\multiprocessing\\popen_forkserver.py',
'PYMODULE'),
('multiprocessing.popen_spawn_posix',
'C:\\Python312\\Lib\\multiprocessing\\popen_spawn_posix.py',
'PYMODULE'),
('multiprocessing.popen_spawn_win32',
'C:\\Python312\\Lib\\multiprocessing\\popen_spawn_win32.py',
'PYMODULE'),
('multiprocessing.process',
'C:\\Python312\\Lib\\multiprocessing\\process.py',
'PYMODULE'),
('multiprocessing.queues',
'C:\\Python312\\Lib\\multiprocessing\\queues.py',
'PYMODULE'),
('multiprocessing.reduction',
'C:\\Python312\\Lib\\multiprocessing\\reduction.py',
'PYMODULE'),
('multiprocessing.resource_sharer',
'C:\\Python312\\Lib\\multiprocessing\\resource_sharer.py',
'PYMODULE'),
('multiprocessing.resource_tracker',
'C:\\Python312\\Lib\\multiprocessing\\resource_tracker.py',
'PYMODULE'),
('multiprocessing.shared_memory',
'C:\\Python312\\Lib\\multiprocessing\\shared_memory.py',
'PYMODULE'),
('multiprocessing.sharedctypes',
'C:\\Python312\\Lib\\multiprocessing\\sharedctypes.py',
'PYMODULE'),
('multiprocessing.spawn',
'C:\\Python312\\Lib\\multiprocessing\\spawn.py',
'PYMODULE'),
('multiprocessing.synchronize',
'C:\\Python312\\Lib\\multiprocessing\\synchronize.py',
'PYMODULE'),
('multiprocessing.util',
'C:\\Python312\\Lib\\multiprocessing\\util.py',
'PYMODULE'),
('netrc', 'C:\\Python312\\Lib\\netrc.py', 'PYMODULE'),
('nturl2path', 'C:\\Python312\\Lib\\nturl2path.py', 'PYMODULE'),
('numbers', 'C:\\Python312\\Lib\\numbers.py', 'PYMODULE'),
('numpy',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\__init__.py',
'PYMODULE'),
('numpy.__config__',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\__config__.py',
'PYMODULE'),
('numpy._array_api_info',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_array_api_info.py',
'PYMODULE'),
('numpy._core',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\__init__.py',
'PYMODULE'),
('numpy._core._add_newdocs',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_add_newdocs.py',
'PYMODULE'),
('numpy._core._add_newdocs_scalars',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_add_newdocs_scalars.py',
'PYMODULE'),
('numpy._core._asarray',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_asarray.py',
'PYMODULE'),
('numpy._core._dtype',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_dtype.py',
'PYMODULE'),
('numpy._core._dtype_ctypes',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_dtype_ctypes.py',
'PYMODULE'),
('numpy._core._exceptions',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_exceptions.py',
'PYMODULE'),
('numpy._core._internal',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_internal.py',
'PYMODULE'),
('numpy._core._methods',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_methods.py',
'PYMODULE'),
('numpy._core._string_helpers',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_string_helpers.py',
'PYMODULE'),
('numpy._core._type_aliases',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_type_aliases.py',
'PYMODULE'),
('numpy._core._ufunc_config',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_ufunc_config.py',
'PYMODULE'),
('numpy._core.arrayprint',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\arrayprint.py',
'PYMODULE'),
('numpy._core.defchararray',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\defchararray.py',
'PYMODULE'),
('numpy._core.einsumfunc',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\einsumfunc.py',
'PYMODULE'),
('numpy._core.fromnumeric',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\fromnumeric.py',
'PYMODULE'),
('numpy._core.function_base',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\function_base.py',
'PYMODULE'),
('numpy._core.getlimits',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\getlimits.py',
'PYMODULE'),
('numpy._core.memmap',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\memmap.py',
'PYMODULE'),
('numpy._core.multiarray',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\multiarray.py',
'PYMODULE'),
('numpy._core.numeric',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\numeric.py',
'PYMODULE'),
('numpy._core.numerictypes',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\numerictypes.py',
'PYMODULE'),
('numpy._core.overrides',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\overrides.py',
'PYMODULE'),
('numpy._core.printoptions',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\printoptions.py',
'PYMODULE'),
('numpy._core.records',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\records.py',
'PYMODULE'),
('numpy._core.shape_base',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\shape_base.py',
'PYMODULE'),
('numpy._core.strings',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\strings.py',
'PYMODULE'),
('numpy._core.umath',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\umath.py',
'PYMODULE'),
('numpy._distributor_init',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_distributor_init.py',
'PYMODULE'),
('numpy._expired_attrs_2_0',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_expired_attrs_2_0.py',
'PYMODULE'),
('numpy._globals',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_globals.py',
'PYMODULE'),
('numpy._pytesttester',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_pytesttester.py',
'PYMODULE'),
('numpy._typing',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\__init__.py',
'PYMODULE'),
('numpy._typing._add_docstring',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_add_docstring.py',
'PYMODULE'),
('numpy._typing._array_like',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_array_like.py',
'PYMODULE'),
('numpy._typing._char_codes',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_char_codes.py',
'PYMODULE'),
('numpy._typing._dtype_like',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_dtype_like.py',
'PYMODULE'),
('numpy._typing._nbit',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nbit.py',
'PYMODULE'),
('numpy._typing._nbit_base',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nbit_base.py',
'PYMODULE'),
('numpy._typing._nested_sequence',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nested_sequence.py',
'PYMODULE'),
('numpy._typing._scalars',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_scalars.py',
'PYMODULE'),
('numpy._typing._shape',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_shape.py',
'PYMODULE'),
('numpy._typing._ufunc',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_ufunc.py',
'PYMODULE'),
('numpy._utils',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\__init__.py',
'PYMODULE'),
('numpy._utils._conversions',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\_conversions.py',
'PYMODULE'),
('numpy._utils._inspect',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\_inspect.py',
'PYMODULE'),
('numpy.char',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\char\\__init__.py',
'PYMODULE'),
('numpy.core',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\__init__.py',
'PYMODULE'),
('numpy.core._utils',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\_utils.py',
'PYMODULE'),
('numpy.core.multiarray',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\multiarray.py',
'PYMODULE'),
('numpy.ctypeslib',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ctypeslib\\__init__.py',
'PYMODULE'),
('numpy.ctypeslib._ctypeslib',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ctypeslib\\_ctypeslib.py',
'PYMODULE'),
('numpy.dtypes',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\dtypes.py',
'PYMODULE'),
('numpy.exceptions',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\exceptions.py',
'PYMODULE'),
('numpy.f2py',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\__init__.py',
'PYMODULE'),
('numpy.f2py.__version__',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\__version__.py',
'PYMODULE'),
('numpy.f2py._backends',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\__init__.py',
'PYMODULE'),
('numpy.f2py._backends._backend',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\_backend.py',
'PYMODULE'),
('numpy.f2py._backends._meson',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\_meson.py',
'PYMODULE'),
('numpy.f2py._isocbind',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_isocbind.py',
'PYMODULE'),
('numpy.f2py.auxfuncs',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\auxfuncs.py',
'PYMODULE'),
('numpy.f2py.capi_maps',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\capi_maps.py',
'PYMODULE'),
('numpy.f2py.cb_rules',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\cb_rules.py',
'PYMODULE'),
('numpy.f2py.cfuncs',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\cfuncs.py',
'PYMODULE'),
('numpy.f2py.common_rules',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\common_rules.py',
'PYMODULE'),
('numpy.f2py.crackfortran',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\crackfortran.py',
'PYMODULE'),
('numpy.f2py.diagnose',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\diagnose.py',
'PYMODULE'),
('numpy.f2py.f2py2e',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\f2py2e.py',
'PYMODULE'),
('numpy.f2py.f90mod_rules',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\f90mod_rules.py',
'PYMODULE'),
('numpy.f2py.func2subr',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\func2subr.py',
'PYMODULE'),
('numpy.f2py.rules',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\rules.py',
'PYMODULE'),
('numpy.f2py.symbolic',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\symbolic.py',
'PYMODULE'),
('numpy.f2py.use_rules',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\use_rules.py',
'PYMODULE'),
('numpy.fft',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\__init__.py',
'PYMODULE'),
('numpy.fft._helper',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_helper.py',
'PYMODULE'),
('numpy.fft._pocketfft',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_pocketfft.py',
'PYMODULE'),
('numpy.lib',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\__init__.py',
'PYMODULE'),
('numpy.lib._array_utils_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_array_utils_impl.py',
'PYMODULE'),
('numpy.lib._arraypad_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arraypad_impl.py',
'PYMODULE'),
('numpy.lib._arraysetops_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arraysetops_impl.py',
'PYMODULE'),
('numpy.lib._arrayterator_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arrayterator_impl.py',
'PYMODULE'),
('numpy.lib._datasource',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_datasource.py',
'PYMODULE'),
('numpy.lib._format_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_format_impl.py',
'PYMODULE'),
('numpy.lib._function_base_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_function_base_impl.py',
'PYMODULE'),
('numpy.lib._histograms_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_histograms_impl.py',
'PYMODULE'),
('numpy.lib._index_tricks_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_index_tricks_impl.py',
'PYMODULE'),
('numpy.lib._iotools',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_iotools.py',
'PYMODULE'),
('numpy.lib._nanfunctions_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_nanfunctions_impl.py',
'PYMODULE'),
('numpy.lib._npyio_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_npyio_impl.py',
'PYMODULE'),
('numpy.lib._polynomial_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_polynomial_impl.py',
'PYMODULE'),
('numpy.lib._scimath_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_scimath_impl.py',
'PYMODULE'),
('numpy.lib._shape_base_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_shape_base_impl.py',
'PYMODULE'),
('numpy.lib._stride_tricks_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py',
'PYMODULE'),
('numpy.lib._twodim_base_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_twodim_base_impl.py',
'PYMODULE'),
('numpy.lib._type_check_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_type_check_impl.py',
'PYMODULE'),
('numpy.lib._ufunclike_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_ufunclike_impl.py',
'PYMODULE'),
('numpy.lib._utils_impl',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_utils_impl.py',
'PYMODULE'),
('numpy.lib._version',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_version.py',
'PYMODULE'),
('numpy.lib.array_utils',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\array_utils.py',
'PYMODULE'),
('numpy.lib.format',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\format.py',
'PYMODULE'),
('numpy.lib.introspect',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\introspect.py',
'PYMODULE'),
('numpy.lib.mixins',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\mixins.py',
'PYMODULE'),
('numpy.lib.npyio',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\npyio.py',
'PYMODULE'),
('numpy.lib.recfunctions',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\recfunctions.py',
'PYMODULE'),
('numpy.lib.scimath',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\scimath.py',
'PYMODULE'),
('numpy.lib.stride_tricks',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\stride_tricks.py',
'PYMODULE'),
('numpy.linalg',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\__init__.py',
'PYMODULE'),
('numpy.linalg._linalg',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\_linalg.py',
'PYMODULE'),
('numpy.ma',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\__init__.py',
'PYMODULE'),
('numpy.ma.core',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\core.py',
'PYMODULE'),
('numpy.ma.extras',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\extras.py',
'PYMODULE'),
('numpy.ma.mrecords',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\mrecords.py',
'PYMODULE'),
('numpy.matlib',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matlib.py',
'PYMODULE'),
('numpy.matrixlib',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matrixlib\\__init__.py',
'PYMODULE'),
('numpy.matrixlib.defmatrix',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matrixlib\\defmatrix.py',
'PYMODULE'),
('numpy.polynomial',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\__init__.py',
'PYMODULE'),
('numpy.polynomial._polybase',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\_polybase.py',
'PYMODULE'),
('numpy.polynomial.chebyshev',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\chebyshev.py',
'PYMODULE'),
('numpy.polynomial.hermite',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\hermite.py',
'PYMODULE'),
('numpy.polynomial.hermite_e',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\hermite_e.py',
'PYMODULE'),
('numpy.polynomial.laguerre',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\laguerre.py',
'PYMODULE'),
('numpy.polynomial.legendre',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\legendre.py',
'PYMODULE'),
('numpy.polynomial.polynomial',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\polynomial.py',
'PYMODULE'),
('numpy.polynomial.polyutils',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\polyutils.py',
'PYMODULE'),
('numpy.random',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\__init__.py',
'PYMODULE'),
('numpy.random._pickle',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_pickle.py',
'PYMODULE'),
('numpy.rec',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\rec\\__init__.py',
'PYMODULE'),
('numpy.strings',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\strings\\__init__.py',
'PYMODULE'),
('numpy.testing',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\__init__.py',
'PYMODULE'),
('numpy.testing._private',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\__init__.py',
'PYMODULE'),
('numpy.testing._private.extbuild',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\extbuild.py',
'PYMODULE'),
('numpy.testing._private.utils',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\utils.py',
'PYMODULE'),
('numpy.testing.overrides',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\overrides.py',
'PYMODULE'),
('numpy.typing',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\typing\\__init__.py',
'PYMODULE'),
('numpy.version',
'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\version.py',
'PYMODULE'),
('opcode', 'C:\\Python312\\Lib\\opcode.py', 'PYMODULE'),
('pathlib', 'C:\\Python312\\Lib\\pathlib.py', 'PYMODULE'),
('pdb', 'C:\\Python312\\Lib\\pdb.py', 'PYMODULE'),
('pickle', 'C:\\Python312\\Lib\\pickle.py', 'PYMODULE'),
('pkgutil', 'C:\\Python312\\Lib\\pkgutil.py', 'PYMODULE'),
('platform', 'C:\\Python312\\Lib\\platform.py', 'PYMODULE'),
('pprint', 'C:\\Python312\\Lib\\pprint.py', 'PYMODULE'),
('py_compile', 'C:\\Python312\\Lib\\py_compile.py', 'PYMODULE'),
('pydoc', 'C:\\Python312\\Lib\\pydoc.py', 'PYMODULE'),
('pydoc_data', 'C:\\Python312\\Lib\\pydoc_data\\__init__.py', 'PYMODULE'),
('pydoc_data.topics',
'C:\\Python312\\Lib\\pydoc_data\\topics.py',
'PYMODULE'),
('queue', 'C:\\Python312\\Lib\\queue.py', 'PYMODULE'),
('quopri', 'C:\\Python312\\Lib\\quopri.py', 'PYMODULE'),
('random', 'C:\\Python312\\Lib\\random.py', 'PYMODULE'),
('runpy', 'C:\\Python312\\Lib\\runpy.py', 'PYMODULE'),
('secrets', 'C:\\Python312\\Lib\\secrets.py', 'PYMODULE'),
('selectors', 'C:\\Python312\\Lib\\selectors.py', 'PYMODULE'),
('shlex', 'C:\\Python312\\Lib\\shlex.py', 'PYMODULE'),
('shutil', 'C:\\Python312\\Lib\\shutil.py', 'PYMODULE'),
('signal', 'C:\\Python312\\Lib\\signal.py', 'PYMODULE'),
('socket', 'C:\\Python312\\Lib\\socket.py', 'PYMODULE'),
('socketserver', 'C:\\Python312\\Lib\\socketserver.py', 'PYMODULE'),
('ssl', 'C:\\Python312\\Lib\\ssl.py', 'PYMODULE'),
('statistics', 'C:\\Python312\\Lib\\statistics.py', 'PYMODULE'),
('string', 'C:\\Python312\\Lib\\string.py', 'PYMODULE'),
('stringprep', 'C:\\Python312\\Lib\\stringprep.py', 'PYMODULE'),
('subprocess', 'C:\\Python312\\Lib\\subprocess.py', 'PYMODULE'),
('sysconfig', 'C:\\Python312\\Lib\\sysconfig.py', 'PYMODULE'),
('tarfile', 'C:\\Python312\\Lib\\tarfile.py', 'PYMODULE'),
('tempfile', 'C:\\Python312\\Lib\\tempfile.py', 'PYMODULE'),
('textwrap', 'C:\\Python312\\Lib\\textwrap.py', 'PYMODULE'),
('threading', 'C:\\Python312\\Lib\\threading.py', 'PYMODULE'),
('tkinter', 'C:\\Python312\\Lib\\tkinter\\__init__.py', 'PYMODULE'),
('tkinter.commondialog',
'C:\\Python312\\Lib\\tkinter\\commondialog.py',
'PYMODULE'),
('tkinter.constants',
'C:\\Python312\\Lib\\tkinter\\constants.py',
'PYMODULE'),
('tkinter.dialog', 'C:\\Python312\\Lib\\tkinter\\dialog.py', 'PYMODULE'),
('tkinter.filedialog',
'C:\\Python312\\Lib\\tkinter\\filedialog.py',
'PYMODULE'),
('tkinter.messagebox',
'C:\\Python312\\Lib\\tkinter\\messagebox.py',
'PYMODULE'),
('tkinter.simpledialog',
'C:\\Python312\\Lib\\tkinter\\simpledialog.py',
'PYMODULE'),
('token', 'C:\\Python312\\Lib\\token.py', 'PYMODULE'),
('tokenize', 'C:\\Python312\\Lib\\tokenize.py', 'PYMODULE'),
('tracemalloc', 'C:\\Python312\\Lib\\tracemalloc.py', 'PYMODULE'),
('tty', 'C:\\Python312\\Lib\\tty.py', 'PYMODULE'),
('typing', 'C:\\Python312\\Lib\\typing.py', 'PYMODULE'),
('unittest', 'C:\\Python312\\Lib\\unittest\\__init__.py', 'PYMODULE'),
('unittest._log', 'C:\\Python312\\Lib\\unittest\\_log.py', 'PYMODULE'),
('unittest.async_case',
'C:\\Python312\\Lib\\unittest\\async_case.py',
'PYMODULE'),
('unittest.case', 'C:\\Python312\\Lib\\unittest\\case.py', 'PYMODULE'),
('unittest.loader', 'C:\\Python312\\Lib\\unittest\\loader.py', 'PYMODULE'),
('unittest.main', 'C:\\Python312\\Lib\\unittest\\main.py', 'PYMODULE'),
('unittest.result', 'C:\\Python312\\Lib\\unittest\\result.py', 'PYMODULE'),
('unittest.runner', 'C:\\Python312\\Lib\\unittest\\runner.py', 'PYMODULE'),
('unittest.signals', 'C:\\Python312\\Lib\\unittest\\signals.py', 'PYMODULE'),
('unittest.suite', 'C:\\Python312\\Lib\\unittest\\suite.py', 'PYMODULE'),
('unittest.util', 'C:\\Python312\\Lib\\unittest\\util.py', 'PYMODULE'),
('urllib', 'C:\\Python312\\Lib\\urllib\\__init__.py', 'PYMODULE'),
('urllib.error', 'C:\\Python312\\Lib\\urllib\\error.py', 'PYMODULE'),
('urllib.parse', 'C:\\Python312\\Lib\\urllib\\parse.py', 'PYMODULE'),
('urllib.request', 'C:\\Python312\\Lib\\urllib\\request.py', 'PYMODULE'),
('urllib.response', 'C:\\Python312\\Lib\\urllib\\response.py', 'PYMODULE'),
('webbrowser', 'C:\\Python312\\Lib\\webbrowser.py', 'PYMODULE'),
('xml', 'C:\\Python312\\Lib\\xml\\__init__.py', 'PYMODULE'),
('xml.parsers', 'C:\\Python312\\Lib\\xml\\parsers\\__init__.py', 'PYMODULE'),
('xml.parsers.expat',
'C:\\Python312\\Lib\\xml\\parsers\\expat.py',
'PYMODULE'),
('xml.sax', 'C:\\Python312\\Lib\\xml\\sax\\__init__.py', 'PYMODULE'),
('xml.sax._exceptions',
'C:\\Python312\\Lib\\xml\\sax\\_exceptions.py',
'PYMODULE'),
('xml.sax.expatreader',
'C:\\Python312\\Lib\\xml\\sax\\expatreader.py',
'PYMODULE'),
('xml.sax.handler', 'C:\\Python312\\Lib\\xml\\sax\\handler.py', 'PYMODULE'),
('xml.sax.saxutils', 'C:\\Python312\\Lib\\xml\\sax\\saxutils.py', 'PYMODULE'),
('xml.sax.xmlreader',
'C:\\Python312\\Lib\\xml\\sax\\xmlreader.py',
'PYMODULE'),
('xmlrpc', 'C:\\Python312\\Lib\\xmlrpc\\__init__.py', 'PYMODULE'),
('xmlrpc.client', 'C:\\Python312\\Lib\\xmlrpc\\client.py', 'PYMODULE'),
('zipfile', 'C:\\Python312\\Lib\\zipfile\\__init__.py', 'PYMODULE'),
('zipfile._path',
'C:\\Python312\\Lib\\zipfile\\_path\\__init__.py',
'PYMODULE'),
('zipfile._path.glob',
'C:\\Python312\\Lib\\zipfile\\_path\\glob.py',
'PYMODULE'),
('zipimport', 'C:\\Python312\\Lib\\zipimport.py', 'PYMODULE')])

View File

@ -0,0 +1,223 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean these modules are required for running your program. Both
Python's standard library and 3rd-party Python packages often conditionally
import optional modules, some of which may be available only on certain
platforms.
Types of import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), http.server (delayed, optional), netrc (delayed, conditional), getpass (delayed)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional)
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
missing module named fcntl - imported by subprocess (optional)
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named termios - imported by tty (top-level), getpass (optional)
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named pyimod02_importers - imported by C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
missing module named _dummy_thread - imported by numpy._core.arrayprint (optional)
missing module named typing_extensions - imported by numpy._typing._nested_sequence (conditional), numpy.random.bit_generator (top-level)
missing module named charset_normalizer - imported by numpy.f2py.crackfortran (optional)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named 'java.lang' - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named _winreg - imported by platform (delayed, optional)
missing module named psutil - imported by numpy.testing._private.utils (delayed, optional)
missing module named readline - imported by cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, optional)
missing module named win32pdh - imported by numpy.testing._private.utils (delayed, conditional)
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
missing module named _typeshed - imported by numpy.random._common (top-level), numpy.random.bit_generator (top-level)
missing module named threadpoolctl - imported by numpy.lib._utils_impl (delayed, optional)
missing module named numpy._core.zeros - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.void - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.vecmat - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.vecdot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.ushort - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.unsignedinteger - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ulonglong - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ulong - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.uintp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.uintc - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.uint64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.uint32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.uint16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.uint - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ubyte - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.trunc - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.true_divide - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._function_base_impl (top-level), numpy (conditional)
missing module named numpy._core.trace - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.timedelta64 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.tensordot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.tanh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.tan - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.swapaxes - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.sum - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.subtract - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.str_ - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.square - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.sqrt - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.spacing - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.sort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.sinh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.single - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.signedinteger - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.signbit - imported by numpy._core (conditional), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.sign - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.short - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.rint - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.right_shift - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.remainder - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.reciprocal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.radians - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.rad2deg - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.prod - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.power - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.positive - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.pi - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.outer - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.ones - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.object_ - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.number - imported by numpy._core (conditional), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.not_equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.nextafter - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.newaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.negative - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ndarray - imported by numpy._core (top-level), numpy.lib._utils_impl (top-level), numpy (conditional), numpy.testing._private.utils (top-level)
missing module named numpy._core.multiply - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.moveaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.modf - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.mod - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.minimum - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.maximum - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.matvec - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.matrix_transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.matmul - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.longlong - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.longdouble - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.long - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_xor - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_or - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_not - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_and - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logaddexp2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logaddexp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log10 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log1p - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.linspace - imported by numpy._core (top-level), numpy.lib._index_tricks_impl (top-level), numpy (conditional)
missing module named numpy._core.less_equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.less - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.left_shift - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ldexp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.lcm - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.isscalar - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.isnan - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.isfinite - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.intp - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy._array_api_info (top-level), numpy.testing._private.utils (top-level)
missing module named numpy._core.integer - imported by numpy._core (conditional), numpy (conditional), numpy.fft._helper (top-level)
missing module named numpy._core.intc - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.int64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.int32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.int16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.int8 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.inf - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.inexact - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.iinfo - imported by numpy._core (top-level), numpy.lib._twodim_base_impl (top-level), numpy (conditional)
missing module named numpy._core.hypot - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.hstack - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.heaviside - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.half - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.greater_equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.greater - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.gcd - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.frompyfunc - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.frexp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fmod - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fmin - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fmax - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.floor_divide - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.floor - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.floating - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.float_power - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.float32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level), numpy.testing._private.utils (top-level)
missing module named numpy._core.float16 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.finfo - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.fabs - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.expm1 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.exp2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.exp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.euler_gamma - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.errstate - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.empty_like - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.empty - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._helper (top-level), numpy.testing._private.utils (top-level)
missing module named numpy._core.e - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.double - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.dot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.divmod - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.divide - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.diagonal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.degrees - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.deg2rad - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.datetime64 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.csingle - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.cross - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.count_nonzero - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.cosh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.cos - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.copysign - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.conjugate - imported by numpy._core (conditional), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.conj - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.complexfloating - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.complex64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.clongdouble - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.character - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ceil - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.cdouble - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.cbrt - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bytes_ - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.byte - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bool_ - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_xor - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_or - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_count - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_and - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.atleast_3d - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
missing module named numpy._core.atleast_2d - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.atleast_1d - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.asarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._array_utils_impl (top-level), numpy (conditional), numpy.fft._helper (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy._core.asanyarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.array - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional), numpy.testing._private.utils (top-level)
missing module named numpy._core.argsort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.arctanh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arctan2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arctan - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arcsinh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arcsin - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arccosh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arccos - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.amin - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.amax - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.all - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed)
missing module named numpy._core.add - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named yaml - imported by numpy.__config__ (delayed)
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)

View File

@ -0,0 +1,81 @@
@echo off
setlocal
cd /d "%~dp0"
echo ============================================================
echo BUILD MASKREVIEWER WINDOWS
echo ============================================================
echo.
where py >nul 2>nul
if %errorlevel%==0 (
set PY=py
) else (
set PY=python
)
%PY% --version
if errorlevel 1 (
echo.
echo [ERRO] Python nao encontrado.
echo Instale Python 3.11 ou 3.12 para Windows e tente novamente.
pause
exit /b 1
)
if not exist ".build_venv\Scripts\python.exe" (
echo [1/4] Criando ambiente de build...
%PY% -m venv .build_venv
if errorlevel 1 goto :erro
)
call ".build_venv\Scripts\activate.bat"
echo [2/4] Instalando dependencias de build...
python -m pip install --upgrade pip
python -m pip install pyinstaller numpy opencv-python
if errorlevel 1 goto :erro
echo [3/4] Limpando builds antigos...
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
if exist MaskReviewer.spec del /q MaskReviewer.spec
echo [4/4] Gerando EXE...
python -m PyInstaller ^
--noconfirm ^
--clean ^
--onedir ^
--windowed ^
--name MaskReviewer ^
--collect-all cv2 ^
MaskReviewer.py
if errorlevel 1 goto :erro
echo.
echo ============================================================
echo BUILD CONCLUIDO
echo ============================================================
echo Pasta gerada:
echo %CD%\dist\MaskReviewer
echo.
echo Coloque ao lado do MaskReviewer.exe:
echo labelmap.txt
echo group\
echo.
echo Estrutura final recomendada:
echo MaskReviewer\
echo MaskReviewer.exe
echo _internal\
echo labelmap.txt
echo group\
echo.
pause
exit /b 0
:erro
echo.
echo [ERRO] Falha no build.
pause
exit /b 1

View File

@ -0,0 +1,19 @@
@echo off
setlocal
cd /d "%~dp0"
where py >nul 2>nul
if %errorlevel%==0 (
set PY=py
) else (
set PY=python
)
%PY% -c "import cv2, numpy" >nul 2>nul
if errorlevel 1 (
echo Instalando numpy e opencv-python...
%PY% -m pip install numpy opencv-python
)
%PY% MaskReviewer.py
if errorlevel 1 pause

View File

@ -8,7 +8,8 @@ Exporta o checkpoint PyTorch do SegFormer Multi-Head OAK-FCC-3 para ONNX.
Exemplo:
python _10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.pt --out backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.onnx --include-norm --postprocess argmax_fullres
python .\_10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.pt --out backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.onnx --include-norm --postprocess argmax_fullres
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --opset 17 --device cuda --include-norm --postprocess argmax_fullres
"""
from __future__ import annotations

View File

@ -13,6 +13,7 @@ para o SegFormer OAK-FCC-3 Multi-Head.
Exemplo:
python _11_validate_onnx.py --config config.json --max_samples 20 --device cuda --onnx_provider cuda --torch_no_amp
python .\_11_validate_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --max_samples 50 --device cuda --onnx_provider cuda --torch_no_amp
Para validar o ONNX com saída já redimensionada:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -25,10 +25,7 @@ Mostra:
Exemplo:
python .\\_9_test_infer_multihead.py ^
--config config.json ^
--split_folder val ^
--ckpt backup\segformer_b1\test_multi\stacked_raw5_multihead\best_score.pt
python .\\_9_test_multihead.py --config config.json --split_folder val --ckpt backup\segformer_b1\test_multi\stacked_raw5_multihead\best_score.pt
Controles:
D / seta direita : próxima amostra
@ -634,7 +631,20 @@ def collect_samples(root: Path, heads_config: Dict[str, dict], require_masks: bo
missing = []
for head_name, hcfg in heads_config.items():
mask_dir = group_dir / str(hcfg.get("mask_dir", "masks"))
mask_dir_name = str(hcfg.get("mask_dir", "masks"))
is_derived = (
bool(hcfg.get("derived", False))
or mask_dir_name == "__derived_target__"
)
# Heads derivadas não possuem máscara física.
# O GT target será construído posteriormente a partir de:
# vegetation == 1 AND cana == 0.
if is_derived:
masks[head_name] = None
continue
mask_dir = group_dir / mask_dir_name
mask_path = mask_dir / f"{base}.npy"
if mask_path.exists():
@ -2002,14 +2012,25 @@ def main():
sample_metrics[head_name] = {"iou": iou, "miou": miou, "acc": acc}
metric_lines.append(f"{head_name}: mIoU={miou:.3f} acc={acc:.3f}")
if gt_target is not None:
cm_t = confusion_matrix_np(pred_target, gt_target, 2, ignore_id)
if gt_target is not None and pred_target_op is not None:
cm_t = confusion_matrix_np(pred_target_op, gt_target, 2, ignore_id)
iou_t, miou_t, acc_t = metrics_from_cm(cm_t)
sample_metrics["target_op"] = {"iou": iou_t, "miou": miou_t, "acc": acc_t}
metric_lines.append(f"target_op: IoU_alvo={iou_t[1]:.3f} acc={acc_t:.3f}")
sample_metrics["target_op"] = {
"iou": iou_t,
"miou": miou_t,
"acc": acc_t,
}
metric_lines.append(
f"target_op: IoU_alvo={iou_t[1]:.3f} acc={acc_t:.3f}"
)
if "target" in sample_metrics:
iou_head = sample_metrics["target"]["iou"]
metric_lines.append(f"target_head: IoU_alvo={iou_head[1]:.3f}")
metric_lines.append(
f"target_head: IoU_alvo={iou_head[1]:.3f}"
)
if idx not in visited:
for head_name, pred in preds.items():

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
{
"camera": "oak-fcc-3",
"modelo": "segformer_b1",
"model_name": "2026_08_18",
"model_name": "2026_08_28_linear_demosaic",
"main_class_name": "erva",
"es_classes": "",
"model_to_use": "geral",
"raw_size": [1280, 800],
"resolucao": [640, 400],
"resolucao": [960, 600],
"roi_inicio": 0.0,
"roi_tamanho": 1.0,
"shaves": 3,
"source_channels": ["R", "G", "B", "RE", "NIR"],
"channels": 7,
"input_channels": ["R", "G", "B", "RE", "NIR", "NDVI", "NDRE"],
"channels": 5,
"input_channels": ["R", "G", "B", "RE", "NIR"],
"derived_channels": {
"epsilon": 1e-6,
"clip_min": -1.0,
@ -20,7 +20,7 @@
},
"backbone": "nvidia/mit-b1",
"fusion_mode": "stacked",
"stats_source_tag": "stacked_raw5_ndvi_ndre",
"stats_source_tag": "stacked_raw5",
"module_params_json": "calibration/module_params.json",
"ckpt_test": "best_score",
"multi_head": true,
@ -41,7 +41,7 @@
"mask_dir": "masks_vegetation",
"classes": {"background": 0, "vegetation": 1},
"ignore_index": 255,
"loss_weight": 0.20
"loss_weight": 0.15
},
"cana": {
"enabled": true,
@ -50,7 +50,7 @@
"mask_dir": "masks_cana",
"classes": {"not_cana": 0, "cana": 1},
"ignore_index": 255,
"loss_weight": 0.25
"loss_weight": 0.35
},
"target": {
"enabled": true,
@ -59,23 +59,158 @@
"mask_dir": "__derived_target__",
"classes": {"background": 0, "target": 1},
"ignore_index": 255,
"loss_weight": 0.35,
"loss_weight": 0.30,
"derived": true
}
},
"target_distillation": {
"enabled": true,
"hard_weight": 0.85,
"distill_weight": 0.15,
"rampup_enabled": true,
"start_epoch": 8,
"rampup_epochs": 12,
"w_sem_erva": 0.45,
"w_veg_not_cana": 0.35,
"w_veg_suppressed": 0.20,
"cana_suppression_power": 1.5,
"teacher_min": 0.0,
"teacher_max": 1.0,
"detach_teacher": true
"training_v2": {
"model": {
"decoder_mode": "shared_light",
"spectral_input_init": "zero_extra"
},
"augmentation": {
"enabled": true,
"horizontal_flip_p": 0.5,
"vertical_flip_p": 0.0,
"affine_p": 0.7,
"rotate_deg": 5.0,
"scale_min": 0.9,
"scale_max": 1.1,
"translate_frac": 0.04,
"crop_p": 0.45,
"crop_scale_min": 0.7,
"crop_scale_max": 1.0,
"crop_focus_target_p": 0.55,
"crop_focus_cana_p": 0.25,
"global_gain_p": 0.35,
"global_gain_min": 0.92,
"global_gain_max": 1.08,
"band_gain_p": 0.25,
"band_gain_min": 0.96,
"band_gain_max": 1.04,
"rgb_gamma_p": 0.2,
"rgb_gamma_min": 0.94,
"rgb_gamma_max": 1.06,
"noise_p": 0.2,
"noise_sigma_min": 0.001,
"noise_sigma_max": 0.008,
"blur_p": 0.12,
"blur_kernel": 3,
"sensor_channel_dropout_p": 0.0,
"clip_physical": true
},
"sampler": {
"mode": "diverse",
"samples_per_epoch": 0,
"tiny_target_pct": 0.005,
"small_target_pct": 0.02,
"medium_target_pct": 0.1
},
"class_weighting": {
"method": "log_inverse",
"log_offset": 1.02,
"power": 0.5,
"min_weight": 0.25,
"max_weight": 4.0
},
"loss": {
"dice_reduction": "per_image",
"dice_smooth": 1.0,
"boundary_weight": 0.0,
"ohem_ratio": 0.0,
"safety": {
"enabled": true,
"weight": 0.08,
"cana_weight": 1.0,
"ground_weight": 0.2
}
},
"optimizer": {
"encoder_lr": null,
"patch_lr_mult": 2.0,
"heads_lr_mult": 5.0,
"weight_decay": null,
"no_decay_bias": true,
"no_decay_norm": true,
"betas": [
0.9,
0.999
],
"eps": 1e-08
},
"scheduler": {
"mode": "poly",
"warmup_ratio": 0.05,
"warmup_start_factor": 0.1,
"poly_power": 1.0,
"min_lr_ratio": 0.02
},
"optimization": {
"grad_clip_norm": 1.0,
"matmul_precision": "high",
"cudnn_benchmark": true,
"persistent_workers": true,
"prefetch_factor": 2
},
"target_distillation": {
"enabled": false,
"mode": "cross_head",
"start_epoch": 8,
"rampup_epochs": 12,
"hard_weight": 0.75,
"distill_weight": 0.25,
"teacher_confidence_min": 0.6,
"detach_teacher": true,
"w_sem_erva": 0.45,
"w_veg_not_cana": 0.35,
"w_veg_suppressed": 0.2,
"cana_suppression_power": 1.5
},
"metrics": {
"target_thresholds": [
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
0.9
],
"ece_bins": 15,
"scenario_metrics": true,
"group_metrics": true,
"rich_train_metrics": false,
"operational_threshold": {
"max_cana_spray_rate": 0.02,
"max_ground_spray_rate": 0.03,
"max_weed_miss_rate": 0.2,
"score_weights": {
"target_iou": 0.35,
"target_f1": 0.2,
"cana_safety": 0.25,
"ground_safety": 0.1,
"weed_recall": 0.1
}
}
},
"selection_score": {
"target_iou": 0.4,
"cana_iou": 0.2,
"target_f1": 0.1,
"vegetation_miou": 0.1,
"semantic_miou": 0.05,
"cana_safety": 0.15
},
"checkpoint": {
"early_stop_min_delta": 0.0005,
"save_best_safety": true,
"save_best_legacy": true,
"save_best_operational": true
},
"data": {
"validate_npy_content": true,
"skip_corrupt_samples": true,
"max_corrupt_fraction": 0.005
}
}
}

View File

@ -1,620 +0,0 @@
import argparse
import json
import sys
import time
from pathlib import Path
import cv2
import numpy as np
try:
import torch
except Exception:
torch = None
# ============================================================
# Ajuste de import local
# ============================================================
THIS_FILE = Path(__file__).resolve()
# Esperado:
# .../Python/Scripts/workers/camera_worker/oak_fcc3_core/benchmark_raw_bruto_scientific.py
WORKERS_DIR = THIS_FILE.parents[2]
if str(WORKERS_DIR) not in sys.path:
sys.path.insert(0, str(WORKERS_DIR))
from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client
try:
from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService
except Exception:
MultiSpecSegformerService = None
# ============================================================
# Utils
# ============================================================
def now_ms():
return time.perf_counter() * 1000.0
def mean(xs):
return float(np.mean(xs)) if xs else 0.0
def p95(xs):
return float(np.percentile(xs, 95)) if xs else 0.0
def maxv(xs):
return float(np.max(xs)) if xs else 0.0
def last_mean(xs, n=30):
return float(np.mean(xs[-n:])) if xs else 0.0
def last_max(xs, n=30):
return float(np.max(xs[-n:])) if xs else 0.0
def load_json_if_exists(path):
if not path:
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def parse_float_list(s, expected=5, default=None):
if s is None:
return default
vals = [float(x.strip()) for x in str(s).split(",") if x.strip() != ""]
if len(vals) != expected:
raise ValueError(f"Esperado {expected} valores, veio {len(vals)}: {s}")
return np.array(vals, dtype=np.float32)
def extract_mean_std_from_model_config(cfg):
if not isinstance(cfg, dict):
return None, None
mean_cfg = cfg.get("mean") or cfg.get("channel_mean") or cfg.get("norm_mean")
std_cfg = cfg.get("std") or cfg.get("channel_std") or cfg.get("norm_std")
norm = cfg.get("normalization") or cfg.get("norm") or {}
if mean_cfg is None and isinstance(norm, dict):
mean_cfg = norm.get("mean") or norm.get("channel_mean")
if std_cfg is None and isinstance(norm, dict):
std_cfg = norm.get("std") or norm.get("channel_std")
if mean_cfg is None or std_cfg is None:
return None, None
mean_arr = np.array(mean_cfg, dtype=np.float32)
std_arr = np.array(std_cfg, dtype=np.float32)
if mean_arr.size != 5 or std_arr.size != 5:
return None, None
return mean_arr, std_arr
def tensor_stats(tensor):
names = ["R", "G", "B", "RE", "NIR"]
out = {}
for i, name in enumerate(names):
ch = tensor[i].astype(np.float32)
out[name] = {
"min": float(np.min(ch)),
"p01": float(np.percentile(ch, 1)),
"p50": float(np.percentile(ch, 50)),
"p99": float(np.percentile(ch, 99)),
"max": float(np.max(ch)),
"mean": float(np.mean(ch)),
"std": float(np.std(ch)),
}
return out
def print_tensor_stats(label, tensor):
print("============================================")
print(f"[{label}] TENSOR")
print(f"shape={tensor.shape} dtype={tensor.dtype}")
stats = tensor_stats(tensor)
for ch, s in stats.items():
print(
f"{ch:>3} | "
f"min={s['min']:.4f} "
f"p01={s['p01']:.4f} "
f"p50={s['p50']:.4f} "
f"p99={s['p99']:.4f} "
f"max={s['max']:.4f} "
f"mean={s['mean']:.4f} "
f"std={s['std']:.4f}"
)
print("============================================")
def to_u8_01(arr):
arr = np.asarray(arr, dtype=np.float32)
arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0)
arr = np.clip(arr, 0.0, 1.0)
return (arr * 255.0).astype(np.uint8)
def make_panel(tensor):
rgb = np.stack([tensor[0], tensor[1], tensor[2]], axis=2)
rgb_bgr = cv2.cvtColor(to_u8_01(rgb), cv2.COLOR_RGB2BGR)
re_bgr = cv2.cvtColor(to_u8_01(tensor[3]), cv2.COLOR_GRAY2BGR)
nir_bgr = cv2.cvtColor(to_u8_01(tensor[4]), cv2.COLOR_GRAY2BGR)
false_rgb = np.stack(
[
tensor[4], # visual R = NIR
tensor[3], # visual G = RE
tensor[0], # visual B = R real
],
axis=2,
)
false_bgr = cv2.cvtColor(to_u8_01(false_rgb), cv2.COLOR_RGB2BGR)
def title(img, text):
h, w = img.shape[:2]
bar_h = 34
bar = np.zeros((bar_h, w, 3), dtype=np.uint8)
cv2.putText(
bar,
text,
(10, 24),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(255, 255, 255),
2,
cv2.LINE_AA,
)
return np.vstack([bar, img])
rgb_bgr = title(rgb_bgr, "RGB")
re_bgr = title(re_bgr, "RE")
nir_bgr = title(nir_bgr, "NIR")
false_bgr = title(false_bgr, "Falso color NIR/RE/R")
top = np.hstack([rgb_bgr, re_bgr])
bottom = np.hstack([nir_bgr, false_bgr])
return np.vstack([top, bottom])
# ============================================================
# Benchmark processor
# ============================================================
class ScientificBenchmark:
def __init__(self, args):
self.args = args
self.target_size = (int(args.width), int(args.height))
self.model_cfg = load_json_if_exists(args.model_config_json)
mean_cfg, std_cfg = extract_mean_std_from_model_config(self.model_cfg)
if args.model_mean is not None:
self.mean = parse_float_list(args.model_mean, expected=5)
elif mean_cfg is not None:
self.mean = mean_cfg
else:
self.mean = np.array([0.5, 0.5, 0.5, 0.5, 0.5], dtype=np.float32)
if args.model_std is not None:
self.std = parse_float_list(args.model_std, expected=5)
elif std_cfg is not None:
self.std = std_cfg
else:
self.std = np.array([0.25, 0.25, 0.25, 0.25, 0.25], dtype=np.float32)
self.mean_chw = self.mean[:, None, None].astype(np.float32)
self.std_chw = self.std[:, None, None].astype(np.float32)
self.device = None
if torch is not None and torch.cuda.is_available():
self.device = torch.device("cuda")
elif torch is not None:
self.device = torch.device("cpu")
self.model_svc = None
if args.run_model:
if MultiSpecSegformerService is None:
raise RuntimeError("MultiSpecSegformerService não pôde ser importado.")
if not isinstance(self.model_cfg, dict):
raise RuntimeError("--run_model requer --model_config_json válido.")
self.model_svc = MultiSpecSegformerService(
model_config=self.model_cfg,
mostrar_log=print,
)
dummy = np.zeros((5, args.height, args.width), dtype=np.float32)
for _ in range(max(0, int(args.warmup_model))):
self.model_svc.infer_tensor_fast(dummy, keep_probs=False)
print(f"[BENCH] Warmup modelo concluído: {args.warmup_model}x")
def process_once(self, client):
"""
Mede uma iteração completa do fluxo científico.
"""
times = {
"capture_ms": 0.0,
"decode_ms": 0.0,
"controller_ms": 0.0,
"fuse_total_ms": 0.0,
"fuse_dark_ms": 0.0,
"fuse_radnorm_ms": 0.0,
"fuse_flat_ms": 0.0,
"fuse_prepare_ms": 0.0,
"fuse_warp_ms": 0.0,
"fuse_crop_resize_ms": 0.0,
"fuse_concat_ms": 0.0,
"model_norm_ms": 0.0,
"torch_ms": 0.0,
"infer_ms": 0.0,
"total_ms": 0.0,
"sync_dt_ms": 0.0,
}
t_total0 = now_ms()
# ========================================================
# 1. Captura RAW_BRUTO
# ========================================================
t0 = now_ms()
raw_frame, raw_meta = client.get_next_raw_frame(timeout=self.args.timeout)
times["capture_ms"] = now_ms() - t0
#cp = raw_meta.get("capture_perf", {})
#print(
# "[CAP_ASYNC] "
# f"get_wait={cp.get('async_get_wait_ms',0):.2f}ms "
# f"age={cp.get('async_packet_age_ms',0):.2f}ms "
# f"seq={cp.get('async_packet_seq')} "
# f"thread_wait={cp.get('wait_total_ms',0):.2f}ms "
# f"sleep={cp.get('sleep_ms',0):.2f}ms/{cp.get('sleep_count',0)} "
# f"drain={cp.get('drain_total_ms',0):.2f}ms "
# f"copy={cp.get('drain_frombuffer_copy_ms',0):.2f}ms "
# f"status={cp.get('async_status',{})}"
#)
times["sync_dt_ms"] = float(raw_meta.get("sync_dt_ms", 0.0) or 0.0)
# ========================================================
# 2. Decode RAW10 packed -> float científico por câmera
# ========================================================
t0 = now_ms()
decoded = client.decode_stream_cameras(raw_frame, raw_meta)
times["decode_ms"] = now_ms() - t0
# ========================================================
# 3. RadiometricController update, se ativo
# Isso NÃO é a radiometric_normalization do tensor.
# É o controlador de exposição/ganho.
# ========================================================
t0 = now_ms()
client.update_radiometry(decoded, raw_meta)
times["controller_ms"] = now_ms() - t0
# ========================================================
# 4. Fusão científica no RawProcessorCore
# dark + radnorm + flat + homografia + crop/resize + concat
# ========================================================
t0 = now_ms()
tensor = client.build_infer_tensor_from_decoded(
decoded=decoded,
meta=raw_meta,
channels_expected=5,
target_size=self.target_size,
)
times["fuse_total_ms"] = now_ms() - t0
# Pega detalhamento interno do core
try:
perf = (client.core.last_fusion_result or {}).get("perf", {}) or {}
times["fuse_dark_ms"] = float(perf.get("dark_ms", 0.0) or 0.0)
times["fuse_radnorm_ms"] = float(perf.get("radnorm_ms", 0.0) or 0.0)
times["fuse_flat_ms"] = float(perf.get("flat_ms", 0.0) or 0.0)
times["fuse_prepare_ms"] = float(perf.get("prepare_ms", 0.0) or 0.0)
times["fuse_warp_ms"] = float(perf.get("warp_total_ms", 0.0) or 0.0)
times["fuse_crop_resize_ms"] = float(perf.get("crop_resize_ms", 0.0) or 0.0)
times["fuse_concat_ms"] = float(perf.get("concat_ms", 0.0) or 0.0)
except Exception:
pass
# ========================================================
# 5. Normalização do modelo, opcional
# ========================================================
if self.args.simulate_model_norm:
t0 = now_ms()
tensor = (tensor - self.mean_chw) / np.maximum(self.std_chw, 1e-6)
tensor = np.ascontiguousarray(tensor, dtype=np.float32)
times["model_norm_ms"] = now_ms() - t0
# ========================================================
# 6. Transferência para torch/cuda, opcional
# ========================================================
if self.args.to_torch:
if torch is None:
raise RuntimeError("--to_torch requer torch instalado.")
t0 = now_ms()
x = torch.from_numpy(tensor).unsqueeze(0).to(self.device, non_blocking=True)
if self.device is not None and self.device.type == "cuda":
torch.cuda.synchronize()
times["torch_ms"] = now_ms() - t0
# ========================================================
# 7. Inferência real, opcional
# ========================================================
pred = None
if self.args.run_model:
t0 = now_ms()
pred = self.model_svc.infer_tensor_fast(tensor, keep_probs=False)
times["infer_ms"] = now_ms() - t0
times["total_ms"] = now_ms() - t_total0
return tensor, pred, raw_frame, raw_meta, decoded, times
# ============================================================
# Main
# ============================================================
def main():
ap = argparse.ArgumentParser(
description="Benchmark científico OAK-FCC-3 RAW_BRUTO -> tensor multiespectral final."
)
ap.add_argument(
"--module_params",
default=r"C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\calibration\module_params.json",
help="Caminho do module_params.json.",
)
ap.add_argument("--width", type=int, default=1024, help="Largura final do tensor.")
ap.add_argument("--height", type=int, default=640, help="Altura final do tensor.")
ap.add_argument("--fps", type=float, default=30.0)
ap.add_argument("--seconds", type=float, default=20.0)
ap.add_argument("--timeout", type=float, default=3.0)
ap.add_argument("--warmup", type=int, default=5)
ap.add_argument("--mx_id", default=None)
ap.add_argument("--sync_tolerance_ms", type=float, default=25.0)
ap.add_argument("--buffer_size", type=int, default=8)
ap.add_argument("--simulate_model_norm", action="store_true")
ap.add_argument("--model_mean", default=None)
ap.add_argument("--model_std", default=None)
ap.add_argument("--to_torch", action="store_true")
ap.add_argument("--run_model", action="store_true")
ap.add_argument("--model_config_json", default=None)
ap.add_argument("--warmup_model", type=int, default=3)
ap.add_argument("--save_debug", action="store_true")
ap.add_argument("--debug_dir", default="raw_bruto_scientific_benchmark")
ap.add_argument("--show", action="store_true")
ap.add_argument("--display_scale", type=float, default=0.65)
args = ap.parse_args()
bench = ScientificBenchmark(args)
client = OakFcc3Client(
width=args.width,
height=args.height,
fps=args.fps,
frame_type="RAW_BRUTO",
output_dtype="uint8",
capture_mode="TRIPLE",
raw_policy="require_triple",
module_calibration_json=args.module_params,
sync_tolerance_ms=args.sync_tolerance_ms,
buffer_size=args.buffer_size,
mx_id=args.mx_id,
)
client.core.warmup_numba_raw10_decode()
samples = {
"capture_ms": [],
"decode_ms": [],
"controller_ms": [],
"fuse_total_ms": [],
"fuse_dark_ms": [],
"fuse_radnorm_ms": [],
"fuse_flat_ms": [],
"fuse_prepare_ms": [],
"fuse_warp_ms": [],
"fuse_crop_resize_ms": [],
"fuse_concat_ms": [],
"model_norm_ms": [],
"torch_ms": [],
"infer_ms": [],
"total_ms": [],
"sync_dt_ms": [],
}
n_frames = 0
t_start = time.perf_counter()
t_last_log = t_start
last_tensor = None
last_meta = None
try:
client.start(print_debug=True)
# Warmup de câmera/controlador/filas
print(f"[BENCH] Warmup frames: {args.warmup}")
for _ in range(max(0, int(args.warmup))):
try:
bench.process_once(client)
except Exception as e:
print(f"[WARN] warmup falhou: {type(e).__name__}: {e}")
time.sleep(0.02)
print("============================================")
print("[BENCH] Iniciando benchmark científico RAW_BRUTO")
print(f"target tensor : (5,{args.height},{args.width})")
print(f"duration : {args.seconds}s")
print("============================================")
while True:
now = time.perf_counter()
elapsed = now - t_start
if elapsed >= args.seconds:
break
try:
tensor, pred, raw_frame, raw_meta, decoded, times = bench.process_once(client)
except TimeoutError as e:
print(f"[TIMEOUT] {e}")
continue
n_frames += 1
last_tensor = tensor
last_meta = raw_meta
for k in samples:
samples[k].append(float(times.get(k, 0.0) or 0.0))
if now - t_last_log >= 1.0:
elapsed = now - t_start
fps = n_frames / max(elapsed, 1e-6)
print(
"[RAW_SCI_PERF] "
f"elapsed={elapsed:.1f}s "
f"frames={n_frames} "
f"fps={fps:.2f} "
f"sync={last_mean(samples['sync_dt_ms']):.2f}ms "
f"capture={last_mean(samples['capture_ms']):.2f}ms "
f"decode={last_mean(samples['decode_ms']):.2f}ms "
f"controller={last_mean(samples['controller_ms']):.2f}ms "
f"fuse={last_mean(samples['fuse_total_ms']):.2f}ms "
f"radnorm={last_mean(samples['fuse_radnorm_ms']):.2f}ms "
f"flat={last_mean(samples['fuse_flat_ms']):.2f}ms "
f"warp={last_mean(samples['fuse_warp_ms']):.2f}ms "
f"crop_resize={last_mean(samples['fuse_crop_resize_ms']):.2f}ms "
f"concat={last_mean(samples['fuse_concat_ms']):.2f}ms "
f"model_norm={last_mean(samples['model_norm_ms']):.2f}ms "
f"torch={last_mean(samples['torch_ms']):.2f}ms "
f"infer={last_mean(samples['infer_ms']):.2f}ms "
f"total={last_mean(samples['total_ms']):.2f}ms "
f"tensor_shape={tuple(tensor.shape)}"
)
t_last_log = now
elapsed_total = time.perf_counter() - t_start
fps_total = n_frames / max(elapsed_total, 1e-6)
print("============================================")
print("RESULTADO FINAL RAW_BRUTO CIENTÍFICO")
print(f"elapsed : {elapsed_total:.2f}s")
print(f"frames : {n_frames}")
print(f"fps : {fps_total:.2f}")
print("--------------------------------------------")
def print_metric(name):
xs = samples[name]
print(
f"{name:18s} "
f"mean={mean(xs):8.2f}ms "
f"p95={p95(xs):8.2f}ms "
f"max={maxv(xs):8.2f}ms"
)
for name in [
"sync_dt_ms",
"capture_ms",
"decode_ms",
"controller_ms",
"fuse_total_ms",
"fuse_dark_ms",
"fuse_radnorm_ms",
"fuse_flat_ms",
"fuse_prepare_ms",
"fuse_warp_ms",
"fuse_crop_resize_ms",
"fuse_concat_ms",
"model_norm_ms",
"torch_ms",
"infer_ms",
"total_ms",
]:
print_metric(name)
print("============================================")
if last_tensor is not None:
print_tensor_stats("LAST RAW_BRUTO SCI", last_tensor)
if args.save_debug:
debug_dir = Path(args.debug_dir)
debug_dir.mkdir(parents=True, exist_ok=True)
np.save(str(debug_dir / "last_tensor.npy"), last_tensor)
stats_path = debug_dir / "last_tensor_stats.json"
with open(stats_path, "w", encoding="utf-8") as f:
json.dump(tensor_stats(last_tensor), f, indent=2, ensure_ascii=False)
panel = make_panel(last_tensor)
cv2.imwrite(str(debug_dir / "last_tensor_panel.png"), panel)
if last_meta is not None:
with open(debug_dir / "last_meta.json", "w", encoding="utf-8") as f:
json.dump(last_meta, f, indent=2, ensure_ascii=False)
print(f"[SAVE] Debug salvo em: {debug_dir}")
if args.show:
panel = make_panel(last_tensor)
if args.display_scale and abs(args.display_scale - 1.0) > 1e-6:
new_w = max(1, int(panel.shape[1] * args.display_scale))
new_h = max(1, int(panel.shape[0] * args.display_scale))
panel = cv2.resize(panel, (new_w, new_h), interpolation=cv2.INTER_AREA)
cv2.imshow("RAW_BRUTO scientific tensor", panel)
print("[INFO] Pressione qualquer tecla para fechar.")
cv2.waitKey(0)
cv2.destroyAllWindows()
finally:
try:
client.stop()
except Exception:
pass
if __name__ == "__main__":
main()

View File

@ -1,645 +0,0 @@
import argparse
import json
import sys
import time
import threading
from pathlib import Path
import cv2
import numpy as np
# ============================================================
# Ajuste de import local
# ============================================================
THIS_FILE = Path(__file__).resolve()
WORKERS_DIR = THIS_FILE.parents[2]
if str(WORKERS_DIR) not in sys.path:
sys.path.insert(0, str(WORKERS_DIR))
from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client
try:
from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService
except Exception:
MultiSpecSegformerService = None
# ============================================================
# FPS / utilidades
# ============================================================
class FpsMeter:
def __init__(self, alpha=0.15):
self.alpha = float(alpha)
self.last_ts = None
self.fps = 0.0
def tick(self):
now = time.perf_counter()
if self.last_ts is not None:
dt = now - self.last_ts
if dt > 1e-9:
inst = 1.0 / dt
self.fps = inst if self.fps <= 0 else (1.0 - self.alpha) * self.fps + self.alpha * inst
self.last_ts = now
return self.fps
def load_json_if_exists(path):
if not path:
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def init_model_service(args):
"""
Mesmo contrato do benchmark científico:
--run_model exige --model_config_json
MultiSpecSegformerService(model_config=cfg).infer_tensor_fast(tensor, keep_probs=False)
"""
if not args.run_model:
return None
if MultiSpecSegformerService is None:
raise RuntimeError("MultiSpecSegformerService não pôde ser importado.")
model_cfg = load_json_if_exists(args.model_config_json)
if not isinstance(model_cfg, dict):
raise RuntimeError("--run_model requer --model_config_json válido.")
svc = MultiSpecSegformerService(
model_config=model_cfg,
mostrar_log=print,
)
dummy = np.zeros((5, int(args.height), int(args.width)), dtype=np.float32)
for _ in range(max(0, int(args.warmup_model))):
svc.infer_tensor_fast(dummy, keep_probs=False)
print(f"[MODEL] Warmup concluído: {args.warmup_model}x")
return svc
def to_u8_01(arr, auto_level=False):
arr = np.asarray(arr, dtype=np.float32)
arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0)
if auto_level:
p1 = float(np.percentile(arr, 1))
p99 = float(np.percentile(arr, 99))
den = max(p99 - p1, 1e-6)
arr = (arr - p1) / den
arr = np.clip(arr, 0.0, 1.0)
return (arr * 255.0).astype(np.uint8)
def ensure_bgr(img, auto_level=False):
img = np.asarray(img)
if img.ndim == 2:
g = to_u8_01(img, auto_level=auto_level)
return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR)
if img.ndim == 3 and img.shape[2] == 3:
u8 = to_u8_01(img, auto_level=auto_level)
# decoded/tensor RGB vem em RGB; OpenCV mostra BGR
return cv2.cvtColor(u8, cv2.COLOR_RGB2BGR)
raise RuntimeError(f"Imagem inválida para visualização: shape={img.shape}")
def tensor_rgb_to_bgr(tensor, auto_level=False):
rgb = np.stack([tensor[0], tensor[1], tensor[2]], axis=2)
return ensure_bgr(rgb, auto_level=auto_level)
def tensor_channel_to_bgr(tensor, idx, auto_level=False):
return ensure_bgr(tensor[idx], auto_level=auto_level)
def add_title(img, title, color=(255, 255, 255)):
out = img.copy()
h, w = out.shape[:2]
bar_h = 34
bar = np.zeros((bar_h, w, 3), dtype=np.uint8)
cv2.putText(bar, str(title), (10, 23), cv2.FONT_HERSHEY_SIMPLEX, 0.62, color, 2, cv2.LINE_AA)
return np.vstack([bar, out])
def add_hud(panel, lines):
out = panel.copy()
x, y = 12, 45
for line in lines:
cv2.putText(out, line, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.62, (0, 255, 255), 2, cv2.LINE_AA)
y += 24
return out
def resize_tile(img, tile_w, tile_h):
return cv2.resize(img, (int(tile_w), int(tile_h)), interpolation=cv2.INTER_AREA)
def get_cam_by_role(decoded, role):
role = str(role).lower()
for cam_id, item in decoded.items():
r = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
if r == role:
return cam_id
return None
def decoded_raw_tiles(decoded, tile_w, tile_h, auto_level=False):
"""
Retorna tiles RAW/decoded para RGB, RE, NIR antes da fusão final.
Aqui 'RAW_BRUTO' significa o conteúdo decodificado vindo das câmeras, ainda no espaço nativo.
"""
tiles = {}
rgb_id = get_cam_by_role(decoded, "rgb")
re_id = get_cam_by_role(decoded, "re")
nir_id = get_cam_by_role(decoded, "nir")
if rgb_id is not None:
img = decoded[rgb_id]["image"]
tiles["rgb"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h)
else:
tiles["rgb"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8)
if re_id is not None:
img = decoded[re_id]["image"]
tiles["re"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h)
else:
tiles["re"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8)
if nir_id is not None:
img = decoded[nir_id]["image"]
tiles["nir"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h)
else:
tiles["nir"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8)
return tiles
def tensor_tiles(tensor, tile_w, tile_h, auto_level=False):
return {
"rgb": resize_tile(tensor_rgb_to_bgr(tensor, auto_level=auto_level), tile_w, tile_h),
"re": resize_tile(tensor_channel_to_bgr(tensor, 3, auto_level=auto_level), tile_w, tile_h),
"nir": resize_tile(tensor_channel_to_bgr(tensor, 4, auto_level=auto_level), tile_w, tile_h),
}
def colorize_label_map(label_map, num_classes=None):
label = np.asarray(label_map)
if label.ndim == 3:
label = np.argmax(label, axis=0)
label = label.astype(np.int32)
if num_classes is None:
num_classes = int(max(1, label.max() + 1))
# Paleta simples e estável. BGR.
palette = np.array([
[40, 40, 40],
[60, 180, 60],
[60, 60, 220],
[220, 180, 60],
[180, 60, 180],
[180, 180, 60],
[60, 180, 180],
[220, 220, 220],
], dtype=np.uint8)
out = palette[label % len(palette)]
return out
def try_extract_prediction_tiles(pred, target_w, target_h):
"""
Tentativa genérica. Adapte aqui se o benchmark tiver nomes específicos das cabeças.
Retorna até 3 tiles BGR: semântica/head0/head1.
"""
if pred is None:
blank = np.zeros((target_h, target_w, 3), dtype=np.uint8)
return [blank, blank.copy(), blank.copy()], ["Pred vazio", "Head 1", "Head 2"]
candidates = []
names = []
if isinstance(pred, dict):
# nomes comuns
for key in ("mask", "pred_mask", "class_map", "semantic", "semantic_mask", "segmentation"):
if key in pred:
candidates.append(pred[key])
names.append(key)
heads = pred.get("heads") or pred.get("head_outputs") or pred.get("predictions")
if isinstance(heads, dict):
for k, v in heads.items():
candidates.append(v)
names.append(str(k))
elif isinstance(heads, (list, tuple)):
for i, v in enumerate(heads):
candidates.append(v)
names.append(f"head_{i}")
else:
candidates.append(pred)
names.append("prediction")
tiles = []
out_names = []
for name, arr in zip(names, candidates):
arr = np.asarray(arr)
# remove batch se existir
if arr.ndim == 4 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim == 3:
# CHW logits/probs ou HWC RGB/probs
if arr.shape[0] <= 32:
vis = colorize_label_map(np.argmax(arr, axis=0), num_classes=arr.shape[0])
elif arr.shape[2] in (1, 3):
vis = ensure_bgr(arr[:, :, 0] if arr.shape[2] == 1 else arr, auto_level=True)
else:
vis = ensure_bgr(np.max(arr, axis=2), auto_level=True)
elif arr.ndim == 2:
# se parecer label map, colore; se parecer float, cinza auto-level
if np.issubdtype(arr.dtype, np.integer) and int(np.max(arr)) <= 64:
vis = colorize_label_map(arr)
else:
vis = ensure_bgr(arr, auto_level=True)
elif arr.ndim == 1:
# vetor de classe/score: desenha texto
vis = np.zeros((target_h, target_w, 3), dtype=np.uint8)
txt = np.array2string(arr[:8], precision=2, separator=", ")
cv2.putText(vis, txt[:80], (10, target_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
else:
continue
vis = resize_tile(vis, target_w, target_h)
tiles.append(vis)
out_names.append(name)
if len(tiles) >= 3:
break
while len(tiles) < 3:
tiles.append(np.zeros((target_h, target_w, 3), dtype=np.uint8))
out_names.append(f"pred_{len(tiles)}")
return tiles[:3], out_names[:3]
def try_run_model(model_svc, tensor):
"""
Inferência real, igual ao benchmark científico.
Mantém esta função isolada para adaptar fácil caso o retorno do modelo mude.
"""
if model_svc is None:
return None, "model_svc_none"
if hasattr(model_svc, "infer_tensor_fast"):
return model_svc.infer_tensor_fast(tensor, keep_probs=False), None
if hasattr(model_svc, "infer"):
return model_svc.infer(tensor), None
return None, "model_svc_sem_infer"
def build_grid(raw_tiles, final_tiles, pred_tiles=None, pred_names=None, tile_w=420, tile_h=260):
rows = []
row_defs = [
("RGB", "rgb"),
("RE", "re"),
("NIR", "nir"),
]
for i, (label, key) in enumerate(row_defs):
left = add_title(raw_tiles[key], f"RAW_BRUTO decoded {label}")
mid = add_title(final_tiles[key], f"Tensor final {label}")
cells = [left, mid]
if pred_tiles is not None:
name = pred_names[i] if pred_names and i < len(pred_names) else f"Pred {i}"
cells.append(add_title(pred_tiles[i], name))
# iguala altura após título
h_min = min(c.shape[0] for c in cells)
norm = [cv2.resize(c, (tile_w, h_min), interpolation=cv2.INTER_AREA) if c.shape[1] != tile_w or c.shape[0] != h_min else c for c in cells]
rows.append(np.hstack(norm))
return np.vstack(rows)
def get_core_perf(client):
for attr in ("raw_processor", "processor", "core", "raw_processor_core"):
obj = getattr(client, attr, None)
if obj is not None and getattr(obj, "last_fusion_result", None) is not None:
return obj.last_fusion_result.get("perf", {}) or {}
return {}
# ============================================================
# Estado compartilhado / workers assíncronos
# ============================================================
class SharedState:
def __init__(self):
self.lock = threading.Lock()
self.running = True
self.latest_decoded = None
self.latest_meta = None
self.latest_tensor = None
self.latest_perf = {}
self.latest_pred = None
self.latest_model_warn = None
self.latest_model_ms = 0.0
self.latest_error = None
self.tensor_fps = FpsMeter()
self.model_fps = FpsMeter()
self.preview_fps = FpsMeter()
self.tensor_seq = 0
self.model_seq = 0
def stop(self):
with self.lock:
self.running = False
def is_running(self):
with self.lock:
return bool(self.running)
def tensor_worker(client, state, args):
"""
Roda no talo: captura RAW_BRUTO, monta tensor final e atualiza cache.
Não depende do FPS da janela.
"""
while state.is_running():
try:
frame, meta, decoded = client.get_next_decoded(timeout=args.timeout)
if not isinstance(frame, dict):
raise RuntimeError(f"RAW_BRUTO esperado como dict. Veio {type(frame)}")
tensor = client.build_infer_tensor_from_decoded(
decoded=decoded,
meta=meta,
channels_expected=5,
target_size=(args.width, args.height),
)
tensor = np.ascontiguousarray(tensor.astype(np.float32, copy=False))
perf = get_core_perf(client)
fps = state.tensor_fps.tick()
with state.lock:
state.latest_decoded = decoded
state.latest_meta = meta
state.latest_tensor = tensor
state.latest_perf = dict(perf or {})
state.tensor_seq += 1
state.latest_error = None
except Exception as e:
with state.lock:
state.latest_error = f"tensor_worker: {type(e).__name__}: {e}"
time.sleep(0.02)
def model_worker(model_svc, state, args):
"""
Opcional: roda inferência no último tensor disponível.
Não bloqueia o worker de tensor nem a janela.
"""
last_seq = -1
while state.is_running():
with state.lock:
tensor = None if state.latest_tensor is None else state.latest_tensor.copy()
seq = state.tensor_seq
if tensor is None or seq == last_seq:
time.sleep(0.005)
continue
last_seq = seq
try:
t0_model = time.perf_counter()
pred, warn = try_run_model(model_svc, tensor)
model_ms = (time.perf_counter() - t0_model) * 1000.0
if warn is None:
state.model_fps.tick()
with state.lock:
state.latest_pred = pred
state.latest_model_warn = warn
state.latest_model_ms = float(model_ms)
state.model_seq += 1
except Exception as e:
with state.lock:
state.latest_model_warn = f"model_worker: {type(e).__name__}: {e}"
time.sleep(0.02)
def snapshot_state(state):
"""
Copia referências do cache para desenhar. A janela só roda na cadência do preview.
"""
with state.lock:
return {
"decoded": state.latest_decoded,
"meta": state.latest_meta,
"tensor": state.latest_tensor,
"perf": dict(state.latest_perf or {}),
"pred": state.latest_pred,
"model_warn": state.latest_model_warn,
"model_ms": state.latest_model_ms,
"error": state.latest_error,
"tensor_fps": state.tensor_fps.fps,
"model_fps": state.model_fps.fps,
"tensor_seq": state.tensor_seq,
"model_seq": state.model_seq,
}
# ============================================================
# Main loop assíncrono
# ============================================================
def main():
ap = argparse.ArgumentParser(description="Preview assíncrono RAW_BRUTO decoded vs tensor final multispectral.")
ap.add_argument("--module_params", default=r"C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\calibration\module_params.json")
ap.add_argument("--width", type=int, default=1024, help="Largura final do tensor.")
ap.add_argument("--height", type=int, default=640, help="Altura final do tensor.")
ap.add_argument("--fps", type=float, default=40.0, help="FPS alvo da câmera.")
ap.add_argument("--preview_fps", type=float, default=5.0, help="FPS da janela OpenCV apenas.")
ap.add_argument("--timeout", type=float, default=2.0)
ap.add_argument("--warmup", type=int, default=5)
ap.add_argument("--mx_id", default=None)
ap.add_argument("--display_scale", type=float, default=0.75)
ap.add_argument("--tile_w", type=int, default=420)
ap.add_argument("--tile_h", type=int, default=260)
ap.add_argument("--auto_level_raw", action="store_true")
ap.add_argument("--auto_level_tensor", action="store_true")
ap.add_argument("--run_model", action="store_true", help="Roda inferência em thread separada usando o último tensor cacheado.")
ap.add_argument("--model_config_json", default=None, help="JSON de configuração do modelo SegFormer, igual ao benchmark.")
ap.add_argument("--warmup_model", type=int, default=3, help="Número de inferências dummy para aquecer o modelo.")
ap.add_argument("--save_last", default=None)
args = ap.parse_args()
client = OakFcc3Client(
width=args.width,
height=args.height,
fps=args.fps,
frame_type="RAW_BRUTO",
capture_mode="TRIPLE",
raw_policy="require_triple",
module_calibration_json=args.module_params,
mx_id=args.mx_id,
)
model_svc = init_model_service(args) if args.run_model else None
state = SharedState()
last_panel = None
last_warn_ts = 0.0
last_seq_drawn = -1
try:
client.start(print_debug=True)
for _ in range(max(0, int(args.warmup))):
try:
client.get_next_decoded(timeout=args.timeout)
except Exception:
pass
time.sleep(0.03)
tw = threading.Thread(target=tensor_worker, args=(client, state, args), daemon=True)
tw.start()
mw = None
if args.run_model:
mw = threading.Thread(target=model_worker, args=(model_svc, state, args), daemon=True)
mw.start()
min_period = 1.0 / max(float(args.preview_fps), 0.1)
next_draw_ts = 0.0
print("[INFO] Preview assíncrono iniciado. Pressione Q ou ESC para sair.")
print("[INFO] Tensor FPS = geração real do tensor. Preview FPS = janela. Model FPS = inferência, se habilitada.")
while True:
now = time.perf_counter()
if now < next_draw_ts:
time.sleep(min(0.005, next_draw_ts - now))
key = cv2.waitKey(1) & 0xFF
if key in (27, ord('q'), ord('Q')):
break
continue
next_draw_ts = now + min_period
snap = snapshot_state(state)
if snap["tensor"] is None or snap["decoded"] is None:
blank = np.zeros((360, 900, 3), dtype=np.uint8)
cv2.putText(blank, "Aguardando primeiro tensor...", (30, 180), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA)
cv2.imshow("OAK RAW_BRUTO vs Tensor Final", blank)
key = cv2.waitKey(1) & 0xFF
if key in (27, ord('q'), ord('Q')):
break
continue
# Desenha só usando o cache. Não captura nem monta tensor aqui.
t_draw0 = time.perf_counter()
raw_tiles = decoded_raw_tiles(
snap["decoded"],
tile_w=args.tile_w,
tile_h=args.tile_h,
auto_level=args.auto_level_raw,
)
final_tiles = tensor_tiles(
snap["tensor"],
tile_w=args.tile_w,
tile_h=args.tile_h,
auto_level=args.auto_level_tensor,
)
pred_tiles = None
pred_names = None
if args.run_model:
pred_tiles, pred_names = try_extract_prediction_tiles(
snap["pred"],
target_w=args.tile_w,
target_h=args.tile_h,
)
if snap["model_warn"]:
tnow = time.time()
if tnow - last_warn_ts > 2.0:
print(f"[WARN][MODEL] {snap['model_warn']}")
last_warn_ts = tnow
panel = build_grid(
raw_tiles=raw_tiles,
final_tiles=final_tiles,
pred_tiles=pred_tiles,
pred_names=pred_names,
tile_w=args.tile_w,
tile_h=args.tile_h,
)
preview_fps = state.preview_fps.tick()
draw_ms = (time.perf_counter() - t_draw0) * 1000.0
perf = snap["perf"]
meta = snap["meta"] or {}
tensor_seq = int(snap["tensor_seq"])
dropped_for_preview = max(0, tensor_seq - last_seq_drawn - 1) if last_seq_drawn >= 0 else 0
last_seq_drawn = tensor_seq
hud = [
f"Preview FPS: {preview_fps:.1f} | Tensor FPS: {snap['tensor_fps']:.1f} | Model FPS: {snap['model_fps']:.1f} | infer={snap.get('model_ms', 0.0):.1f}ms",
f"draw={draw_ms:.1f}ms flat={perf.get('flat_ms', 0):.1f} warp={perf.get('warp_total_ms', 0):.1f} crop={perf.get('crop_resize_ms', 0):.1f} fuse={perf.get('total_ms', 0):.1f}",
f"seq={tensor_seq} skipped_preview={dropped_for_preview} frame_type={meta.get('frame_type')} run_model={args.run_model}",
]
if snap["error"]:
hud.append(str(snap["error"])[:120])
panel = add_hud(panel, hud)
last_panel = panel
disp = panel
if args.display_scale and abs(args.display_scale - 1.0) > 1e-6:
disp = cv2.resize(
disp,
(max(1, int(disp.shape[1] * args.display_scale)), max(1, int(disp.shape[0] * args.display_scale))),
interpolation=cv2.INTER_AREA,
)
cv2.imshow("OAK RAW_BRUTO vs Tensor Final", disp)
key = cv2.waitKey(1) & 0xFF
if key in (27, ord('q'), ord('Q')):
break
finally:
state.stop()
time.sleep(0.05)
if args.save_last and last_panel is not None:
out_path = Path(args.save_last)
out_path.parent.mkdir(parents=True, exist_ok=True)
cv2.imwrite(str(out_path), last_panel)
print(f"[SAVE] {out_path}")
try:
client.stop()
except Exception:
pass
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View File

@ -46,6 +46,7 @@ class OakFcc3Client:
mx_id=None,
imu_modo="rotation_vector",
imu_freq_hz=200,
evaluate_quality=True,
**kwargs,
):
self.width = width
@ -63,6 +64,16 @@ class OakFcc3Client:
self.imu_modo = str(imu_modo).strip().lower()
self.imu_freq_hz = int(imu_freq_hz)
# Auditoria radiométrica completa do Raw5.
#
# True mantém o comportamento histórico e é útil para captura científica,
# normalize/auditoria e ferramentas offline.
#
# No runtime em tempo real deve ficar False: evaluate_frame_quality()
# calcula estatísticas/percentis pesados e não faz parte da montagem
# necessária para a inferência.
self.evaluate_quality = bool(evaluate_quality)
self.mx_id = str(mx_id) if mx_id else None
self.svc = OakFcc3Service(
@ -327,8 +338,33 @@ class OakFcc3Client:
target_size=target_size,
)
def build_infer_tensor_from_decoded(self, decoded, meta, channels_expected, target_size=None):
def build_infer_tensor_from_decoded(
self,
decoded,
meta,
channels_expected,
target_size=None,
evaluate_quality=None,
):
"""
Monta o Raw5 físico a partir das câmeras já decodificadas.
evaluate_quality:
- None -> usa self.evaluate_quality
- True -> executa evaluate_frame_quality() e atualiza
core.last_frame_quality_result
- False -> não executa a auditoria pesada e limpa
core.last_frame_quality_result
A flag altera somente a auditoria de qualidade. Não altera decode,
radiometria, flat-field, homografia, crop/resize ou patch normalization.
"""
channels_expected = self._validate_physical_channel_count(channels_expected)
if evaluate_quality is None:
evaluate_quality = self.evaluate_quality
evaluate_quality = bool(evaluate_quality)
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
tensor = self.core.resize_tensor_chw(tensor, target_size=target_size)
@ -338,7 +374,12 @@ class OakFcc3Client:
if bool(patch_cfg.get("enabled", False)):
tensor = self.core.apply_patch_normalization_to_tensor(tensor)
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
if evaluate_quality:
self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor)
else:
# Evita deixar um resultado antigo parecer referente ao frame atual.
self.core.last_frame_quality_result = None
return tensor
def decode_stream_cameras(self, frame, meta):
@ -382,12 +423,19 @@ class OakFcc3Client:
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
def build_multispec_tensor(self, decoded, meta=None, target_size=None):
def build_multispec_tensor(
self,
decoded,
meta=None,
target_size=None,
evaluate_quality=None,
):
tensor = self.build_infer_tensor_from_decoded(
decoded=decoded,
meta=meta,
channels_expected=5,
target_size=target_size,
evaluate_quality=evaluate_quality,
)
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))

View File

@ -9,6 +9,8 @@ import cv2
import depthai as dai
import numpy as np
from itertools import product
class OakFcc3Manager:
"""
@ -43,6 +45,8 @@ class OakFcc3Manager:
raw_policy="allow_single",
roles=None,
sync_mode="best",
hardware_sync_enabled=True,
frame_sync_master="CAM_A",
sync_tolerance_ms=12.0,
buffer_size=8,
only_camera=None,
@ -52,6 +56,9 @@ class OakFcc3Manager:
imu_modo="rotation_vector",
imu_freq_hz=200,
):
self.hardware_sync_enabled = hardware_sync_enabled
self.frame_sync_master = frame_sync_master
self.fps = fps
# Para compatibilidade, mantemos width/height.
@ -432,6 +439,49 @@ class OakFcc3Manager:
# Pipeline creation, MULTISPEC aligned on OAK
# ============================================================
def _aplicar_frame_sync(self, cam, cam_id):
cam_id_normalizado = str(cam_id).strip().upper()
master = str(self.frame_sync_master).strip().upper()
if not self.hardware_sync_enabled:
print(
f"[OAK FSYNC] cam={cam_id_normalizado} "
f"habilitado=False modo=DISABLED"
)
return
cameras_validas = {"CAM_A", "CAM_B", "CAM_C"}
if master not in cameras_validas:
raise ValueError(
f"frame_sync_master inválido: {self.frame_sync_master}. "
f"Esperado: CAM_A, CAM_B ou CAM_C."
)
if cam_id_normalizado not in cameras_validas:
print(
f"[OAK FSYNC] Câmera ignorada: "
f"cam_id={cam_id_normalizado}"
)
return
if cam_id_normalizado == master:
modo = dai.CameraControl.FrameSyncMode.OUTPUT
nome_modo = "OUTPUT"
else:
modo = dai.CameraControl.FrameSyncMode.INPUT
nome_modo = "INPUT"
cam.initialControl.setFrameSyncMode(modo)
print(
f"[OAK FSYNC] "
f"cam={cam_id_normalizado} "
f"master={master} "
f"habilitado=True "
f"modo={nome_modo}"
)
def _create_color_camera_multispec(self, socket):
cam = self.pipeline.create(dai.node.ColorCamera)
cam.setBoardSocket(socket)
@ -591,6 +641,7 @@ class OakFcc3Manager:
raise RuntimeError(f"Role não suportada no MULTISPEC: cam_id={cam_id}, role={role}")
self.apply_initial_camera_controls_to_node(cam, cam_id)
self._aplicar_frame_sync(cam, cam_id)
xin_ctrl = self.pipeline.create(dai.node.XLinkIn)
xin_ctrl.setStreamName(f"{cam_id}_ctrl")
@ -871,6 +922,7 @@ class OakFcc3Manager:
)
self.apply_initial_camera_controls_to_node(cam, socket_name)
self._aplicar_frame_sync(cam, socket_name)
xin_ctrl = self.pipeline.create(dai.node.XLinkIn)
xin_ctrl.setStreamName(f"{socket_name}_ctrl")
@ -1241,6 +1293,41 @@ class OakFcc3Manager:
f"Tolerância atual={self.sync_tolerance_ms} ms."
)
def _encontrar_melhor_tripleta(self, required_cam_ids):
listas = [
list(self.buffers[cam_id])
for cam_id in required_cam_ids
]
melhor_selecao = None
melhor_score = None
for combinacao in product(*listas):
timestamps = [
item["timestamp"]
for item in combinacao
]
menor_ts = min(timestamps)
maior_ts = max(timestamps)
spread_ms = (maior_ts - menor_ts) * 1000.0
# Primeiro prioriza menor dispersão.
# Em empate, prefere o pacote mais recente.
score = (
spread_ms,
-menor_ts,
)
if melhor_score is None or score < melhor_score:
melhor_score = score
melhor_selecao = {
cam_id: item
for cam_id, item in zip(required_cam_ids, combinacao)
}
return melhor_selecao
def _extract_frame_controls(self, msg):
controls = {
@ -1330,9 +1417,19 @@ class OakFcc3Manager:
t0_ts = self._cap_now_ms()
try:
ts = msg.getTimestamp().total_seconds()
ts_start = msg.getTimestampDevice(
dai.CameraExposureOffset.START
).total_seconds()
ts_end = msg.getTimestampDevice(
dai.CameraExposureOffset.END
).total_seconds()
except Exception:
ts = time.time()
ts_start = msg.getTimestamp().total_seconds()
ts_end = ts_start
ts = ts_start
if perf is not None:
perf["drain_get_timestamp_ms"] += self._cap_now_ms() - t0_ts
@ -1395,7 +1492,9 @@ class OakFcc3Manager:
self.buffers[cam_id].append({
"frame": frame,
"timestamp": ts,
"timestamp": ts_start,
"timestamp_start": ts_start,
"timestamp_end": ts_end,
"controls": frame_controls,
})
@ -1420,34 +1519,25 @@ class OakFcc3Manager:
def _try_get_synced_packet(self, perf=None):
required_cam_ids = self._get_required_cam_ids()
if not required_cam_ids:
if perf is not None:
perf["wait_reason"] = "no_required_cameras"
return None
# Todas as câmeras precisam ter pelo menos um frame disponível.
for cam_id in required_cam_ids:
if cam_id not in self.buffers or len(self.buffers[cam_id]) == 0:
if perf is not None:
perf["wait_reason"] = f"empty_buffer:{cam_id}"
return None
ref_cam_id = min(required_cam_ids, key=lambda cid: len(self.buffers[cid]))
ref_item = self.buffers[ref_cam_id][0]
ref_ts = ref_item["timestamp"]
# Procura a melhor combinação entre todos os frames disponíveis.
selected = self._encontrar_melhor_tripleta(required_cam_ids)
selected = {}
for cam_id in required_cam_ids:
best_item = None
best_dt = None
for item in self.buffers[cam_id]:
dt = abs(item["timestamp"] - ref_ts)
if best_dt is None or dt < best_dt:
best_dt = dt
best_item = item
if best_item is None:
if perf is not None:
perf["wait_reason"] = f"no_best_item:{cam_id}"
return None
selected[cam_id] = best_item
if not selected or len(selected) != len(required_cam_ids):
if perf is not None:
perf["wait_reason"] = "no_valid_selection"
return None
timestamps = {
cam_id: item["timestamp"]
@ -1459,44 +1549,80 @@ class OakFcc3Manager:
for cam_id, item in selected.items()
}
ts_values = list(timestamps.values())
sync_dt_ms = (
(max(ts_values) - min(ts_values)) * 1000.0
if len(ts_values) >= 2
else 0.0
)
sync_ok = sync_dt_ms <= self.sync_tolerance_ms
if perf is not None:
perf["selected_ts_by_cam"] = {cam_id: float(ts) for cam_id, ts in timestamps.items()}
perf["selected_ts_by_cam"] = {
cam_id: float(ts)
for cam_id, ts in timestamps.items()
}
perf["selected_seq_by_cam"] = {
cam_id: item.get("controls", {}).get("sequence_num")
for cam_id, item in selected.items()
}
ts_values = list(timestamps.values())
sync_dt_ms = (max(ts_values) - min(ts_values)) * 1000.0 if len(ts_values) >= 2 else 0.0
sync_ok = sync_dt_ms <= self.sync_tolerance_ms
perf["sync_dt_ms"] = float(sync_dt_ms)
perf["sync_ok"] = bool(sync_ok)
modo_sync = str(self.sync_mode).strip().lower()
# No modo estrito, nunca entrega uma tripleta fora da tolerância.
if not sync_ok and modo_sync == "strict":
#Remove o frame globalmente mais antigo entre as cabeças
#dos buffers. Frames futuros somente estarão mais distantes
#desse frame, então ele não conseguirá formar uma combinação
#melhor posteriormente.
oldest_cam_id = min(
required_cam_ids,
key=lambda cam_id: self.buffers[cam_id][0]["timestamp"]
)
dropped_item = self.buffers[oldest_cam_id].popleft()
if not sync_ok and self.sync_mode == "strict":
oldest_cam_id = min(timestamps, key=timestamps.get)
if len(self.buffers[oldest_cam_id]) > 0:
self.buffers[oldest_cam_id].popleft()
if perf is not None:
perf["wait_reason"] = f"strict_drop_oldest:{oldest_cam_id}"
perf["sync_dt_ms"] = float(sync_dt_ms)
perf["sync_ok"] = False
perf["dropped_timestamp"] = float(dropped_item["timestamp"])
return None
# Em best/best_effort, entrega a melhor combinação disponível,
# mesmo quando estiver fora da tolerância.
frames = {
cam_id: item["frame"]
for cam_id, item in selected.items()
}
# Consome todos os frames anteriores e o próprio frame selecionado.
for cam_id, used_item in selected.items():
while len(self.buffers[cam_id]) > 0:
while self.buffers[cam_id]:
item = self.buffers[cam_id].popleft()
if item is used_item:
break
if perf is not None:
perf["wait_reason"] = "synced_selected"
perf["sync_dt_ms"] = float(sync_dt_ms)
perf["sync_ok"] = bool(sync_ok)
perf["wait_reason"] = (
"synced_selected"
if sync_ok
else "best_effort_selected_outside_tolerance"
)
return frames, timestamps, sync_dt_ms, sync_ok, frame_controls
return (
frames,
timestamps,
sync_dt_ms,
sync_ok,
frame_controls,
)
def _get_available_cam_ids_ordered(self):
role_order = ["rgb", "re", "nir"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,761 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
camera_startup_profile_production.py
====================================
Gerador/auditor FINAL de produção para o bloco `camera_settings`
do módulo multiespectral.
IMPORTANTE
----------
Isto NÃO é uma calibração óptica/radiométrica nova.
O script antigo "Sensor Calibration Tool" misturava:
- camera_settings;
- ajustes manuais AE/AWB/exposure/gain;
- ROIs de cana/erva/solo;
- guidance heurístico;
- rgb_calibration manual;
- snapshots/offline samples.
Na arquitetura final de produto essas responsabilidades já foram separadas.
Esta ferramenta preserva SOMENTE o que ainda é necessário ao runtime:
camera_settings
e força explicitamente:
rgb_calibration.enabled = false
gains = 1,1,1
para impedir que ganhos RGB históricos/experimentais vazem para o tensor.
Dependência
-----------
A ferramenta lê o artefato homologado da etapa radiométrica:
calibration/radiometry_calibration_v5.json
e reaproveita:
radiometric_normalization.reference_controls
como valores de fallback do startup.
Política normal de produto
--------------------------
RGB:
AE = ON
AWB = ON
exposure_time_us = referência radiométrica (fallback)
analogue_gain = ISO_ref / 100
colour_gains = [1.0, 1.0]
RE:
AE = ON
AWB = OFF
exposure_time_us = referência radiométrica (fallback)
analogue_gain = ISO_ref / 100
NIR:
AE = ON
AWB = OFF
exposure_time_us = referência radiométrica (fallback)
analogue_gain = ISO_ref / 100
Por que manter os valores manuais se AE está ON?
-------------------------------------------------
No manager atual, com AE ligado, exposure/gain do JSON não são forçados
fisicamente. Eles ficam armazenados como estado/fallback para uma eventual
transição futura para manual.
Assim:
- o campo continua adaptando exposição automaticamente;
- a normalização radiométrica usa os controles reais de cada frame;
- se AE for desabilitado em algum fluxo futuro, o fallback já é coerente
com a calibração radiométrica do módulo.
Saída
-----
calibration/camera_startup_profile_v3.json
Contém:
module_params_fragment.camera_settings
module_params_fragment.rgb_calibration
O assembler final do module_params continua sendo a autoridade de merge.
Fail-closed
-----------
- exige CAM_A = OV9782 ou AR0234;
- exige CAM_B/C = OV9282;
- exige artefato radiométrico promovido;
- exige hardware_signature compatível;
- exige reference_controls válidos para rgb/re/nir;
- não aceita ISO inválido;
- não ativa rgb_calibration;
- preserva backup do ativo anterior.
Uso
---
python camera_startup_profile_production.py
Com rastreabilidade:
python camera_startup_profile_production.py ^
--module-id MOD_MS_001 ^
--operator Diego
Somente auditoria, sem salvar:
python camera_startup_profile_production.py --check-only
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, List
import depthai as dai
SCHEMA = "multispec_camera_startup_profile_v3"
ISO_BASE = 100.0
ROLES = ("rgb", "re", "nir")
PRODUCT_TOPOLOGY = {
"rgb": {
"socket": "CAM_A",
"allowed_sensors": ("OV9782", "AR0234"),
},
"re": {
"socket": "CAM_B",
"allowed_sensors": ("OV9282",),
},
"nir": {
"socket": "CAM_C",
"allowed_sensors": ("OV9282",),
},
}
SENSOR_NATIVE_SIZE = {
"OV9782": (1280, 800),
"AR0234": (1920, 1200),
"OV9282": (1280, 800),
}
def now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def stamp() -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
def ensure_dir(path: str | Path):
Path(path).mkdir(parents=True, exist_ok=True)
def load_json(path: str | Path) -> dict:
p = Path(path)
if not p.is_file():
raise FileNotFoundError(f"Arquivo não encontrado: {p}")
with p.open("r", encoding="utf-8") as f:
return json.load(f)
def save_json_atomic(path: str | Path, data: dict):
p = Path(path)
ensure_dir(p.parent)
tmp = p.with_suffix(p.suffix + ".tmp")
with tmp.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, p)
def socket_name(socket) -> str:
name = getattr(socket, "name", None)
if name:
return str(name)
text = str(socket)
for candidate in ("CAM_A", "CAM_B", "CAM_C", "CAM_D"):
if candidate in text:
return candidate
return text
def supported_types(feature) -> List[str]:
return [
str(v).upper()
for v in (getattr(feature, "supportedTypes", []) or [])
]
def feature_is_color(feature) -> bool:
types = supported_types(feature)
sensor = str(getattr(feature, "sensorName", "") or "").upper()
if any("COLOR" in x for x in types):
return True
if any("MONO" in x for x in types):
return False
return sensor in {"OV9782", "AR0234"}
def feature_is_mono(feature) -> bool:
types = supported_types(feature)
if any("MONO" in x for x in types):
return True
if any("COLOR" in x for x in types):
return False
return not feature_is_color(feature)
@dataclass
class CameraInfo:
role: str
socket: str
sensor: str
width: int
height: int
is_color: bool
is_mono: bool
def discover_hardware(mx_id: Optional[str]):
info = dai.DeviceInfo(mx_id) if mx_id else None
ctx = dai.Device(info) if info is not None else dai.Device()
with ctx as device:
actual_mx = None
for method_name in ("getMxId", "getDeviceId"):
fn = getattr(device, method_name, None)
if callable(fn):
try:
value = fn()
if value:
actual_mx = str(value)
break
except Exception:
pass
usb_speed = None
try:
usb_speed = str(device.getUsbSpeed())
except Exception:
pass
rows = []
for f in device.getConnectedCameraFeatures():
rows.append({
"socket": socket_name(f.socket),
"sensor": str(getattr(f, "sensorName", "") or "").upper(),
"width": int(getattr(f, "width", 0) or 0),
"height": int(getattr(f, "height", 0) or 0),
"is_color": feature_is_color(f),
"is_mono": feature_is_mono(f),
"supported_types": supported_types(f),
})
return rows, actual_mx, usb_speed
def validate_and_resolve_hardware(rows: list[dict]) -> Dict[str, CameraInfo]:
by_socket = {row["socket"]: row for row in rows}
resolved = {}
errors = []
for role in ROLES:
contract = PRODUCT_TOPOLOGY[role]
sock = contract["socket"]
row = by_socket.get(sock)
if row is None:
errors.append(f"{role.upper()}: {sock} ausente")
continue
sensor = row["sensor"]
if sensor not in contract["allowed_sensors"]:
errors.append(
f"{role.upper()}: {sock} sensor={sensor}, "
f"permitidos={contract['allowed_sensors']}"
)
continue
expected_w, expected_h = SENSOR_NATIVE_SIZE[sensor]
if row["width"] and row["height"]:
if (row["width"], row["height"]) != (expected_w, expected_h):
errors.append(
f"{role.upper()}: {sensor} anunciou "
f"{row['width']}x{row['height']}, esperado "
f"{expected_w}x{expected_h}"
)
if role == "rgb" and not row["is_color"]:
errors.append(f"RGB: {sock}/{sensor} não anunciado COLOR")
if role in ("re", "nir") and not row["is_mono"]:
errors.append(f"{role.upper()}: {sock}/{sensor} não anunciado MONO")
resolved[role] = CameraInfo(
role=role,
socket=sock,
sensor=sensor,
width=expected_w,
height=expected_h,
is_color=bool(row["is_color"]),
is_mono=bool(row["is_mono"]),
)
if errors:
raise RuntimeError(
"Topologia de produto inválida:\n - "
+ "\n - ".join(errors)
)
return resolved
def hardware_signature_from_resolved(resolved: Dict[str, CameraInfo]) -> dict:
return {
role: {
"socket": resolved[role].socket,
"sensor": resolved[role].sensor,
"size": [resolved[role].width, resolved[role].height],
}
for role in ROLES
}
def normalize_signature(sig: dict) -> dict:
"""
Aceita assinatura no formato dos calibradores de produção.
Retorna apenas role/socket/sensor/size para comparação robusta.
"""
out = {}
for role in ROLES:
item = (sig or {}).get(role, {}) or {}
socket = (
item.get("socket")
or item.get("socket_name")
)
sensor = (
item.get("sensor")
or item.get("sensor_name")
)
size = item.get("size")
if size is None:
width = item.get("width")
height = item.get("height")
if width is not None and height is not None:
size = [int(width), int(height)]
if socket is None or sensor is None or size is None:
raise RuntimeError(
f"hardware_signature incompleta para {role}: {item}"
)
out[role] = {
"socket": str(socket),
"sensor": str(sensor).upper(),
"size": [int(size[0]), int(size[1])],
}
return out
def extract_radiometric_fragment(data: dict) -> dict:
"""
O artefato radiométrico de produção guarda:
module_params_fragment.radiometric_normalization
"""
fragment = data.get("module_params_fragment", {}) or {}
rn = fragment.get("radiometric_normalization")
if not isinstance(rn, dict):
raise RuntimeError(
"Artefato radiométrico sem "
"module_params_fragment.radiometric_normalization"
)
if not rn.get("enabled", False):
raise RuntimeError(
"radiometric_normalization do artefato não está enabled=true"
)
return rn
def validate_radiometry_artifact(
data: dict,
current_signature: dict,
):
schema = str(data.get("schema", ""))
if schema != "multispec_radiometric_calibration_v5":
raise RuntimeError(
f"Schema radiométrico inesperado: {schema!r}. "
"Esperado 'multispec_radiometric_calibration_v5'."
)
status = str(data.get("status", "")).lower()
if status not in ("good", "warning"):
raise RuntimeError(
f"Radiometria não homologada para consumo: status={status!r}"
)
if not bool(data.get("promoted", False)):
raise RuntimeError(
"Artefato radiométrico não foi promovido."
)
artifact_sig = normalize_signature(
data.get("hardware_signature", {})
)
if artifact_sig != current_signature:
raise RuntimeError(
"Hardware conectado não corresponde ao hardware da radiometria.\n"
f"Atual : {current_signature}\n"
f"Rad : {artifact_sig}"
)
rn = extract_radiometric_fragment(data)
refs = rn.get("reference_controls", {}) or {}
for role in ROLES:
ref = refs.get(role)
if not isinstance(ref, dict):
raise RuntimeError(
f"reference_controls ausente para {role}"
)
exp = ref.get("exposure_time_us")
iso = ref.get("sensitivity_iso")
if exp is None or float(exp) <= 0:
raise RuntimeError(
f"reference exposure inválida para {role}: {exp}"
)
if iso is None or float(iso) < 50:
raise RuntimeError(
f"reference ISO inválido para {role}: {iso}"
)
return rn
def iso_to_analogue_gain(iso: float) -> float:
"""
O manager atual representa analogue_gain aproximadamente como ISO/100.
"""
gain = float(iso) / ISO_BASE
return max(1.0, gain)
def build_camera_settings(reference_controls: dict) -> dict:
settings = {}
for role in ROLES:
ref = reference_controls[role]
exp = int(round(float(ref["exposure_time_us"])))
iso = float(ref["sensitivity_iso"])
gain = iso_to_analogue_gain(iso)
if role == "rgb":
settings[role] = {
"ae_enable": True,
"awb_enable": True,
"exposure_time_us": exp,
"analogue_gain": gain,
"colour_gains": [1.0, 1.0],
}
else:
settings[role] = {
"ae_enable": True,
"awb_enable": False,
"exposure_time_us": exp,
"analogue_gain": gain,
"colour_gains": None,
}
return settings
def build_rgb_calibration_policy() -> dict:
"""
Produto final mantém correção multiplicativa RGB desligada.
Se um dia ela for homologada, deve existir um calibrador separado e
dataset/runtime precisam usar o mesmo contrato.
"""
return {
"enabled": False,
"gains": {
"R": 1.0,
"G": 1.0,
"B": 1.0,
},
"policy": "disabled_product_default",
"reason": (
"No validated RGB tensor-gain calibration is currently active. "
"Historical manual gains are intentionally discarded."
),
}
def compare_existing_module_params(
module_params_path: Optional[str],
generated_fragment: dict,
) -> dict:
if not module_params_path:
return {
"available": False,
"path": None,
}
p = Path(module_params_path)
if not p.is_file():
return {
"available": False,
"path": str(p),
}
current = load_json(p)
current_camera = current.get("camera_settings")
current_rgb = current.get("rgb_calibration")
generated_camera = generated_fragment["camera_settings"]
generated_rgb = generated_fragment["rgb_calibration"]
return {
"available": True,
"path": str(p),
"schema": current.get("schema"),
"camera_settings_equal": current_camera == generated_camera,
"rgb_calibration_equal": current_rgb == generated_rgb,
"current_camera_settings": current_camera,
"generated_camera_settings": generated_camera,
"current_rgb_calibration": current_rgb,
"generated_rgb_calibration": generated_rgb,
}
def main():
parser = argparse.ArgumentParser(
description=(
"Gerador/auditor de produção para camera_settings e "
"política rgb_calibration do módulo multiespectral."
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--mx-id",
default=None,
help="MX ID da OAK. Vazio usa o primeiro dispositivo disponível.",
)
parser.add_argument(
"--radiometry-json",
default="calibration/radiometry_calibration_v5.json",
help="Artefato radiométrico homologado.",
)
parser.add_argument(
"--out-json",
default="calibration/camera_startup_profile_v3.json",
help="Artefato ativo desta etapa.",
)
parser.add_argument(
"--compare-module-params",
default="calibration/module_params.json",
help="Opcional: audita diferenças contra module_params existente.",
)
parser.add_argument(
"--module-id",
default="",
)
parser.add_argument(
"--operator",
default="",
)
parser.add_argument(
"--notes",
default="",
)
parser.add_argument(
"--check-only",
action="store_true",
help="Valida e imprime o fragmento, mas não salva.",
)
args = parser.parse_args()
rows, actual_mx, usb_speed = discover_hardware(args.mx_id)
resolved = validate_and_resolve_hardware(rows)
current_signature = hardware_signature_from_resolved(resolved)
radiometry = load_json(args.radiometry_json)
rn = validate_radiometry_artifact(
radiometry,
current_signature,
)
reference_controls = rn["reference_controls"]
camera_settings = build_camera_settings(reference_controls)
rgb_calibration = build_rgb_calibration_policy()
module_params_fragment = {
"camera_settings": camera_settings,
"rgb_calibration": {
"enabled": False,
"gains": {
"R": 1.0,
"G": 1.0,
"B": 1.0,
},
},
}
audit = compare_existing_module_params(
args.compare_module_params,
module_params_fragment,
)
payload = {
"schema": SCHEMA,
"created_at": now_str(),
"status": "good",
"promoted": not args.check_only,
"runtime_effect": (
"startup_camera_policy_and_explicit_rgb_gain_disable"
),
"device_mx_id": actual_mx,
"usb_speed": usb_speed,
"hardware_inventory": rows,
"hardware_signature": current_signature,
"source_radiometry": {
"path": str(args.radiometry_json),
"schema": radiometry.get("schema"),
"session_id": radiometry.get("session_id"),
"status": radiometry.get("status"),
"promoted": radiometry.get("promoted"),
"reference_controls": reference_controls,
},
"startup_policy": {
"mode": "ae_with_calibrated_manual_fallback",
"rgb_awb": "auto_for_isp_preview_only",
"spectral_awb": "off_not_applicable",
"manual_fallback_source": (
"radiometric_normalization.reference_controls"
),
"note": (
"With AE enabled, manager stores exposure/gain as fallback "
"and does not force them manually at startup."
),
},
"rgb_calibration_policy": rgb_calibration,
"module_params_fragment": module_params_fragment,
"existing_module_params_audit": audit,
"traceability": {
"module_id": args.module_id or None,
"operator": args.operator or None,
},
"notes": args.notes or "",
}
print("=" * 82)
print("CAMERA STARTUP PROFILE - PRODUCTION")
print(f"MX ID : {actual_mx}")
print(f"USB : {usb_speed}")
print("-" * 82)
for role in ROLES:
c = resolved[role]
s = camera_settings[role]
print(
f"{c.socket} -> {role.upper():3s} | "
f"{c.sensor:8s} | {c.width}x{c.height} | "
f"AE={s['ae_enable']} AWB={s['awb_enable']} | "
f"fallback={s['exposure_time_us']}us gain={s['analogue_gain']:.3f}"
)
print("-" * 82)
print("rgb_calibration: DISABLED | gains = 1.0 / 1.0 / 1.0")
if audit.get("available"):
print(
"module_params audit: "
f"camera_settings_equal={audit['camera_settings_equal']} | "
f"rgb_calibration_equal={audit['rgb_calibration_equal']}"
)
else:
print("module_params audit: arquivo não disponível")
if args.check_only:
print("[CHECK-ONLY] Nenhum arquivo alterado.")
print("=" * 82)
return
out = Path(args.out_json)
ensure_dir(out.parent)
if out.exists():
backup = out.with_name(
out.stem
+ f".backup_{stamp()}"
+ out.suffix
)
shutil.copy2(out, backup)
save_json_atomic(out, payload)
print(f"[PASS] Artefato salvo: {out}")
print("[ASSEMBLER] Consumir module_params_fragment deste JSON.")
print("=" * 82)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,654 +0,0 @@
import json
import argparse
import os
from copy import deepcopy
from datetime import datetime
# ============================================================
# Helpers
# ============================================================
def now_str():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def load_json(path, required=True):
if not path or not os.path.isfile(path):
if required:
raise FileNotFoundError(f"Arquivo não encontrado: {path}")
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path, data):
out_dir = os.path.dirname(os.path.abspath(path))
if out_dir:
os.makedirs(out_dir, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
def rel_or_abs(path):
"""
Mantém o caminho como veio, mas normaliza separadores.
Isso evita quebrar projetos Windows/Linux e deixa o module_params legível.
"""
if path is None:
return None
return str(path).replace("\\", "/")
def deep_merge(base, update, *, skip_none=True):
"""
Merge recursivo seguro.
- dict + dict: combina recursivamente.
- listas/escalares: valor novo substitui o antigo.
- None: por padrão NÃO apaga valor antigo, para evitar perder calibração quando
um arquivo fonte não conhece determinada chave.
"""
if not isinstance(base, dict):
base = {}
out = deepcopy(base)
if not isinstance(update, dict):
return out
for key, value in update.items():
if value is None and skip_none:
continue
if isinstance(value, dict) and isinstance(out.get(key), dict):
out[key] = deep_merge(out[key], value, skip_none=skip_none)
else:
out[key] = deepcopy(value)
return out
def first_dict(*values):
for value in values:
if isinstance(value, dict):
return value
return None
# ============================================================
# Defaults coerentes com RawProcessorCore + module_params atual
# ============================================================
DEFAULT_RGB_PROCESSING = {
"mode": "bayer_planes",
}
DEFAULT_PATCH_NORMALIZATION = {
"enabled": True,
"apply_when_metering_mode": "reference_patches",
"apply_stage": "after_fusion",
"method": "gray_scale_with_white_guard",
"space": "multispec_tensor",
"targets_by_patch_channel": {
"black": {
"R": 0.06,
"G": 0.06,
"B": 0.06,
"RE": 0.06,
"NIR": 0.06,
},
"gray": {
"R": 0.34,
"G": 0.34,
"B": 0.34,
"RE": 0.24,
"NIR": 0.30,
},
"white": {
"R": 0.78,
"G": 0.78,
"B": 0.78,
"RE": 0.78,
"NIR": 0.78,
},
},
"white_guard_max": 0.92,
"white_guard_max_by_channel": {
"R": 0.92,
"G": 0.92,
"B": 0.92,
"RE": 0.88,
"NIR": 0.88,
},
"scale_min": 0.35,
"scale_max": 2.5,
"clip_output": True,
"require_valid_gray": True,
"use_black_for_offset": False,
"save_patch_stats": True,
"rgb_saturation_guard_enabled": True,
"rgb_saturation_guard_mode": "fade_strength",
"rgb_saturation_soft_start": 0.88,
"rgb_saturation_hard": 0.97,
"rgb_saturation_threshold": 0.97,
}
DEFAULT_FLATFIELD_RUNTIME = {
"strength": 0.35,
"strength_by_channel": {
"R": 0.9,
"G": 0.9,
"B": 0.9,
"RE": 0.25,
"NIR": 0.25,
},
"gain_min_runtime": 0.75,
"gain_max_runtime": 1.35,
"runtime_smooth_ksize": 81,
"saturation_guard_enabled": True,
"saturation_guard_mode": "fade_strength",
"saturation_guard_threshold": 0.97,
"saturation_guard_soft_start": 0.88,
"saturation_guard_hard": 0.97,
}
DEFAULT_RADIOMETRIC_NORMALIZATION = {
"enabled": False,
"method": "exposure_gain_reference",
"apply_stage": "after_dark_before_flat_gain",
"reference_controls": {
"rgb": {"exposure_time_us": 3000, "analogue_gain": 1.0},
"re": {"exposure_time_us": 7000, "analogue_gain": 1.0},
"nir": {"exposure_time_us": 7000, "analogue_gain": 1.0},
},
"clip_output": True,
}
DEFAULT_RADIOMETRIC_CONFIG = {
"enabled": True,
"interval_s": 0.25,
"verbose": True,
"metering_mode": "reference_patches",
"spectral_control_mode": "shared",
"control_metric": "p50",
"target_value": 0.5,
"deadband": 0.055,
"p95_limit": 0.975,
"saturation_limit_pct": 5.0,
"alpha": 0.18,
"exp_step_gain": 0.55,
"prefer_exposure": True,
"exp_min_us": 100,
"exp_max_us": 80000,
"gain_min": 1.0,
"gain_max": 4.0,
"exp_apply_threshold_us": 15,
"gain_apply_threshold": 0.05,
"apply_same_spectral_to_both": True,
"spectral_roles": ["re", "nir"],
"dark_limit_pct": 35.0,
"control_strategy": "ratio",
"ratio_alpha": 0.42,
"ratio_min": 0.72,
"ratio_max": 1.38,
"reduce_fast_factor": 0.8,
"factor_min": 0.62,
"factor_max": 1.42,
"gain_return_enabled": True,
"gain_reduce_on_saturation": True,
"gain_increase_required_cycles": 3,
"gain_decrease_required_cycles": 1,
"gain_step_up": 0.3,
"gain_step_down": 0.5,
"gain_hard_reset_on_saturation": False,
"exp_high_ratio_for_gain": 0.95,
"exp_low_ratio_for_gain_return": 0.75,
"role_limits": {
"rgb": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 2.0},
"re": {"exp_min_us": 100, "exp_max_us": 3500, "gain_min": 1.0, "gain_max": 2.0},
"nir": {"exp_min_us": 100, "exp_max_us": 3500, "gain_min": 1.0, "gain_max": 2.0},
},
"ready_required_cycles": 3,
"patch_control_mode": "gray_primary",
"patch_require_order": True,
"patch_min_separation": 0.08,
"patch_white_sat_limit_pct": 5.0,
"patch_white_p95_limit": 0.985,
"patch_black_dark_limit_pct": 80.0,
"patch_black_max_p50": 0.2,
"patch_gray_min_p50": 0.08,
"patch_gray_max_p50": 0.85,
"patch_roi_contract": "multi_roi_by_role_v1",
"patch_roi_reduce_method": "median_valid_rois",
"patch_roi_outlier_reject": True,
"patch_roi_max_p50_delta": 0.12,
"global_saturation_guard_enabled": True,
"global_guard_roi_pct": {"x0": 0.05, "y0": 0.05, "x1": 0.95, "y1": 0.76},
"global_guard_sat_threshold": 0.985,
"global_guard_near_sat_threshold": 0.94,
"global_guard_sat_pct_soft": 0.50,
"global_guard_sat_pct_hard": 1.5,
"global_guard_sat_pct_extreme": 5.0,
"global_guard_blob_pct_soft": 0.20,
"global_guard_blob_pct_hard": 0.80,
"global_guard_blob_pct_extreme": 2.2,
"global_guard_min_blob_px": 48,
"global_guard_downsample_max_side": 320,
"global_guard_reduce_factor_soft": 0.96,
"global_guard_reduce_factor_hard": 0.82,
"global_guard_reduce_factor_extreme": 0.62,
"sun_guard_enabled": True,
"sun_guard_p99_threshold": 0.96,
"sun_guard_near_sat_pct_threshold": 2.0,
"sun_guard_freeze_increase_cycles": 1,
"sun_guard_allow_decrease": True,
"guard_force_apply_enabled": True,
"guard_force_apply_soft": False,
"guard_force_apply_hard": True,
"guard_force_apply_extreme": True,
"guard_force_apply_on_patch_saturation": True,
"guard_freeze_cycles_soft": 1,
"guard_freeze_cycles_hard": 2,
"guard_freeze_cycles_extreme": 3,
"guard_reapply_min_exp_on_emergency": True,
"guard_min_exp_margin_us": 80,
"patch_two_roi_soften_risk": True,
"patch_two_roi_white_risk_percentile": 75,
"patch_two_roi_other_risk_percentile": 50,
"patch_white_single_roi_saturation_reject": True,
"patch_white_roi_reject_sat_pct": 5.0,
"patch_white_roi_reject_p95": 0.995,
}
def default_module_template():
return {
"schema": "multispec_module_params_v3",
"saved_at": now_str(),
"frame_type": "RAW_BRUTO",
"capture_mode_requested": "AUTO",
"capture_mode_effective": "AUTO",
"raw_policy": "allow_single",
"sensor_width": 1280,
"sensor_height": 800,
"bayer_pattern": "BGGR",
"rgb_processing": deepcopy(DEFAULT_RGB_PROCESSING),
"camera_settings": {},
"fusion_config": {
"alignment_mode": "manual_affine",
"baseline_mm": 75.0,
"reference_camera": "rgb",
"manual_offsets": {
"re": {"dx": 0, "dy": 0, "theta_deg": 0.0},
"nir": {"dx": 0, "dy": 0, "theta_deg": 0.0},
},
"homographies": {
"re_to_rgb": None,
"nir_to_rgb": None,
},
"crop_valid_common": True,
"resize_after_crop": True,
"target_size": None,
},
"radiometric_config": deepcopy(DEFAULT_RADIOMETRIC_CONFIG),
"radiometric_normalization": deepcopy(DEFAULT_RADIOMETRIC_NORMALIZATION),
"patch_normalization": deepcopy(DEFAULT_PATCH_NORMALIZATION),
"rgb_calibration": {
"enabled": False,
"gains": {"R": 1.0, "G": 1.0, "B": 1.0},
},
"flatfield_config": deep_merge(
{
"enabled": False,
"reason": "flatfield não informado ou arquivo inexistente",
"subtract_dark": True,
"apply_before_fusion": True,
"apply_after_decode": True,
"apply_space": "native_camera_space",
"map_type": "gain",
"channels": ["R", "G", "B", "RE", "NIR"],
"channel_maps": {},
"clip_output": True,
},
DEFAULT_FLATFIELD_RUNTIME,
),
}
# ============================================================
# Builders / normalizers
# ============================================================
def build_flatfield_config(flatfield_json_path, flatfield_data, previous_flatfield_config=None):
"""
Espera o JSON gerado pelo flatfield_calibration_tool_v2.py.
Importante: usa previous_flatfield_config como base para preservar knobs runtime
que não existem no arquivo de calibração do flat-field, como strength,
gain_min_runtime, runtime_smooth_ksize e saturation_guard_*.
"""
base = deep_merge(
deep_merge({}, previous_flatfield_config or {}),
DEFAULT_FLATFIELD_RUNTIME,
)
if not isinstance(flatfield_data, dict):
return deep_merge(base, {
"enabled": False,
"reason": "flatfield_json ausente ou inválido",
})
outputs = flatfield_data.get("outputs", {}) or {}
maps = flatfield_data.get("maps", {}) or {}
npz_path = outputs.get("npz")
if not npz_path:
base_name, _ = os.path.splitext(flatfield_json_path)
npz_path = base_name + ".npz"
channels = flatfield_data.get("channels") or base.get("channels") or ["R", "G", "B", "RE", "NIR"]
channel_maps = {}
previous_channel_maps = base.get("channel_maps", {}) or {}
for ch in channels:
m = maps.get(ch, {}) or {}
prev = previous_channel_maps.get(ch, {}) or {}
channel_maps[ch] = deep_merge(prev, {
"gain_key": m.get("gain_key", f"gain_{ch}"),
"flat_norm_key": m.get("flat_norm_key", f"flat_norm_{ch}"),
"white_median_key": m.get("white_median_key", f"white_median_{ch}"),
"dark_median_key": m.get("dark_median_key", f"dark_median_{ch}"),
"shape": m.get("shape"),
"gain_min": m.get("gain_min"),
"gain_max": m.get("gain_max"),
"gain_mean": m.get("gain_mean"),
"gain_std": m.get("gain_std"),
})
generated = {
"enabled": True,
"subtract_dark": True,
"schema": flatfield_data.get("schema", "multispec_flatfield_v1"),
"created_at": flatfield_data.get("created_at"),
"json_file": rel_or_abs(flatfield_json_path),
"npz_file": rel_or_abs(npz_path),
"apply_before_fusion": True,
"apply_after_decode": True,
"apply_space": "native_camera_space",
"map_type": "gain",
"formula": "channel_corrected = max(channel_linear - dark, 0) * gain_map",
"channels": channels,
"channel_maps": channel_maps,
"exp_gain_correct_during_flat_capture": bool(flatfield_data.get("exp_gain_correct", False)),
"smooth_ksize": flatfield_data.get("smooth_ksize"),
"min_gain": flatfield_data.get("min_gain"),
"max_gain": flatfield_data.get("max_gain"),
"notes": flatfield_data.get("notes", ""),
}
return deep_merge(base, generated)
def pick_radiometric_config(radiometric_data: dict, selected_profile: str | None = None):
if not isinstance(radiometric_data, dict):
return None
root_cfg = radiometric_data.get("radiometric_config")
if isinstance(root_cfg, dict):
return root_cfg
active_profile = radiometric_data.get("active_profile")
if active_profile in ("global_scene_mode", "three_reference_patches_mode"):
cfg = radiometric_data.get(active_profile, {}).get("radiometric_config")
if isinstance(cfg, dict):
return cfg
if selected_profile:
cfg = radiometric_data.get(selected_profile, {}).get("radiometric_config")
if isinstance(cfg, dict):
return cfg
return None
def normalize_patch_normalization_contract(base_patch_config, incoming_patch_config=None):
"""
Garante o contrato atual do RawProcessorCore.
- Sempre tem targets_by_patch_channel.
- Preserva white_guard_max_by_channel.
- Preserva rgb_saturation_guard_*.
- Remove a chave legada targets, porque ela não é usada pelo core atual.
"""
cfg = deep_merge(DEFAULT_PATCH_NORMALIZATION, base_patch_config or {})
cfg = deep_merge(cfg, incoming_patch_config or {})
legacy_targets = cfg.pop("targets", None)
if isinstance(legacy_targets, dict) and "targets_by_patch_channel" not in cfg:
# Fallback conservador. Na prática, com DEFAULT_PATCH_NORMALIZATION acima,
# normalmente não entra aqui. Mantido só para arquivos muito antigos.
t = deepcopy(DEFAULT_PATCH_NORMALIZATION["targets_by_patch_channel"])
for patch_type in ("black", "gray", "white"):
if patch_type in legacy_targets:
scalar = legacy_targets.get(patch_type)
try:
scalar = float(scalar)
for ch in ("R", "G", "B", "RE", "NIR"):
t[patch_type][ch] = scalar
except Exception:
pass
cfg["targets_by_patch_channel"] = t
cfg = deep_merge(DEFAULT_PATCH_NORMALIZATION, cfg)
return cfg
def normalize_radiometric_config(base_rad_config, incoming_rad_config=None):
cfg = deep_merge(DEFAULT_RADIOMETRIC_CONFIG, base_rad_config or {})
cfg = deep_merge(cfg, incoming_rad_config or {})
return cfg
def normalize_radiometric_normalization(base_config, incoming_config=None):
cfg = deep_merge(DEFAULT_RADIOMETRIC_NORMALIZATION, base_config or {})
cfg = deep_merge(cfg, incoming_config or {})
cfg["enabled"] = bool(cfg.get("enabled", False))
return cfg
def build_fusion_config(fusion_data, previous_fusion_config=None):
base = previous_fusion_config or {}
generated = {
"alignment_mode": fusion_data.get("alignment_mode"),
"baseline_mm": fusion_data.get("baseline_mm"),
"reference_camera": fusion_data.get("reference_camera"),
"manual_offsets": fusion_data.get("manual_offsets"),
"homographies": fusion_data.get("homographies"),
"crop_valid_common": fusion_data.get("crop_valid_common"),
"resize_after_crop": fusion_data.get("resize_after_crop"),
"target_size": fusion_data.get("target_size"),
}
cfg = deep_merge(default_module_template()["fusion_config"], base)
cfg = deep_merge(cfg, generated)
return cfg
def load_base_module(args):
"""
Carrega defaults do module_params atual.
Prioridade:
1. --base_module_json, se informado.
2. --out, se já existir.
3. template interno coerente com o contrato atual.
"""
candidates = []
if args.base_module_json:
candidates.append(args.base_module_json)
if args.out:
candidates.append(args.out)
for path in candidates:
if path and os.path.isfile(path):
print(f"[INFO] Usando module_params base: {path}")
return deep_merge(default_module_template(), load_json(path, required=True))
print("[WARN] Nenhum module_params base encontrado. Usando defaults internos.")
return default_module_template()
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Monta o module_params.json preservando o contrato atual do RawProcessorCore.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--camera_json", default="calibration/sensor_calibration.json")
parser.add_argument("--fusion_json", default="calibration/manual_offsets.json")
parser.add_argument("--radiometric_json", default="calibration/radiometric_config.json")
parser.add_argument(
"--radiometric_profile",
default="global_scene_mode",
choices=["global_scene_mode", "three_reference_patches_mode"],
)
parser.add_argument("--flatfield_json", default="calibration/flatfield_maps_v1.json")
parser.add_argument("--disable_flatfield", action="store_true")
parser.add_argument("--base_module_json", default=None, help="module_params atual usado como defaults antes de sobrescrever")
parser.add_argument("--out", default="calibration/module_params.json")
args = parser.parse_args()
module_base = load_base_module(args)
cam_data = load_json(args.camera_json, required=True)
fusion_data = load_json(args.fusion_json, required=True)
radiometric_data = load_json(args.radiometric_json, required=False) if args.radiometric_json else {}
# =========================
# MODULE PARAMS FINAL
# =========================
module_params = deepcopy(module_base)
module_params["schema"] = "multispec_module_params_v3"
module_params["saved_at"] = now_str()
# =========================
# ROOT / CAMERA
# =========================
root_updates = {
"frame_type": cam_data.get("frame_type", fusion_data.get("frame_type")),
"capture_mode_requested": cam_data.get("capture_mode_requested"),
"capture_mode_effective": cam_data.get("capture_mode_effective"),
"raw_policy": cam_data.get("raw_policy"),
"sensor_width": cam_data.get("sensor_width", fusion_data.get("sensor_width")),
"sensor_height": cam_data.get("sensor_height", fusion_data.get("sensor_height")),
"bayer_pattern": cam_data.get("bayer_pattern", fusion_data.get("bayer_pattern")),
}
module_params = deep_merge(module_params, root_updates)
module_params["rgb_processing"] = deep_merge(
deep_merge(DEFAULT_RGB_PROCESSING, module_base.get("rgb_processing", {})),
cam_data.get("rgb_processing") if isinstance(cam_data.get("rgb_processing"), dict) else {},
)
camera_settings = cam_data.get("camera_settings")
if not isinstance(camera_settings, dict):
camera_settings = module_base.get("camera_settings")
if not isinstance(camera_settings, dict):
raise RuntimeError("camera_json sem camera_settings válido e sem fallback no module_params base")
module_params["camera_settings"] = camera_settings
module_params["rgb_calibration"] = deep_merge(
module_base.get("rgb_calibration", {}),
cam_data.get("rgb_calibration") if isinstance(cam_data.get("rgb_calibration"), dict) else {},
)
# =========================
# FUSION
# =========================
module_params["fusion_config"] = build_fusion_config(
fusion_data,
previous_fusion_config=module_base.get("fusion_config", {}),
)
# =========================
# RADIOMETRIC
# =========================
incoming_rad = pick_radiometric_config(radiometric_data, selected_profile=args.radiometric_profile)
if not isinstance(incoming_rad, dict):
incoming_rad = cam_data.get("radiometric_config") if isinstance(cam_data.get("radiometric_config"), dict) else {}
module_params["radiometric_config"] = normalize_radiometric_config(
module_base.get("radiometric_config", {}),
incoming_rad,
)
incoming_rad_norm = first_dict(
radiometric_data.get("radiometric_normalization") if isinstance(radiometric_data, dict) else None,
cam_data.get("radiometric_normalization") if isinstance(cam_data, dict) else None,
) or {}
module_params["radiometric_normalization"] = normalize_radiometric_normalization(
module_base.get("radiometric_normalization", {}),
incoming_rad_norm,
)
incoming_patch_norm = first_dict(
radiometric_data.get("patch_normalization") if isinstance(radiometric_data, dict) else None,
cam_data.get("patch_normalization") if isinstance(cam_data, dict) else None,
) or {}
module_params["patch_normalization"] = normalize_patch_normalization_contract(
module_base.get("patch_normalization", {}),
incoming_patch_norm,
)
# =========================
# FLATFIELD
# =========================
previous_flatfield = module_base.get("flatfield_config", {}) or {}
if args.disable_flatfield:
module_params["flatfield_config"] = deep_merge(previous_flatfield, {
"enabled": False,
"reason": "desabilitado via --disable_flatfield",
})
elif args.flatfield_json and os.path.isfile(args.flatfield_json):
flatfield_data = load_json(args.flatfield_json, required=True)
module_params["flatfield_config"] = build_flatfield_config(
args.flatfield_json,
flatfield_data,
previous_flatfield_config=previous_flatfield,
)
else:
# Não achou novo flatfield: preserva o anterior se já existia.
module_params["flatfield_config"] = deep_merge(previous_flatfield, DEFAULT_FLATFIELD_RUNTIME)
if not module_params["flatfield_config"].get("enabled", False):
module_params["flatfield_config"]["reason"] = "flatfield não informado ou arquivo inexistente"
# =========================
# Save
# =========================
save_json(args.out, module_params)
print(f"[OK] module_params gerado em: {args.out}")
print("[OK] contrato preservado: rgb_processing, patch_normalization, radiometric_config e knobs runtime do flatfield")
flat_cfg = module_params.get("flatfield_config", {}) or {}
if flat_cfg.get("enabled"):
print(f"[OK] flatfield habilitado: {flat_cfg.get('npz_file')}")
else:
print(f"[WARN] flatfield desabilitado: {flat_cfg.get('reason')}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff