ajuste no carregamento do mapa no rover

This commit is contained in:
Diego Freitas 2026-09-22 17:04:07 -03:00
parent 2fe3fa4f0e
commit 96c5049899
8 changed files with 1991 additions and 63 deletions

View File

@ -745,30 +745,44 @@ namespace AgroBase.Services
private static void NormalizarMapa(MapaFeatureCollectionModel dados)
{
dados.type = string.IsNullOrWhiteSpace(dados.type) ? "FeatureCollection" : dados.type;
dados.type = string.IsNullOrWhiteSpace(dados.type)
? "FeatureCollection"
: dados.type;
var idsUtilizados = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < dados.features.Count; i++)
{
MapaFeatureModel feature = dados.features[i];
feature.type = string.IsNullOrWhiteSpace(feature.type) ? "Feature" : feature.type;
feature.type = string.IsNullOrWhiteSpace(feature.type)
? "Feature"
: feature.type;
if (feature.properties == null)
{
feature.properties = new MapaFeaturePropertiesModel();
}
/*
* Mantemos a compatibilidade com o fluxo atual:
* a seleção usa IDs internos começando em zero.
*/
string id = (i + 1).ToString(CultureInfo.InvariantCulture);
feature.geometry.id = id;
string id = feature.properties.Id?.Trim();
if (string.IsNullOrWhiteSpace(feature.properties.Id))
// Mapa sem ID explícito:
// gera um ID numérico sequencial.
if (string.IsNullOrWhiteSpace(id))
{
feature.properties.Id = id;
id = (i + 1).ToString(
CultureInfo.InvariantCulture
);
}
if (!idsUtilizados.Add(id))
{
throw new InvalidOperationException(
$"O mapa possui ID de rua duplicado: '{id}'."
);
}
// Uma única identidade em todo o sistema.
feature.properties.Id = id;
feature.geometry.id = id;
}
}
@ -899,6 +913,15 @@ namespace AgroBase.Services
}
function obterIdRua(feature, indice) {
if (
feature &&
feature.properties &&
feature.properties.Id !== undefined &&
feature.properties.Id !== null
) {
return String(feature.properties.Id);
}
if (
feature &&
feature.geometry &&
@ -916,7 +939,7 @@ namespace AgroBase.Services
return String(feature.id);
}
return String(indice);
return String(indice + 1);
}
function ruaSelecionada(id) {

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
r"""
_10_export_onnx.py
Exporta o checkpoint PyTorch do SegFormer Multi-Head OAK-FCC-3 para ONNX.
@ -10,7 +10,7 @@ Exemplo:
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --checkpoint .\backup\segformer_b1\2026_09_18\stacked_raw5\best_score.pt --out .\backup\segformer_b1\2026_09_18\stacked_raw5\best_score_audit.onnx --opset 17 --device cuda --include-norm --norm-stats .\backup\segformer_b1\2026_09_18\stacked_raw5\norm_stats.json --postprocess none
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --checkpoint .\backup\segformer_b1\2026_09_18\stacked_raw5\best_operational.pt --out .\backup\segformer_b1\2026_09_18\stacked_raw5\best_operation.onnx --opset 17 --device cuda --include-norm --norm-stats .\backup\segformer_b1\2026_09_18\stacked_raw5\norm_stats.json --postprocess argmax_fullres
python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --checkpoint .\backup\segformer_b1\2026_09_18\stacked_raw5\best_operational.pt --out .\backup\segformer_b1\2026_09_18\stacked_raw5\best_operation.onnx --opset 17 --device cuda --include-norm --norm-stats .\backup\segformer_b1\2026_09_18\stacked_raw5\norm_stats.json --postprocess argmax_fullres --target-threshold auto --vegetation-threshold 0.5 --cana-threshold 0.5
"""
@ -178,6 +178,95 @@ def load_selected_norm_stats(
return path, mean_sel, std_sel, requested
def _coerce_threshold(value):
"""Converte float/dict de metadata em threshold [0, 1]."""
if isinstance(value, dict):
value = value.get("threshold")
try:
if value is None:
return None
v = float(value)
if math.isfinite(v) and 0.0 <= v <= 1.0:
return v
except Exception:
pass
return None
def extract_operational_threshold_from_checkpoint(ckpt: dict):
"""
Resolve o threshold operacional da head target salvo pelo trainer.
Suporta o contrato atual e alguns formatos anteriores usados nos
checkpoints/auditores do projeto.
"""
if not isinstance(ckpt, dict):
return None, "none"
candidates = []
best = ckpt.get("best", {}) or {}
if isinstance(best, dict):
candidates.extend([
("best.operational_threshold", best.get("operational_threshold")),
("best.best_operational_threshold", best.get("best_operational_threshold")),
])
extra = ckpt.get("extra", {}) or {}
if isinstance(extra, dict):
score_now = extra.get("score_now", {}) or {}
if isinstance(score_now, dict):
candidates.extend([
("extra.score_now.operational_threshold", score_now.get("operational_threshold")),
("extra.score_now.best_operational_threshold", score_now.get("best_operational_threshold")),
])
val = extra.get("val", {}) or {}
if isinstance(val, dict):
agri = val.get("agricultural", {}) or {}
if isinstance(agri, dict):
candidates.append((
"extra.val.agricultural.best_operational_threshold",
agri.get("best_operational_threshold"),
))
for source, value in candidates:
threshold = _coerce_threshold(value)
if threshold is not None:
return threshold, source
return None, "none"
def resolve_threshold_arg(raw_value, *, head_name: str, ckpt: dict, allow_auto: bool):
"""Resolve CLI numérica ou `auto` e devolve (valor, fonte)."""
raw = str(raw_value).strip().lower()
if raw == "auto":
if not allow_auto:
raise RuntimeError(
f"--{head_name}-threshold=auto não é suportado para esta head. "
"Informe um valor explícito entre 0 e 1."
)
value, source = extract_operational_threshold_from_checkpoint(ckpt)
if value is None:
raise RuntimeError(
"Não encontrei o operational_threshold no checkpoint para a head target. "
"Para export de campo não vou cair silenciosamente para 0.5. "
"Informe --target-threshold <valor> explicitamente ou use um checkpoint "
"que contenha best.operational_threshold."
)
return value, f"checkpoint:{source}"
value = _coerce_threshold(raw)
if value is None:
raise RuntimeError(
f"--{head_name}-threshold inválido: {raw_value!r}. Use valor entre 0 e 1"
+ (" ou 'auto'" if allow_auto else "") + "."
)
return value, "cli"
# ============================================================
# Wrapper ONNX
# ============================================================
@ -189,8 +278,8 @@ class MultiHeadOnnxWrapper(nn.Module):
Pode exportar:
- logits crus
- logits redimensionados
- argmax em baixa resolução
- argmax em resolução da entrada
- máscara em baixa resolução (threshold explícito nas heads binárias)
- máscara em resolução da entrada (threshold explícito nas heads binárias)
Também pode embutir a normalização:
x = (x - mean) / std
@ -205,6 +294,7 @@ class MultiHeadOnnxWrapper(nn.Module):
norm_mean: List[float] | None = None,
norm_std: List[float] | None = None,
postprocess: str = "none",
binary_thresholds: Dict[str, float] | None = None,
):
super().__init__()
self.model = model
@ -212,6 +302,17 @@ class MultiHeadOnnxWrapper(nn.Module):
self.resize_to_input = bool(resize_to_input)
self.include_norm = bool(include_norm)
self.postprocess = str(postprocess).lower()
self.binary_thresholds = {
str(k).strip().lower(): float(v)
for k, v in (binary_thresholds or {}).items()
}
invalid_threshold_heads = sorted(
set(self.binary_thresholds) - {"vegetation", "cana", "target"}
)
if invalid_threshold_heads:
raise RuntimeError(
f"Threshold binário configurado para heads inválidas: {invalid_threshold_heads}"
)
if self.postprocess not in ("none", "resize_logits", "argmax_lowres", "argmax_fullres"):
raise RuntimeError(f"postprocess inválido: {self.postprocess}")
@ -232,6 +333,21 @@ class MultiHeadOnnxWrapper(nn.Module):
self.register_buffer("norm_mean", torch.empty(0))
self.register_buffer("norm_std", torch.empty(0))
def _mask_from_logits(self, head_name: str, logits: torch.Tensor) -> torch.Tensor:
"""
Heads binárias configuradas usam probabilidade explícita da classe 1.
Demais heads preservam argmax tradicional.
"""
threshold = self.binary_thresholds.get(str(head_name).strip().lower())
if threshold is not None:
# vegetation/cana/target são heads binárias de 2 logits pelo contrato
# do modelo. Evitamos branch dependente de shape aqui para manter o
# trace/export ONNX totalmente estático.
prob_positive = torch.softmax(logits, dim=1)[:, 1, :, :]
return (prob_positive >= threshold).to(torch.uint8)
return torch.argmax(logits, dim=1).to(torch.uint8)
def forward(self, pixel_values: torch.Tensor):
input_hw = pixel_values.shape[-2:]
@ -262,7 +378,7 @@ class MultiHeadOnnxWrapper(nn.Module):
result.append(logits)
elif self.postprocess == "argmax_lowres":
mask = torch.argmax(logits, dim=1).to(torch.uint8)
mask = self._mask_from_logits(head_name, logits)
result.append(mask)
elif self.postprocess == "argmax_fullres":
@ -272,7 +388,7 @@ class MultiHeadOnnxWrapper(nn.Module):
mode="bilinear",
align_corners=False,
)
mask = torch.argmax(logits, dim=1).to(torch.uint8)
mask = self._mask_from_logits(head_name, logits)
result.append(mask)
return tuple(result)
@ -331,6 +447,32 @@ def main():
),
)
parser.add_argument(
"--target-threshold",
default="auto",
help=(
"Threshold da probabilidade da classe positiva da head target quando o "
"postprocess gera máscara. Padrão='auto': lê o operational_threshold "
"salvo no checkpoint e falha se ele não existir."
),
)
parser.add_argument(
"--vegetation-threshold",
default="0.5",
help=(
"Threshold da classe vegetation para export mask. Padrão=0.5. "
"Use override explícito apenas após calibração específica dessa head."
),
)
parser.add_argument(
"--cana-threshold",
default="0.5",
help=(
"Threshold da classe cana para export mask. Padrão=0.5. "
"Use override explícito apenas após calibração específica dessa head."
),
)
parser.add_argument(
"--dynamic-batch",
action="store_true",
@ -462,6 +604,58 @@ def main():
model.to(device)
model.eval()
mask_postprocess = args.postprocess in ("argmax_lowres", "argmax_fullres")
checkpoint_operational_threshold, checkpoint_operational_threshold_source = (
extract_operational_threshold_from_checkpoint(ckpt)
)
binary_thresholds = {}
threshold_sources = {}
if mask_postprocess:
target_threshold, target_threshold_source = resolve_threshold_arg(
args.target_threshold,
head_name="target",
ckpt=ckpt,
allow_auto=True,
)
vegetation_threshold, vegetation_threshold_source = resolve_threshold_arg(
args.vegetation_threshold,
head_name="vegetation",
ckpt=ckpt,
allow_auto=False,
)
cana_threshold, cana_threshold_source = resolve_threshold_arg(
args.cana_threshold,
head_name="cana",
ckpt=ckpt,
allow_auto=False,
)
binary_thresholds = {
"target": target_threshold,
"vegetation": vegetation_threshold,
"cana": cana_threshold,
}
threshold_sources = {
"target": target_threshold_source,
"vegetation": vegetation_threshold_source,
"cana": cana_threshold_source,
}
print("[THRESHOLD] Máscaras binárias serão exportadas com thresholds explícitos:")
for head_name in ("target", "vegetation", "cana"):
print(
f" {head_name:10s}: {binary_thresholds[head_name]:.3f} "
f"({threshold_sources[head_name]})"
)
else:
print(
"[THRESHOLD] postprocess retorna logits; thresholds não são aplicados no grafo. "
f"Checkpoint operational={checkpoint_operational_threshold} "
f"source={checkpoint_operational_threshold_source}"
)
norm_stats_path = None
norm_mean = None
norm_std = None
@ -488,6 +682,7 @@ def main():
norm_mean=norm_mean,
norm_std=norm_std,
postprocess=args.postprocess,
binary_thresholds=binary_thresholds,
)
wrapper.to(device)
wrapper.eval()
@ -564,6 +759,11 @@ def main():
"heads_config": heads_config,
"checkpoint_epoch": ckpt.get("epoch", None),
"checkpoint_best": ckpt.get("best", None),
"checkpoint_operational_threshold": checkpoint_operational_threshold,
"checkpoint_operational_threshold_source": checkpoint_operational_threshold_source,
"thresholds_applied_in_graph": bool(mask_postprocess),
"binary_thresholds": binary_thresholds,
"binary_threshold_sources": threshold_sources,
"include_norm": bool(args.include_norm),
"norm_stats_path": str(norm_stats_path) if norm_stats_path is not None else None,
"norm_channels": norm_channels if norm_channels else input_channel_names,
@ -588,6 +788,10 @@ def main():
"oak.output_heads": json.dumps(output_heads),
"oak.output_names": json.dumps(output_names),
"oak.stats_source_tag": experiment_tag(config, channels),
"oak.thresholds_applied_in_graph": json.dumps(bool(mask_postprocess)),
"oak.binary_thresholds": json.dumps(binary_thresholds),
"oak.binary_threshold_sources": json.dumps(threshold_sources),
"oak.checkpoint_operational_threshold": json.dumps(checkpoint_operational_threshold),
}
existing = {item.key: item for item in onnx_model.metadata_props}
for key, value in metadata_values.items():

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,15 @@ Exemplo:
python .\\_9_test_multihead.py --config config.json --split_folder val --ckpt backup\segformer_b1\test_multi\stacked_raw5_multihead\best_score.pt
python .\_9_test_multihead_v2.py `
--config .\config_bench_ar0234.json `
--test_folder "C:\Users\USER\Desktop\fotos_multiespectrais\empyreo\chao_cana_erva" `
--ckpt .\backup\segformer_b1\2026_09_18\stacked_raw5\best_score.pt `
--norm_stats .\dataset\960x600\group\norm_stats.json `
--runtime_mode all `
--target_threshold auto
Controles:
D / seta direita : próxima amostra
A / seta esquerda: amostra anterior

View File

@ -1629,6 +1629,10 @@ def main() -> None:
raw_root: Optional[Path] = None
raw_source_map: Dict[Tuple[str, str], dict] = {}
raw_source_resolution: Dict[Tuple[str, str], str] = {}
# Amostras normalizadas continuam válidas para auditoria mesmo quando o
# bundle RAW original está incompleto/ausente. Nesses casos, só bloqueamos
# a exportação física RAW-SAFE daquela amostra e registramos o motivo.
raw_source_failures: Dict[Tuple[str, str], str] = {}
if not args.report_only:
raw_root = (
@ -1645,13 +1649,22 @@ def main() -> None:
raw_by_key, raw_by_base = index_raw_review_sources(raw_root)
fallback_count = 0
for sample in samples:
rec, mode = resolve_raw_review_source(
sample,
raw_by_key,
raw_by_base,
dataset_path,
)
key = (str(sample.group), str(sample.base))
try:
rec, mode = resolve_raw_review_source(
sample,
raw_by_key,
raw_by_base,
dataset_path,
)
except RuntimeError as exc:
raw_source_resolution[key] = "unavailable"
raw_source_failures[key] = str(exc)
print(
f"[RAW-SAFE][SKIP-PHYSICAL] {sample.group}/{sample.base} | {exc}"
)
continue
raw_source_map[key] = rec
raw_source_resolution[key] = mode
if mode != "same_group":
@ -1659,6 +1672,7 @@ def main() -> None:
print(
f"[RAW-SAFE] raw_root={raw_root} | resolvidos={len(raw_source_map)}/{len(samples)} "
f"| indisponiveis={len(raw_source_failures)} "
f"| fallback_base_unico={fallback_count}"
)
@ -2029,6 +2043,8 @@ def main() -> None:
domain_manifest_rows: List[dict] = []
domain_manifest_path = review_group_root / "review_domain_manifest.csv"
domain_readme_path = review_group_root / "README_DOMAIN_CONTRACT.txt"
physical_exported_count = 0
physical_export_skipped_count = 0
if not args.report_only:
print(f"\n[COPY RAW-SAFE] candidatos finais: {len(selected_rows)}")
@ -2038,6 +2054,23 @@ def main() -> None:
key = (str(row["group"]), str(row["base"]))
sample = sample_by_key[key]
# A auditoria/relatório desta amostra continua válido. Só não há
# como gerar raw_previews/raw_masks/predictions_raw com segurança.
raw_source = raw_source_map.get(key)
if raw_source is None:
physical_export_skipped_count += 1
row["physical_export_skipped"] = 1
row["physical_export_skip_reason"] = raw_source_failures.get(
key,
"Fonte RAW indisponível para exportação física RAW-SAFE",
)
print(
f"[COPY RAW-SAFE][SKIP] {sample.group}/{sample.base} | "
f"{row['physical_export_skip_reason']}"
)
cache_for_copy.pop((sample.group, sample.base), None)
continue
cached = cache_for_copy.get((sample.group, sample.base))
if cached is not None and cached[0].group == sample.group:
_, chw, gt_sem, pred_sem = cached
@ -2057,26 +2090,42 @@ def main() -> None:
pred_sem = preds["semantic"]
gt_sem = resize_ids(base.load_mask(sample.masks["semantic"]), pred_sem.shape[:2])
raw_source = raw_source_map.get((str(sample.group), str(sample.base)))
if raw_source is None:
raise RuntimeError(
f"Fonte RAW desapareceu durante exportação: {sample.group}/{sample.base}"
try:
domain_row = save_review_item(
row=row,
sample=sample,
chw=chw,
gt_sem=gt_sem,
pred_sem=pred_sem,
raw_source=raw_source,
review_group_root=review_group_root,
base_module=base,
semantic_cmap=semantic_cmap,
ignore_id=ignore_id,
input_channel_names=input_channel_names,
save_panels=args.save_panels,
)
except RuntimeError as exc:
# Um bundle RAW existente, mas geometricamente inválido/corrompido,
# também não deve abortar milhares de amostras já auditáveis.
physical_export_skipped_count += 1
row["physical_export_skipped"] = 1
row["physical_export_skip_reason"] = str(exc)
print(
f"[COPY RAW-SAFE][SKIP] {sample.group}/{sample.base} | {exc}"
)
cache_for_copy.pop((sample.group, sample.base), None)
del chw, gt_sem, pred_sem
if 'preds' in locals():
try:
del preds
except Exception:
pass
continue
domain_row = save_review_item(
row=row,
sample=sample,
chw=chw,
gt_sem=gt_sem,
pred_sem=pred_sem,
raw_source=raw_source,
review_group_root=review_group_root,
base_module=base,
semantic_cmap=semantic_cmap,
ignore_id=ignore_id,
input_channel_names=input_channel_names,
save_panels=args.save_panels,
)
physical_exported_count += 1
row["physical_export_skipped"] = 0
row["physical_export_skip_reason"] = ""
domain_row["raw_source_resolution"] = raw_source_resolution.get(
(str(sample.group), str(sample.base)), ""
)
@ -2249,6 +2298,10 @@ def main() -> None:
"samples": len(rows),
"selected_for_review": len(selected_rows),
"selected_pct": float(100.0 * len(selected_rows) / max(len(rows), 1)),
"raw_source_available": len(raw_source_map) if not args.report_only else None,
"raw_source_unavailable": len(raw_source_failures) if not args.report_only else None,
"physical_exported": physical_exported_count if not args.report_only else 0,
"physical_export_skipped": physical_export_skipped_count if not args.report_only else 0,
"export": {
"mode": str(args.export_mode),
"min_suspicion_pct": float(args.min_suspicion_pct),
@ -2358,6 +2411,10 @@ def main() -> None:
print(f"Summary : {summary_path}")
if not args.report_only:
print(f"Revisão física RAW-SAFE : {review_group_root}")
print(f"RAW disponíveis : {len(raw_source_map)}")
print(f"RAW indisponíveis : {len(raw_source_failures)}")
print(f"Exportados fisicamente : {physical_exported_count}")
print(f"Pulados na exportação RAW : {physical_export_skipped_count}")
print(f"Domain manifest : {domain_manifest_path}")
print(f"Domain contract : {domain_readme_path}")

View File

@ -721,7 +721,7 @@
}
},
"flatfield_config": {
"enabled": true,
"enabled": false,
"npz_file": "flatfield_maps_v1.npz",
"apply_before_fusion": true,
"apply_after_decode": true,

View File

@ -0,0 +1,216 @@
{
"camera": "oak-fcc-3",
"modelo": "segformer_b1",
"model_name": "2026_09_08",
"main_class_name": "erva",
"es_classes": "",
"model_to_use": "geral",
"raw_size": [1280, 800],
"resolucao": [960, 600],
"roi_inicio": 0.0,
"roi_tamanho": 1.0,
"shaves": 3,
"source_channels": ["R", "G", "B", "RE", "NIR"],
"channels": 5,
"input_channels": ["R", "G", "B", "RE", "NIR"],
"derived_channels": {
"epsilon": 1e-6,
"clip_min": -1.0,
"clip_max": 1.0
},
"backbone": "nvidia/mit-b1",
"fusion_mode": "stacked",
"stats_source_tag": "stacked_raw5",
"module_params_json": "calibration/mp_ar0234/module_params.json",
"ckpt_test": "best_operational",
"multi_head": true,
"heads": {
"semantic": {
"enabled": true,
"type": "multiclass",
"num_classes": 3,
"mask_dir": "masks",
"classes": {"chao": 0, "cana": 1, "erva": 2},
"ignore_index": 255,
"loss_weight": 0.20
},
"vegetation": {
"enabled": true,
"type": "binary",
"num_classes": 2,
"mask_dir": "masks_vegetation",
"classes": {"background": 0, "vegetation": 1},
"ignore_index": 255,
"loss_weight": 0.15
},
"cana": {
"enabled": true,
"type": "binary",
"num_classes": 2,
"mask_dir": "masks_cana",
"classes": {"not_cana": 0, "cana": 1},
"ignore_index": 255,
"loss_weight": 0.35
},
"target": {
"enabled": true,
"type": "binary",
"num_classes": 2,
"mask_dir": "__derived_target__",
"classes": {"background": 0, "target": 1},
"ignore_index": 255,
"loss_weight": 0.30,
"derived": 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

@ -872,7 +872,7 @@ class RawProcessorCore:
# OAK_CORE_PERF_LOG_INTERVAL_S=1.0
# OAK_CORE_SHAPES_LOG_INTERVAL_S=5.0
self.core_perf_debug = str(
os.getenv("OAK_CORE_PERF_DEBUG", "1")
os.getenv("OAK_CORE_PERF_DEBUG", "0")
).strip().lower() not in ("0", "false", "no", "off")
try:
self.core_perf_log_interval_s = max(0.2, float(