2026-05-15 10:52:30 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
"""
|
2026-05-22 22:32:11 +00:00
|
|
|
_5_augmentation_raw_oak.py
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Augmentation multiespectral para OAK-FCC-3 usando os .bin RAW das câmeras,
|
|
|
|
|
máscaras e metadados do dataset.
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Objetivo:
|
|
|
|
|
- Ler amostras em dataset/original/group/<grupo>/
|
|
|
|
|
- Carregar RGB/RE/NIR a partir dos .bin RAW10 packed
|
|
|
|
|
- Aplicar augmentation geométrico sincronizado em todos os canais + máscara
|
|
|
|
|
- Aplicar augmentation radiométrico/físico nos RAWs
|
|
|
|
|
- Salvar nova amostra em dataset/augmented/group/<grupo>/
|
|
|
|
|
- Gerar preview RGB novo para inspeção visual
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Estrutura esperada de entrada:
|
|
|
|
|
|
|
|
|
|
dataset/original/group/<grupo>/
|
|
|
|
|
bins/
|
2026-05-15 10:52:30 +00:00
|
|
|
masks/
|
|
|
|
|
metas/
|
2026-05-22 22:32:11 +00:00
|
|
|
previews/ # opcional
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Estrutura de saída:
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
dataset/augmented/group/<grupo>/
|
2026-05-22 22:32:11 +00:00
|
|
|
bins/
|
2026-05-15 10:52:30 +00:00
|
|
|
masks/
|
2026-05-22 22:32:11 +00:00
|
|
|
metas/
|
|
|
|
|
previews/
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Exemplos:
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
python _5_augmentation_raw_oak.py --copies 3 --clear-dst
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
python _5_augmentation_raw_oak.py ^
|
|
|
|
|
--copies 3 ^
|
|
|
|
|
--groups chao,chao_cana,chao_erva,chao_cana_erva ^
|
|
|
|
|
--clear-dst
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
python _5_augmentation_raw_oak.py ^
|
|
|
|
|
--group-copies chao:1,chao_cana:3,chao_erva:3,chao_cana_erva:5 ^
|
2026-05-15 10:52:30 +00:00
|
|
|
--clear-dst
|
2026-05-22 22:32:11 +00:00
|
|
|
|
|
|
|
|
python _5_augmentation_raw_oak.py --dry-run
|
|
|
|
|
|
|
|
|
|
Observações importantes:
|
|
|
|
|
- Este script NÃO augmenta preview PNG como fonte principal.
|
|
|
|
|
- O preview é somente consequência visual do RGB RAW augmentado.
|
|
|
|
|
- A geometria é sempre igual em RGB/RE/NIR/mask.
|
|
|
|
|
- Radiometria usa mesma base global + pequenas variações por banda/câmera.
|
|
|
|
|
- O script tenta ser tolerante a nomes de arquivo, mas espera que base da amostra
|
|
|
|
|
esteja preservada nos nomes dos bins/masks/metas.
|
2026-05-15 10:52:30 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import argparse
|
2026-05-22 22:32:11 +00:00
|
|
|
import copy
|
|
|
|
|
import json
|
|
|
|
|
import math
|
|
|
|
|
import os
|
2026-05-15 10:52:30 +00:00
|
|
|
import random
|
|
|
|
|
import shutil
|
2026-05-22 22:32:11 +00:00
|
|
|
from dataclasses import dataclass, asdict
|
2026-05-15 10:52:30 +00:00
|
|
|
from pathlib import Path
|
2026-05-22 22:32:11 +00:00
|
|
|
from typing import Dict, List, Optional, Tuple, Any
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
import cv2
|
2026-05-22 22:32:11 +00:00
|
|
|
import numpy as np
|
2026-05-15 10:52:30 +00:00
|
|
|
from PIL import Image
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
try:
|
|
|
|
|
from core.raw_processor_core import RawProcessorCore
|
|
|
|
|
_HAS_RAW_PROCESSOR_CORE = True
|
|
|
|
|
except Exception:
|
|
|
|
|
RawProcessorCore = None
|
|
|
|
|
_HAS_RAW_PROCESSOR_CORE = False
|
|
|
|
|
|
|
|
|
|
_CORE_PREVIEW_CACHE = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Configuração base
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
IMG_EXTS = (".png", ".jpg", ".jpeg")
|
|
|
|
|
META_EXTS = (".json",)
|
|
|
|
|
BIN_EXTS = (".bin",)
|
|
|
|
|
|
|
|
|
|
ROLE_ALIASES = {
|
|
|
|
|
"rgb": ["rgb", "color", "cor", "cam_a", "cama", "CAM_A"],
|
|
|
|
|
"re": ["re", "rededge", "red_edge", "red-edge", "cam_b", "camb", "CAM_B"],
|
|
|
|
|
"nir": ["nir", "ir", "infra", "infrared", "cam_c", "camc", "CAM_C"],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CAM_KEYS = ["CAM_A", "CAM_B", "CAM_C", "cam_a", "cam_b", "cam_c", "cam0", "cam1", "cam2"]
|
|
|
|
|
|
|
|
|
|
DEFAULT_BIT_DEPTH = 10
|
|
|
|
|
DEFAULT_BAYER_PATTERN = "BGGR"
|
|
|
|
|
DEFAULT_RAW_FORMAT = "RAW10_PACKED"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Data classes
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CameraBin:
|
|
|
|
|
role: str
|
|
|
|
|
cam_key: str
|
|
|
|
|
path: Path
|
|
|
|
|
width: int
|
|
|
|
|
height: int
|
|
|
|
|
bit_depth: int = DEFAULT_BIT_DEPTH
|
|
|
|
|
raw_format: str = DEFAULT_RAW_FORMAT
|
|
|
|
|
bayer_pattern: str = DEFAULT_BAYER_PATTERN
|
|
|
|
|
data: Optional[np.ndarray] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class AugParams:
|
|
|
|
|
seed: int
|
|
|
|
|
|
|
|
|
|
# Geometria, igual para tudo
|
|
|
|
|
flip_h: bool
|
|
|
|
|
shift_x_frac: float
|
|
|
|
|
shift_y_frac: float
|
|
|
|
|
scale: float
|
|
|
|
|
rotate_deg: float
|
|
|
|
|
perspective: bool
|
|
|
|
|
perspective_strength: float
|
|
|
|
|
|
|
|
|
|
# Radiometria base
|
|
|
|
|
exposure_mult: float
|
|
|
|
|
gamma: float
|
|
|
|
|
|
|
|
|
|
# Ganhos por papel espectral
|
|
|
|
|
rgb_channel_gain: Tuple[float, float, float]
|
|
|
|
|
re_gain: float
|
|
|
|
|
nir_gain: float
|
|
|
|
|
|
|
|
|
|
# Efeitos físicos leves
|
|
|
|
|
shadow_enabled: bool
|
|
|
|
|
shadow_strength: float
|
|
|
|
|
shadow_angle_deg: float
|
|
|
|
|
highlight_enabled: bool
|
|
|
|
|
highlight_strength: float
|
|
|
|
|
noise_enabled: bool
|
|
|
|
|
noise_sigma_dn: float
|
|
|
|
|
blur_enabled: bool
|
|
|
|
|
blur_kernel: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Utilitários gerais
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
def read_config_model() -> str:
|
|
|
|
|
"""Tenta ler config.json para manter compatibilidade com seus scripts atuais."""
|
|
|
|
|
cfg = Path("config.json")
|
|
|
|
|
if not cfg.exists():
|
|
|
|
|
return "."
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with cfg.open("r", encoding="utf-8") as f:
|
|
|
|
|
config = json.load(f)
|
|
|
|
|
return str(config.get("camera", "."))
|
|
|
|
|
except Exception:
|
|
|
|
|
return "."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read_config_value(key: str, default=None):
|
|
|
|
|
cfg = Path("config.json")
|
|
|
|
|
if not cfg.exists():
|
|
|
|
|
return default
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
try:
|
|
|
|
|
with cfg.open("r", encoding="utf-8") as f:
|
|
|
|
|
config = json.load(f)
|
|
|
|
|
return config.get(key, default)
|
|
|
|
|
except Exception:
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_dir(path: Path) -> None:
|
|
|
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_dir(path: Path) -> None:
|
|
|
|
|
if path.exists():
|
|
|
|
|
shutil.rmtree(path)
|
|
|
|
|
ensure_dir(path)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def normalizar_base(stem: str) -> str:
|
|
|
|
|
"""Remove sufixos comuns para casar preview/mask/meta/bin."""
|
|
|
|
|
suffixes = [
|
|
|
|
|
"_rgb", "_RGB", "_Rgb", "_color", "_COLOR",
|
|
|
|
|
"_re", "_RE", "_rededge", "_red_edge", "_nir", "_NIR",
|
|
|
|
|
"_cam_a", "_cam_b", "_cam_c", "_CAM_A", "_CAM_B", "_CAM_C",
|
|
|
|
|
"_cam0", "_cam1", "_cam2", "_CAM0", "_CAM1", "_CAM2",
|
|
|
|
|
"_image", "_img", "_frame", "_preview", "_previews",
|
|
|
|
|
"_mask", "_masks", "_seg", "_SEG", "_segment", "_segmentacao",
|
|
|
|
|
"_meta", "_metadata",
|
|
|
|
|
]
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
out = stem
|
|
|
|
|
changed = True
|
|
|
|
|
while changed:
|
|
|
|
|
changed = False
|
|
|
|
|
for sfx in suffixes:
|
|
|
|
|
if out.endswith(sfx):
|
|
|
|
|
out = out[: -len(sfx)]
|
|
|
|
|
changed = True
|
|
|
|
|
break
|
|
|
|
|
return out
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def list_groups(src_root: Path) -> List[str]:
|
|
|
|
|
if not src_root.exists():
|
|
|
|
|
return []
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
groups = []
|
|
|
|
|
for p in sorted(src_root.iterdir()):
|
|
|
|
|
if not p.is_dir():
|
|
|
|
|
continue
|
|
|
|
|
if (p / "bins").is_dir() and (p / "masks").is_dir() and (p / "metas").is_dir():
|
|
|
|
|
groups.append(p.name)
|
|
|
|
|
return groups
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def map_files_by_base(folder: Path, exts: Tuple[str, ...]) -> Dict[str, Path]:
|
|
|
|
|
by_base: Dict[str, Path] = {}
|
|
|
|
|
if not folder.is_dir():
|
|
|
|
|
return by_base
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
priority = {
|
|
|
|
|
".json": 0,
|
|
|
|
|
".png": 0,
|
|
|
|
|
".bin": 0,
|
|
|
|
|
".jpg": 1,
|
|
|
|
|
".jpeg": 2,
|
|
|
|
|
}
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
for p in sorted(folder.iterdir()):
|
|
|
|
|
if not p.is_file():
|
|
|
|
|
continue
|
|
|
|
|
ext = p.suffix.lower()
|
|
|
|
|
if ext not in exts:
|
|
|
|
|
continue
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
base = normalizar_base(p.stem)
|
|
|
|
|
if base not in by_base:
|
|
|
|
|
by_base[base] = p
|
|
|
|
|
else:
|
|
|
|
|
cur_ext = by_base[base].suffix.lower()
|
|
|
|
|
if priority.get(ext, 99) < priority.get(cur_ext, 99):
|
|
|
|
|
by_base[base] = p
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
return by_base
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def load_json(path: Path) -> Dict[str, Any]:
|
|
|
|
|
with path.open("r", encoding="utf-8") as f:
|
|
|
|
|
return json.load(f)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def save_json(path: Path, data: Dict[str, Any]) -> None:
|
|
|
|
|
ensure_dir(path.parent)
|
|
|
|
|
with path.open("w", encoding="utf-8") as f:
|
|
|
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def load_mask(path: Path) -> np.ndarray:
|
|
|
|
|
im = cv2.imread(str(path), cv2.IMREAD_UNCHANGED)
|
|
|
|
|
if im is None:
|
|
|
|
|
raise FileNotFoundError(path)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if im.ndim == 2:
|
|
|
|
|
return im
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if im.shape[2] == 4:
|
|
|
|
|
im = cv2.cvtColor(im, cv2.COLOR_BGRA2RGBA)
|
|
|
|
|
else:
|
|
|
|
|
im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
|
|
|
|
|
return im
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def save_mask(path: Path, mask: np.ndarray) -> None:
|
|
|
|
|
ensure_dir(path.parent)
|
|
|
|
|
if mask.ndim == 2:
|
|
|
|
|
Image.fromarray(mask).save(path)
|
|
|
|
|
else:
|
|
|
|
|
Image.fromarray(mask).save(path)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
# ============================================================
|
|
|
|
|
# RAW10 pack/unpack
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
def unpack_raw10_packed(buf: bytes, width: int, height: int) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
Desempacota MIPI RAW10 packed no padrão mais comum usado por câmeras/DepthAI:
|
|
|
|
|
|
|
|
|
|
Para cada grupo de 5 bytes:
|
|
|
|
|
byte0 = bits [9:2] do pixel 0
|
|
|
|
|
byte1 = bits [9:2] do pixel 1
|
|
|
|
|
byte2 = bits [9:2] do pixel 2
|
|
|
|
|
byte3 = bits [9:2] do pixel 3
|
|
|
|
|
byte4 = bits [1:0] dos 4 pixels, empilhados em pares de bits
|
|
|
|
|
|
|
|
|
|
Retorna uint16 com valores 0..1023.
|
|
|
|
|
|
|
|
|
|
Importante:
|
|
|
|
|
- A versão anterior tratava byte0..byte3 como bits baixos e byte4 como bits altos.
|
|
|
|
|
Isso embaralha o RAW e gera exatamente aquele padrão de ruído/colorido sem imagem.
|
|
|
|
|
"""
|
|
|
|
|
expected_pixels = width * height
|
|
|
|
|
arr = np.frombuffer(buf, dtype=np.uint8)
|
|
|
|
|
|
|
|
|
|
expected_bytes = (expected_pixels * 10 + 7) // 8
|
|
|
|
|
if arr.size < expected_bytes:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"RAW10 menor que esperado: bytes={arr.size}, esperado>={expected_bytes}, "
|
|
|
|
|
f"shape={width}x{height}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
arr = arr[:expected_bytes]
|
|
|
|
|
groups = arr.size // 5
|
|
|
|
|
main = arr[: groups * 5].reshape(-1, 5).astype(np.uint16)
|
|
|
|
|
|
|
|
|
|
p0 = (main[:, 0] << 2) | ((main[:, 4] >> 0) & 0x03)
|
|
|
|
|
p1 = (main[:, 1] << 2) | ((main[:, 4] >> 2) & 0x03)
|
|
|
|
|
p2 = (main[:, 2] << 2) | ((main[:, 4] >> 4) & 0x03)
|
|
|
|
|
p3 = (main[:, 3] << 2) | ((main[:, 4] >> 6) & 0x03)
|
|
|
|
|
|
|
|
|
|
out = np.empty(groups * 4, dtype=np.uint16)
|
|
|
|
|
out[0::4] = p0
|
|
|
|
|
out[1::4] = p1
|
|
|
|
|
out[2::4] = p2
|
|
|
|
|
out[3::4] = p3
|
|
|
|
|
|
|
|
|
|
if out.size < expected_pixels:
|
|
|
|
|
raise ValueError(f"RAW10 gerou pixels insuficientes: {out.size} < {expected_pixels}")
|
|
|
|
|
|
|
|
|
|
return out[:expected_pixels].reshape(height, width)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pack_raw10_packed(raw: np.ndarray) -> bytes:
|
|
|
|
|
"""
|
|
|
|
|
Empacota uint16 0..1023 para MIPI RAW10 packed no mesmo padrão do unpack:
|
|
|
|
|
|
|
|
|
|
byte0..byte3 = bits altos [9:2]
|
|
|
|
|
byte4 = bits baixos [1:0] de p0,p1,p2,p3
|
|
|
|
|
"""
|
|
|
|
|
flat = np.asarray(raw, dtype=np.uint16).reshape(-1)
|
|
|
|
|
flat = np.clip(flat, 0, 1023).astype(np.uint16)
|
|
|
|
|
|
|
|
|
|
pad = (-flat.size) % 4
|
|
|
|
|
if pad:
|
|
|
|
|
flat = np.pad(flat, (0, pad), mode="edge")
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
p = flat.reshape(-1, 4)
|
|
|
|
|
out = np.empty((p.shape[0], 5), dtype=np.uint8)
|
|
|
|
|
|
|
|
|
|
out[:, 0] = ((p[:, 0] >> 2) & 0xFF).astype(np.uint8)
|
|
|
|
|
out[:, 1] = ((p[:, 1] >> 2) & 0xFF).astype(np.uint8)
|
|
|
|
|
out[:, 2] = ((p[:, 2] >> 2) & 0xFF).astype(np.uint8)
|
|
|
|
|
out[:, 3] = ((p[:, 3] >> 2) & 0xFF).astype(np.uint8)
|
|
|
|
|
out[:, 4] = (
|
|
|
|
|
((p[:, 0] & 0x03) << 0)
|
|
|
|
|
| ((p[:, 1] & 0x03) << 2)
|
|
|
|
|
| ((p[:, 2] & 0x03) << 4)
|
|
|
|
|
| ((p[:, 3] & 0x03) << 6)
|
|
|
|
|
).astype(np.uint8)
|
|
|
|
|
|
|
|
|
|
return out.reshape(-1).tobytes()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pack_raw10_packed_array(raw: np.ndarray) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
RAW16 0..1023 -> ndarray uint8 RAW10 packed shape=(height, packed_width).
|
|
|
|
|
Esse é o formato esperado pelo RawProcessorCore.decode_stream_cameras.
|
|
|
|
|
"""
|
|
|
|
|
h, w = raw.shape[:2]
|
|
|
|
|
packed = np.frombuffer(pack_raw10_packed(raw), dtype=np.uint8)
|
|
|
|
|
packed_w = int(math.ceil(int(w) * 10 / 8))
|
|
|
|
|
return packed.reshape(int(h), packed_w)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read_raw_bin(path: Path, width: int, height: int, raw_format: str) -> np.ndarray:
|
|
|
|
|
raw_format_u = (raw_format or "").upper()
|
|
|
|
|
buf = path.read_bytes()
|
|
|
|
|
|
|
|
|
|
if "RAW10" in raw_format_u or "PACKED" in raw_format_u:
|
|
|
|
|
return unpack_raw10_packed(buf, width, height)
|
|
|
|
|
|
|
|
|
|
# Fallback: RAW16 little-endian com valores possivelmente 0..1023.
|
|
|
|
|
arr = np.frombuffer(buf, dtype=np.uint16)
|
|
|
|
|
expected = width * height
|
|
|
|
|
if arr.size < expected:
|
|
|
|
|
raise ValueError(f"RAW16 menor que esperado em {path}: {arr.size} < {expected}")
|
|
|
|
|
return arr[:expected].reshape(height, width)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_raw_bin(path: Path, raw: np.ndarray, raw_format: str) -> None:
|
|
|
|
|
ensure_dir(path.parent)
|
|
|
|
|
raw_format_u = (raw_format or "").upper()
|
|
|
|
|
|
|
|
|
|
raw10 = np.clip(np.rint(raw), 0, 1023).astype(np.uint16)
|
|
|
|
|
|
|
|
|
|
if "RAW10" in raw_format_u or "PACKED" in raw_format_u:
|
|
|
|
|
path.write_bytes(pack_raw10_packed(raw10))
|
|
|
|
|
else:
|
|
|
|
|
path.write_bytes(raw10.astype(np.uint16).tobytes())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Metadados e descoberta de bins
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
def _text_has_any(text: str, needles: List[str]) -> bool:
|
|
|
|
|
t = text.lower()
|
|
|
|
|
return any(n.lower() in t for n in needles)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def role_from_text(text: str) -> Optional[str]:
|
|
|
|
|
for role, aliases in ROLE_ALIASES.items():
|
|
|
|
|
if _text_has_any(text, aliases):
|
|
|
|
|
return role
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_role(role: Optional[str], cam_key: Optional[str] = None) -> Optional[str]:
|
|
|
|
|
if role:
|
|
|
|
|
r = str(role).lower().strip()
|
|
|
|
|
if r in ("rgb", "color", "cor"):
|
|
|
|
|
return "rgb"
|
|
|
|
|
if r in ("re", "rededge", "red_edge", "red-edge"):
|
|
|
|
|
return "re"
|
|
|
|
|
if r in ("nir", "ir", "infrared", "infra"):
|
|
|
|
|
return "nir"
|
|
|
|
|
|
|
|
|
|
if cam_key:
|
|
|
|
|
ck = str(cam_key).upper()
|
|
|
|
|
# Convenção atual mais provável: CAM_A=RGB, CAM_B=RE, CAM_C=NIR.
|
|
|
|
|
if ck == "CAM_A":
|
|
|
|
|
return "rgb"
|
|
|
|
|
if ck == "CAM_B":
|
|
|
|
|
return "re"
|
|
|
|
|
if ck == "CAM_C":
|
|
|
|
|
return "nir"
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_camera_info(meta: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
"""
|
|
|
|
|
Tenta extrair camera_info de vários formatos possíveis.
|
|
|
|
|
Retorna dict cam_key -> info.
|
|
|
|
|
"""
|
|
|
|
|
candidates = []
|
|
|
|
|
|
|
|
|
|
for key in ["camera_info", "cameras", "camera_infos", "payload_sources_info"]:
|
|
|
|
|
if isinstance(meta.get(key), dict):
|
|
|
|
|
candidates.append(meta[key])
|
|
|
|
|
|
|
|
|
|
# Algumas versões guardam dentro de meta["capture"] ou meta["meta"]
|
|
|
|
|
for parent_key in ["capture", "meta", "frame_meta"]:
|
|
|
|
|
parent = meta.get(parent_key)
|
|
|
|
|
if isinstance(parent, dict):
|
|
|
|
|
for key in ["camera_info", "cameras", "camera_infos"]:
|
|
|
|
|
if isinstance(parent.get(key), dict):
|
|
|
|
|
candidates.append(parent[key])
|
|
|
|
|
|
|
|
|
|
if candidates:
|
|
|
|
|
cam_info = candidates[0]
|
|
|
|
|
out = {}
|
|
|
|
|
for k, v in cam_info.items():
|
|
|
|
|
if isinstance(v, dict):
|
|
|
|
|
out[str(k)] = v
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
# Fallback mínimo caso o meta já tenha campos diretos.
|
|
|
|
|
out = {}
|
|
|
|
|
for ck in CAM_KEYS:
|
|
|
|
|
if isinstance(meta.get(ck), dict):
|
|
|
|
|
out[ck] = meta[ck]
|
2026-05-15 10:52:30 +00:00
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def get_int_any(d: Dict[str, Any], keys: List[str], default: Optional[int] = None) -> Optional[int]:
|
|
|
|
|
for k in keys:
|
|
|
|
|
if k in d and d[k] is not None:
|
|
|
|
|
try:
|
|
|
|
|
return int(d[k])
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return default
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def get_str_any(d: Dict[str, Any], keys: List[str], default: str = "") -> str:
|
|
|
|
|
for k in keys:
|
|
|
|
|
if k in d and d[k] is not None:
|
|
|
|
|
return str(d[k])
|
|
|
|
|
return default
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def find_bin_for_camera(bins_dir: Path, base: str, role: str, cam_key: str) -> Optional[Path]:
|
|
|
|
|
"""
|
|
|
|
|
Localiza o bin de uma câmera por base + role/cam_key.
|
|
|
|
|
É tolerante a nomes tipo:
|
|
|
|
|
base_CAM_A.bin
|
|
|
|
|
base_rgb.bin
|
|
|
|
|
base_cam0.bin
|
|
|
|
|
base_arquivo_CAM_A.bin
|
|
|
|
|
"""
|
|
|
|
|
if not bins_dir.is_dir():
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
role_aliases = ROLE_ALIASES.get(role, [role])
|
|
|
|
|
cam_key_variants = {cam_key, cam_key.upper(), cam_key.lower()}
|
|
|
|
|
|
|
|
|
|
candidates = []
|
|
|
|
|
for p in sorted(bins_dir.glob("*.bin")):
|
|
|
|
|
name = p.stem
|
|
|
|
|
name_l = name.lower()
|
|
|
|
|
base_l = base.lower()
|
|
|
|
|
if not name_l.startswith(base_l):
|
2026-05-15 10:52:30 +00:00
|
|
|
continue
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
score = 0
|
|
|
|
|
if name_l == base_l:
|
|
|
|
|
score += 1
|
|
|
|
|
if any(v.lower() in name_l for v in cam_key_variants):
|
|
|
|
|
score += 10
|
|
|
|
|
if any(a.lower() in name_l for a in role_aliases):
|
|
|
|
|
score += 12
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
# Compatibilidade com cam0/cam1/cam2 caso necessário.
|
|
|
|
|
if role == "rgb" and "cam0" in name_l:
|
|
|
|
|
score += 3
|
|
|
|
|
if role == "re" and "cam1" in name_l:
|
|
|
|
|
score += 3
|
|
|
|
|
if role == "nir" and "cam2" in name_l:
|
|
|
|
|
score += 3
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if score > 0:
|
|
|
|
|
candidates.append((score, p))
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if not candidates:
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
candidates.sort(key=lambda x: x[0], reverse=True)
|
|
|
|
|
return candidates[0][1]
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def discover_sample_cameras(meta: Dict[str, Any], bins_dir: Path, base: str) -> Dict[str, CameraBin]:
|
|
|
|
|
cam_info = extract_camera_info(meta)
|
|
|
|
|
out: Dict[str, CameraBin] = {}
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
# Primeiro caminho: usar camera_info do meta.
|
|
|
|
|
for cam_key, info in cam_info.items():
|
|
|
|
|
role = normalize_role(info.get("role") or info.get("papel") or info.get("type"), cam_key)
|
|
|
|
|
if role not in ("rgb", "re", "nir"):
|
|
|
|
|
role = role_from_text(cam_key) or role_from_text(json.dumps(info, ensure_ascii=False))
|
|
|
|
|
|
|
|
|
|
if role not in ("rgb", "re", "nir"):
|
2026-05-15 10:52:30 +00:00
|
|
|
continue
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
width = get_int_any(info, ["width", "w", "sensor_width", "cols", "shape_w"])
|
|
|
|
|
height = get_int_any(info, ["height", "h", "sensor_height", "rows", "shape_h"])
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if width is None or height is None:
|
|
|
|
|
# Fallback para campos globais do meta.
|
|
|
|
|
width = get_int_any(meta, ["width", "sensor_width", "raw_width", "w"])
|
|
|
|
|
height = get_int_any(meta, ["height", "sensor_height", "raw_height", "h"])
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if width is None or height is None:
|
|
|
|
|
continue
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
bit_depth = get_int_any(info, ["bit_depth", "bits", "raw_bit_depth"], DEFAULT_BIT_DEPTH)
|
|
|
|
|
raw_format = get_str_any(info, ["raw_format", "format", "encoding"], DEFAULT_RAW_FORMAT)
|
|
|
|
|
bayer_pattern = get_str_any(info, ["bayer_pattern", "bayer", "pattern"], DEFAULT_BAYER_PATTERN)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
bin_path = find_bin_for_camera(bins_dir, base, role, cam_key)
|
|
|
|
|
if bin_path is None:
|
|
|
|
|
continue
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
out[role] = CameraBin(
|
|
|
|
|
role=role,
|
|
|
|
|
cam_key=str(cam_key),
|
|
|
|
|
path=bin_path,
|
|
|
|
|
width=int(width),
|
|
|
|
|
height=int(height),
|
|
|
|
|
bit_depth=int(bit_depth or DEFAULT_BIT_DEPTH),
|
|
|
|
|
raw_format=raw_format or DEFAULT_RAW_FORMAT,
|
|
|
|
|
bayer_pattern=bayer_pattern or DEFAULT_BAYER_PATTERN,
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
# Segundo caminho: se o meta não ajudou, tenta descobrir por nome.
|
|
|
|
|
if len(out) < 3:
|
|
|
|
|
global_w = get_int_any(meta, ["width", "sensor_width", "raw_width", "w"])
|
|
|
|
|
global_h = get_int_any(meta, ["height", "sensor_height", "raw_height", "h"])
|
|
|
|
|
|
|
|
|
|
for role, cam_key in [("rgb", "CAM_A"), ("re", "CAM_B"), ("nir", "CAM_C")]:
|
|
|
|
|
if role in out:
|
|
|
|
|
continue
|
|
|
|
|
if global_w is None or global_h is None:
|
|
|
|
|
continue
|
|
|
|
|
bin_path = find_bin_for_camera(bins_dir, base, role, cam_key)
|
|
|
|
|
if bin_path is None:
|
|
|
|
|
continue
|
|
|
|
|
out[role] = CameraBin(
|
|
|
|
|
role=role,
|
|
|
|
|
cam_key=cam_key,
|
|
|
|
|
path=bin_path,
|
|
|
|
|
width=int(global_w),
|
|
|
|
|
height=int(global_h),
|
|
|
|
|
bit_depth=DEFAULT_BIT_DEPTH,
|
|
|
|
|
raw_format=DEFAULT_RAW_FORMAT,
|
|
|
|
|
bayer_pattern=DEFAULT_BAYER_PATTERN,
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
return out
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
# ============================================================
|
|
|
|
|
# Preview RGB simples a partir de RAW Bayer
|
|
|
|
|
# ============================================================
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def _normalize_preview_channel(x: np.ndarray, lo_p: float = 1.0, hi_p: float = 99.5, gamma: float = 1.0 / 2.2) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
Normalização visual robusta para preview, não científica.
|
|
|
|
|
Transforma um canal RAW/float em uint8 bonito para inspeção humana.
|
|
|
|
|
"""
|
|
|
|
|
y = x.astype(np.float32)
|
|
|
|
|
lo = float(np.percentile(y, lo_p))
|
|
|
|
|
hi = float(np.percentile(y, hi_p))
|
|
|
|
|
if hi <= lo:
|
|
|
|
|
hi = lo + 1.0
|
|
|
|
|
y = np.clip((y - lo) / (hi - lo), 0.0, 1.0)
|
|
|
|
|
if gamma and abs(gamma - 1.0) > 1e-6:
|
|
|
|
|
y = np.power(y, gamma)
|
|
|
|
|
return np.clip(y * 255.0, 0, 255).astype(np.uint8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_bayer_cv2_code_like_core(bayer_pattern: str | None, algorithm: str = "ea") -> int:
|
|
|
|
|
"""
|
|
|
|
|
Replica o mapeamento usado pelo RawProcessorCore._get_bayer_cv2_code.
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Observação importante:
|
|
|
|
|
O mapeamento OpenCV parece invertido à primeira vista, mas é exatamente
|
|
|
|
|
o contrato usado no core atual para gerar o RGB do treinamento/inferência.
|
|
|
|
|
"""
|
|
|
|
|
p = str(bayer_pattern or DEFAULT_BAYER_PATTERN or "RGGB").upper()
|
|
|
|
|
algo = str(algorithm or "ea").lower()
|
|
|
|
|
|
|
|
|
|
if algo in ("bilinear", "linear", "fast", "normal"):
|
|
|
|
|
code_map = {
|
|
|
|
|
"BGGR": cv2.COLOR_BayerRG2RGB,
|
|
|
|
|
"RGGB": cv2.COLOR_BayerBG2RGB,
|
|
|
|
|
"GRBG": cv2.COLOR_BayerGR2RGB,
|
|
|
|
|
"GBRG": cv2.COLOR_BayerGB2RGB,
|
|
|
|
|
}
|
|
|
|
|
elif algo in ("ea", "edge_aware", "edge-aware"):
|
|
|
|
|
code_map = {
|
|
|
|
|
"BGGR": cv2.COLOR_BayerRG2RGB_EA,
|
|
|
|
|
"RGGB": cv2.COLOR_BayerBG2RGB_EA,
|
|
|
|
|
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
|
|
|
|
|
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
|
|
|
|
|
}
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"demosaic_algorithm inválido: {algorithm}")
|
|
|
|
|
|
|
|
|
|
if p not in code_map:
|
|
|
|
|
raise ValueError(f"Padrão Bayer não suportado para demosaic: {p}")
|
|
|
|
|
|
|
|
|
|
return code_map[p]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def demosaic_raw10_to_rgb01(raw: np.ndarray, bayer_pattern: str = DEFAULT_BAYER_PATTERN, algorithm: str = "ea") -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
RAW Bayer 0..1023 -> RGB HWC float32 0..1 usando o mesmo mapeamento do core.
|
|
|
|
|
"""
|
|
|
|
|
raw_u16 = np.clip(raw, 0, 1023).astype(np.uint16)
|
|
|
|
|
cv_code = _get_bayer_cv2_code_like_core(bayer_pattern, algorithm=algorithm)
|
|
|
|
|
rgb16 = cv2.cvtColor(raw_u16, cv_code)
|
|
|
|
|
rgb = rgb16.astype(np.float32) / 1023.0
|
|
|
|
|
return np.clip(rgb, 0.0, 1.0).astype(np.float32, copy=False)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def remosaic_rgb01_to_bayer_raw10(rgb: np.ndarray, bayer_pattern: str = DEFAULT_BAYER_PATTERN) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
RGB HWC float32 0..1 -> mosaico Bayer RAW10 uint16.
|
|
|
|
|
|
|
|
|
|
Isso é necessário porque NÃO podemos aplicar warp diretamente no mosaico Bayer.
|
|
|
|
|
O fluxo correto para augmentar RGB RAW é:
|
|
|
|
|
raw Bayer -> demosaic RGB -> augmentation -> remosaic Bayer -> pack RAW10.
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Não é uma reconstrução óptica perfeita, mas preserva o contrato RAW10 Bayer para
|
|
|
|
|
o normalize/RawProcessorCore e evita o xadrez colorido causado por interpolar
|
|
|
|
|
diretamente o mosaico.
|
2026-05-15 10:52:30 +00:00
|
|
|
"""
|
2026-05-22 22:32:11 +00:00
|
|
|
rgb = np.clip(rgb.astype(np.float32), 0.0, 1.0)
|
|
|
|
|
h, w = rgb.shape[:2]
|
|
|
|
|
raw = np.empty((h, w), dtype=np.float32)
|
|
|
|
|
|
|
|
|
|
p = str(bayer_pattern or DEFAULT_BAYER_PATTERN or "RGGB").upper()
|
|
|
|
|
|
|
|
|
|
if p == "RGGB":
|
|
|
|
|
raw[0::2, 0::2] = rgb[0::2, 0::2, 0] # R
|
|
|
|
|
raw[0::2, 1::2] = rgb[0::2, 1::2, 1] # G
|
|
|
|
|
raw[1::2, 0::2] = rgb[1::2, 0::2, 1] # G
|
|
|
|
|
raw[1::2, 1::2] = rgb[1::2, 1::2, 2] # B
|
|
|
|
|
elif p == "BGGR":
|
|
|
|
|
raw[0::2, 0::2] = rgb[0::2, 0::2, 2] # B
|
|
|
|
|
raw[0::2, 1::2] = rgb[0::2, 1::2, 1] # G
|
|
|
|
|
raw[1::2, 0::2] = rgb[1::2, 0::2, 1] # G
|
|
|
|
|
raw[1::2, 1::2] = rgb[1::2, 1::2, 0] # R
|
|
|
|
|
elif p == "GRBG":
|
|
|
|
|
raw[0::2, 0::2] = rgb[0::2, 0::2, 1] # G
|
|
|
|
|
raw[0::2, 1::2] = rgb[0::2, 1::2, 0] # R
|
|
|
|
|
raw[1::2, 0::2] = rgb[1::2, 0::2, 2] # B
|
|
|
|
|
raw[1::2, 1::2] = rgb[1::2, 1::2, 1] # G
|
|
|
|
|
elif p == "GBRG":
|
|
|
|
|
raw[0::2, 0::2] = rgb[0::2, 0::2, 1] # G
|
|
|
|
|
raw[0::2, 1::2] = rgb[0::2, 1::2, 2] # B
|
|
|
|
|
raw[1::2, 0::2] = rgb[1::2, 0::2, 0] # R
|
|
|
|
|
raw[1::2, 1::2] = rgb[1::2, 1::2, 1] # G
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Padrão Bayer não suportado para remosaic: {p}")
|
|
|
|
|
|
|
|
|
|
return np.clip(np.rint(raw * 1023.0), 0, 1023).astype(np.uint16)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rgb01_to_preview_rgb8(rgb: np.ndarray) -> np.ndarray:
|
|
|
|
|
rgb = np.clip(rgb.astype(np.float32), 0.0, 1.0)
|
|
|
|
|
rgb8 = np.zeros(rgb.shape, dtype=np.uint8)
|
|
|
|
|
for c in range(3):
|
|
|
|
|
rgb8[:, :, c] = _normalize_preview_channel(rgb[:, :, c], lo_p=1.0, hi_p=99.5, gamma=1.0 / 2.2)
|
|
|
|
|
return rgb8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def debayer_raw10_to_rgb8(raw: np.ndarray, bayer_pattern: str = DEFAULT_BAYER_PATTERN) -> np.ndarray:
|
2026-05-15 10:52:30 +00:00
|
|
|
"""
|
2026-05-22 22:32:11 +00:00
|
|
|
Preview visual a partir do RAW Bayer, usando o mesmo mapeamento Bayer do core.
|
|
|
|
|
"""
|
|
|
|
|
rgb01 = demosaic_raw10_to_rgb01(raw, bayer_pattern=bayer_pattern, algorithm="ea")
|
|
|
|
|
return rgb01_to_preview_rgb8(rgb01)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def _resolve_module_params_path(meta: Dict[str, Any]) -> Optional[str]:
|
|
|
|
|
candidates = []
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
for key in ("camera_params_json", "module_params_json"):
|
|
|
|
|
v = meta.get(key)
|
|
|
|
|
if v:
|
|
|
|
|
candidates.append(str(v))
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
stream_meta = meta.get("stream_meta") if isinstance(meta.get("stream_meta"), dict) else None
|
|
|
|
|
if stream_meta:
|
|
|
|
|
for key in ("camera_params_json", "module_params_json"):
|
|
|
|
|
v = stream_meta.get(key)
|
|
|
|
|
if v:
|
|
|
|
|
candidates.append(str(v))
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
cfg_module = read_config_value("module_params_json", None)
|
|
|
|
|
if cfg_module:
|
|
|
|
|
candidates.append(str(cfg_module))
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
candidates.append("calibration/module_params.json")
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
for c in candidates:
|
|
|
|
|
p = Path(c)
|
|
|
|
|
if p.is_file():
|
|
|
|
|
return str(p)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def _build_processing_meta_for_core(meta: Dict[str, Any], cameras: Dict[str, CameraBin]) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Monta o meta que o RawProcessorCore espera no build_infer_tensor_from_stream.
|
|
|
|
|
O validador faz isso a partir de stream_meta + camera_info. Aqui criamos/atualizamos
|
|
|
|
|
esse pacote usando os bins augmentados.
|
|
|
|
|
"""
|
|
|
|
|
stream_meta = copy.deepcopy(meta.get("stream_meta") if isinstance(meta.get("stream_meta"), dict) else {})
|
|
|
|
|
|
|
|
|
|
camera_info = stream_meta.get("camera_info")
|
|
|
|
|
if not isinstance(camera_info, dict):
|
|
|
|
|
camera_info = copy.deepcopy(meta.get("camera_info") if isinstance(meta.get("camera_info"), dict) else {})
|
|
|
|
|
|
|
|
|
|
camera_frames = stream_meta.get("camera_frames")
|
|
|
|
|
if not isinstance(camera_frames, dict):
|
|
|
|
|
camera_frames = copy.deepcopy(meta.get("camera_frames") if isinstance(meta.get("camera_frames"), dict) else {})
|
|
|
|
|
|
|
|
|
|
for role, cam in cameras.items():
|
|
|
|
|
info = dict(camera_info.get(cam.cam_key, {}) or {})
|
|
|
|
|
info.update({
|
|
|
|
|
"role": role,
|
|
|
|
|
"width": int(cam.width),
|
|
|
|
|
"height": int(cam.height),
|
|
|
|
|
"bit_depth": int(cam.bit_depth),
|
|
|
|
|
"raw_format": cam.raw_format or DEFAULT_RAW_FORMAT,
|
|
|
|
|
"packed": True,
|
|
|
|
|
"channels": 1,
|
|
|
|
|
"bayer_pattern": cam.bayer_pattern or DEFAULT_BAYER_PATTERN,
|
|
|
|
|
})
|
|
|
|
|
camera_info[cam.cam_key] = info
|
|
|
|
|
camera_frames[cam.cam_key] = dict(info)
|
|
|
|
|
|
|
|
|
|
stream_meta["frame_type"] = "RAW_BRUTO"
|
|
|
|
|
stream_meta["camera_info"] = camera_info
|
|
|
|
|
stream_meta["camera_frames"] = camera_frames
|
|
|
|
|
stream_meta["payload_sources"] = [cameras[r].cam_key for r in ("rgb", "re", "nir") if r in cameras]
|
|
|
|
|
|
|
|
|
|
# A normalização radiométrica do core busca frame_controls no próprio meta.
|
|
|
|
|
# Preservamos os controles originais se existirem.
|
|
|
|
|
for key in ("frame_controls", "actual_camera_controls", "startup_camera_controls"):
|
|
|
|
|
if key not in stream_meta and isinstance(meta.get(key), dict):
|
|
|
|
|
stream_meta[key] = copy.deepcopy(meta[key])
|
|
|
|
|
|
|
|
|
|
return stream_meta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_preview_core_cached(sensor_width: int, sensor_height: int, bayer: str, calib_path: str):
|
|
|
|
|
"""
|
|
|
|
|
Reutiliza o RawProcessorCore entre amostras para evitar recarregar flat-field
|
|
|
|
|
e recriar caches de gain/remap toda hora.
|
|
|
|
|
"""
|
|
|
|
|
if not _HAS_RAW_PROCESSOR_CORE:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
key = (
|
|
|
|
|
int(sensor_width),
|
|
|
|
|
int(sensor_height),
|
|
|
|
|
str(bayer).upper(),
|
|
|
|
|
str(Path(calib_path).resolve()) if calib_path else "",
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
core = _CORE_PREVIEW_CACHE.get(key)
|
|
|
|
|
if core is not None:
|
|
|
|
|
return core
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
core = RawProcessorCore(
|
|
|
|
|
sensor_width=int(sensor_width),
|
|
|
|
|
sensor_height=int(sensor_height),
|
|
|
|
|
bayer_pattern=str(bayer).upper(),
|
|
|
|
|
calibration_json_path=calib_path,
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
_CORE_PREVIEW_CACHE[key] = core
|
|
|
|
|
return core
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def build_core_preview_from_augmented_raws(meta: Dict[str, Any], cameras: Dict[str, CameraBin]) -> Optional[np.ndarray]:
|
|
|
|
|
"""
|
|
|
|
|
Gera o preview salvo passando os bins augmentados pelo RawProcessorCore,
|
|
|
|
|
igual ao check_saved_files faz para montar o MULTISPEC final.
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
Saída: RGB uint8 do tensor final [R,G,B], pronto para salvar com PIL.
|
|
|
|
|
"""
|
|
|
|
|
if not _HAS_RAW_PROCESSOR_CORE:
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
required = ("rgb", "re", "nir")
|
|
|
|
|
if any(r not in cameras or cameras[r].data is None for r in required):
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
sensor_width = int(meta.get("sensor_width") or cameras["rgb"].width)
|
|
|
|
|
sensor_height = int(meta.get("sensor_height") or cameras["rgb"].height)
|
|
|
|
|
bayer = str(meta.get("bayer_pattern") or cameras["rgb"].bayer_pattern or DEFAULT_BAYER_PATTERN)
|
|
|
|
|
calib_path = _resolve_module_params_path(meta)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if not calib_path:
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
frame = {}
|
|
|
|
|
for role in required:
|
|
|
|
|
cam = cameras[role]
|
|
|
|
|
frame[cam.cam_key] = pack_raw10_packed_array(cam.data)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
processing_meta = _build_processing_meta_for_core(meta, cameras)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
core = get_preview_core_cached(
|
|
|
|
|
sensor_width=sensor_width,
|
|
|
|
|
sensor_height=sensor_height,
|
|
|
|
|
bayer=bayer,
|
|
|
|
|
calib_path=calib_path,
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
if core is None:
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
tensor = core.build_infer_tensor_from_stream(frame, processing_meta, 5)
|
|
|
|
|
if tensor is None or not isinstance(tensor, np.ndarray) or tensor.ndim != 3 or tensor.shape[0] < 3:
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
rgb_hwc = np.transpose(tensor[:3].astype(np.float32), (1, 2, 0))
|
|
|
|
|
rgb8 = np.clip(rgb_hwc * 255.0, 0, 255).astype(np.uint8)
|
|
|
|
|
return rgb8
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def build_beauty_preview_from_augmented_raws(cameras: Dict[str, CameraBin]) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
Fallback visual a partir dos RAWs augmentados, sem passar pelo core.
|
|
|
|
|
Preferimos build_core_preview_from_augmented_raws sempre que possível.
|
|
|
|
|
"""
|
|
|
|
|
rgb_cam = cameras.get("rgb")
|
|
|
|
|
if rgb_cam is None or rgb_cam.data is None:
|
|
|
|
|
raise RuntimeError("Sem RAW RGB para gerar preview.")
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
return debayer_raw10_to_rgb8(rgb_cam.data, rgb_cam.bayer_pattern)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
|
|
|
|
|
def save_preview_from_augmented_raws(path: Path, cameras: Dict[str, CameraBin], meta: Optional[Dict[str, Any]] = None) -> str:
|
|
|
|
|
ensure_dir(path.parent)
|
|
|
|
|
|
|
|
|
|
method = "fallback_cam_a_debayer_preview"
|
|
|
|
|
rgb8 = None
|
|
|
|
|
|
|
|
|
|
if meta is not None:
|
|
|
|
|
try:
|
|
|
|
|
rgb8 = build_core_preview_from_augmented_raws(meta, cameras)
|
|
|
|
|
if rgb8 is not None:
|
|
|
|
|
method = "raw_processor_core_multispec_rgb_final"
|
2026-05-15 10:52:30 +00:00
|
|
|
except Exception as e:
|
2026-05-22 22:32:11 +00:00
|
|
|
print(f"[WARN] Preview via RawProcessorCore falhou em {path.name}: {e}. Usando fallback CAM_A.")
|
|
|
|
|
rgb8 = None
|
|
|
|
|
|
|
|
|
|
if rgb8 is None:
|
|
|
|
|
rgb8 = build_beauty_preview_from_augmented_raws(cameras)
|
|
|
|
|
|
|
|
|
|
Image.fromarray(rgb8).save(path)
|
|
|
|
|
return method
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Augmentation
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
def sample_aug_params(rng: random.Random, seed_value: int) -> AugParams:
|
|
|
|
|
# Geometria conservadora.
|
|
|
|
|
perspective = rng.random() < 0.15
|
|
|
|
|
blur_enabled = rng.random() < 0.15
|
|
|
|
|
|
|
|
|
|
return AugParams(
|
|
|
|
|
seed=seed_value,
|
|
|
|
|
flip_h=rng.random() < 0.50,
|
|
|
|
|
shift_x_frac=rng.uniform(-0.015, 0.015),
|
|
|
|
|
shift_y_frac=rng.uniform(-0.015, 0.015),
|
|
|
|
|
scale=rng.uniform(0.92, 1.08),
|
|
|
|
|
rotate_deg=rng.uniform(-4.0, 4.0),
|
|
|
|
|
perspective=perspective,
|
|
|
|
|
perspective_strength=rng.uniform(0.002, 0.012) if perspective else 0.0,
|
|
|
|
|
exposure_mult=rng.uniform(0.75, 1.30),
|
|
|
|
|
gamma=rng.uniform(0.92, 1.08),
|
|
|
|
|
rgb_channel_gain=(
|
|
|
|
|
rng.uniform(0.92, 1.08),
|
|
|
|
|
rng.uniform(0.92, 1.08),
|
|
|
|
|
rng.uniform(0.92, 1.08),
|
|
|
|
|
),
|
|
|
|
|
re_gain=rng.uniform(0.85, 1.20),
|
|
|
|
|
nir_gain=rng.uniform(0.85, 1.20),
|
|
|
|
|
shadow_enabled=rng.random() < 0.25,
|
|
|
|
|
shadow_strength=rng.uniform(0.12, 0.35),
|
|
|
|
|
shadow_angle_deg=rng.uniform(0.0, 180.0),
|
|
|
|
|
highlight_enabled=rng.random() < 0.15,
|
|
|
|
|
highlight_strength=rng.uniform(0.05, 0.18),
|
|
|
|
|
noise_enabled=rng.random() < 0.30,
|
|
|
|
|
noise_sigma_dn=rng.uniform(1.5, 6.0),
|
|
|
|
|
blur_enabled=blur_enabled,
|
|
|
|
|
blur_kernel=3 if blur_enabled else 1,
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
|
|
|
|
|
def build_affine_matrix(width: int, height: int, p: AugParams) -> np.ndarray:
|
|
|
|
|
cx = width * 0.5
|
|
|
|
|
cy = height * 0.5
|
|
|
|
|
M = cv2.getRotationMatrix2D((cx, cy), p.rotate_deg, p.scale)
|
|
|
|
|
M[0, 2] += p.shift_x_frac * width
|
|
|
|
|
M[1, 2] += p.shift_y_frac * height
|
|
|
|
|
return M
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def warp_image(img: np.ndarray, M: np.ndarray, out_size: Tuple[int, int], is_mask: bool) -> np.ndarray:
|
|
|
|
|
interp = cv2.INTER_NEAREST if is_mask else cv2.INTER_LINEAR
|
|
|
|
|
return cv2.warpAffine(
|
|
|
|
|
img,
|
|
|
|
|
M,
|
|
|
|
|
out_size,
|
|
|
|
|
flags=interp,
|
|
|
|
|
borderMode=cv2.BORDER_REFLECT_101,
|
2026-05-15 10:52:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def build_perspective_matrix(width: int, height: int, p: AugParams, rng: random.Random) -> Optional[np.ndarray]:
|
|
|
|
|
if not p.perspective:
|
|
|
|
|
return None
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
s = p.perspective_strength
|
|
|
|
|
dx = width * s
|
|
|
|
|
dy = height * s
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
src = np.float32([
|
|
|
|
|
[0, 0],
|
|
|
|
|
[width - 1, 0],
|
|
|
|
|
[width - 1, height - 1],
|
|
|
|
|
[0, height - 1],
|
|
|
|
|
])
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
dst = src + np.float32([
|
|
|
|
|
[rng.uniform(-dx, dx), rng.uniform(-dy, dy)],
|
|
|
|
|
[rng.uniform(-dx, dx), rng.uniform(-dy, dy)],
|
|
|
|
|
[rng.uniform(-dx, dx), rng.uniform(-dy, dy)],
|
|
|
|
|
[rng.uniform(-dx, dx), rng.uniform(-dy, dy)],
|
|
|
|
|
])
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
return cv2.getPerspectiveTransform(src, dst)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def warp_perspective(img: np.ndarray, H: np.ndarray, out_size: Tuple[int, int], is_mask: bool) -> np.ndarray:
|
|
|
|
|
interp = cv2.INTER_NEAREST if is_mask else cv2.INTER_LINEAR
|
|
|
|
|
return cv2.warpPerspective(
|
|
|
|
|
img,
|
|
|
|
|
H,
|
|
|
|
|
out_size,
|
|
|
|
|
flags=interp,
|
|
|
|
|
borderMode=cv2.BORDER_REFLECT_101,
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
def apply_geometry_raw(raw: np.ndarray, p: AugParams, rng: random.Random) -> np.ndarray:
|
|
|
|
|
h, w = raw.shape[:2]
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
out = raw
|
|
|
|
|
if p.flip_h:
|
|
|
|
|
out = cv2.flip(out, 1)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
M = build_affine_matrix(w, h, p)
|
|
|
|
|
out = warp_image(out, M, (w, h), is_mask=False)
|
|
|
|
|
|
|
|
|
|
H = build_perspective_matrix(w, h, p, rng)
|
|
|
|
|
if H is not None:
|
|
|
|
|
out = warp_perspective(out, H, (w, h), is_mask=False)
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_geometry_mask(mask: np.ndarray, p: AugParams, rng: random.Random) -> np.ndarray:
|
|
|
|
|
h, w = mask.shape[:2]
|
|
|
|
|
|
|
|
|
|
out = mask
|
|
|
|
|
if p.flip_h:
|
|
|
|
|
out = cv2.flip(out, 1)
|
|
|
|
|
|
|
|
|
|
M = build_affine_matrix(w, h, p)
|
|
|
|
|
out = warp_image(out, M, (w, h), is_mask=True)
|
|
|
|
|
|
|
|
|
|
H = build_perspective_matrix(w, h, p, rng)
|
|
|
|
|
if H is not None:
|
|
|
|
|
out = warp_perspective(out, H, (w, h), is_mask=True)
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_spatial_light_map(shape: Tuple[int, int], p: AugParams) -> np.ndarray:
|
|
|
|
|
h, w = shape
|
|
|
|
|
yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
|
|
|
|
|
xx = (xx / max(w - 1, 1)) - 0.5
|
|
|
|
|
yy = (yy / max(h - 1, 1)) - 0.5
|
|
|
|
|
|
|
|
|
|
light = np.ones((h, w), dtype=np.float32)
|
|
|
|
|
|
|
|
|
|
if p.shadow_enabled:
|
|
|
|
|
theta = math.radians(p.shadow_angle_deg)
|
|
|
|
|
direction = math.cos(theta) * xx + math.sin(theta) * yy
|
|
|
|
|
direction = (direction - direction.min()) / max(direction.max() - direction.min(), 1e-6)
|
|
|
|
|
shadow = 1.0 - p.shadow_strength * direction
|
|
|
|
|
light *= shadow
|
|
|
|
|
|
|
|
|
|
if p.highlight_enabled:
|
|
|
|
|
# Mancha larga e suave, simulando região de sol/reflexo.
|
|
|
|
|
cx = np.random.uniform(-0.25, 0.25)
|
|
|
|
|
cy = np.random.uniform(-0.25, 0.25)
|
|
|
|
|
sigma = np.random.uniform(0.20, 0.42)
|
|
|
|
|
d2 = (xx - cx) ** 2 + (yy - cy) ** 2
|
|
|
|
|
blob = np.exp(-d2 / (2 * sigma * sigma))
|
|
|
|
|
light *= 1.0 + p.highlight_strength * blob
|
|
|
|
|
|
|
|
|
|
return np.clip(light, 0.50, 1.45).astype(np.float32)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_gamma_raw(raw: np.ndarray, gamma: float) -> np.ndarray:
|
|
|
|
|
if abs(gamma - 1.0) < 1e-3:
|
|
|
|
|
return raw
|
|
|
|
|
x = np.clip(raw.astype(np.float32) / 1023.0, 0, 1)
|
|
|
|
|
x = np.power(x, gamma)
|
|
|
|
|
return x * 1023.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_bayer_channel_gains(raw: np.ndarray, gains_rgb: Tuple[float, float, float], pattern: str) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
Aplica ganhos aproximados por canal em mosaico Bayer.
|
|
|
|
|
Para preview/treino, isso simula variação de balanço/cor no sensor RGB.
|
|
|
|
|
"""
|
|
|
|
|
out = raw.astype(np.float32).copy()
|
|
|
|
|
r_gain, g_gain, b_gain = gains_rgb
|
|
|
|
|
pat = (pattern or DEFAULT_BAYER_PATTERN).upper()
|
|
|
|
|
|
|
|
|
|
if pat == "RGGB":
|
|
|
|
|
out[0::2, 0::2] *= r_gain
|
|
|
|
|
out[0::2, 1::2] *= g_gain
|
|
|
|
|
out[1::2, 0::2] *= g_gain
|
|
|
|
|
out[1::2, 1::2] *= b_gain
|
|
|
|
|
elif pat == "BGGR":
|
|
|
|
|
out[0::2, 0::2] *= b_gain
|
|
|
|
|
out[0::2, 1::2] *= g_gain
|
|
|
|
|
out[1::2, 0::2] *= g_gain
|
|
|
|
|
out[1::2, 1::2] *= r_gain
|
|
|
|
|
elif pat == "GRBG":
|
|
|
|
|
out[0::2, 0::2] *= g_gain
|
|
|
|
|
out[0::2, 1::2] *= r_gain
|
|
|
|
|
out[1::2, 0::2] *= b_gain
|
|
|
|
|
out[1::2, 1::2] *= g_gain
|
|
|
|
|
elif pat == "GBRG":
|
|
|
|
|
out[0::2, 0::2] *= g_gain
|
|
|
|
|
out[0::2, 1::2] *= b_gain
|
|
|
|
|
out[1::2, 0::2] *= r_gain
|
|
|
|
|
out[1::2, 1::2] *= g_gain
|
|
|
|
|
else:
|
|
|
|
|
out *= float(np.mean(gains_rgb))
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_radiometry_rgb01(rgb: np.ndarray, p: AugParams, rng_np: np.random.Generator, light_map_cache: Dict[Tuple[int, int], np.ndarray]) -> np.ndarray:
|
|
|
|
|
"""
|
|
|
|
|
Radiometria para RGB já demosaicado.
|
|
|
|
|
Evita mexer no mosaico Bayer diretamente.
|
|
|
|
|
"""
|
|
|
|
|
h, w = rgb.shape[:2]
|
|
|
|
|
x = np.clip(rgb.astype(np.float32), 0.0, 1.0)
|
|
|
|
|
|
|
|
|
|
x *= np.float32(p.exposure_mult)
|
|
|
|
|
|
|
|
|
|
gains = np.array(p.rgb_channel_gain, dtype=np.float32).reshape(1, 1, 3)
|
|
|
|
|
x *= gains
|
|
|
|
|
|
|
|
|
|
key = (h, w)
|
|
|
|
|
if key not in light_map_cache:
|
|
|
|
|
light_map_cache[key] = make_spatial_light_map((h, w), p)
|
|
|
|
|
x *= light_map_cache[key][:, :, None]
|
|
|
|
|
|
|
|
|
|
if abs(p.gamma - 1.0) > 1e-3:
|
|
|
|
|
x = np.power(np.clip(x, 0.0, 1.0), p.gamma)
|
|
|
|
|
|
|
|
|
|
if p.blur_enabled and p.blur_kernel >= 3:
|
|
|
|
|
x = cv2.GaussianBlur(x, (p.blur_kernel, p.blur_kernel), 0)
|
|
|
|
|
|
|
|
|
|
if p.noise_enabled:
|
|
|
|
|
# Converte sigma DN RAW10 para escala 0..1.
|
|
|
|
|
sigma01 = float(p.noise_sigma_dn) / 1023.0
|
|
|
|
|
noise = rng_np.normal(0.0, sigma01, size=x.shape).astype(np.float32)
|
|
|
|
|
x += noise
|
|
|
|
|
|
|
|
|
|
return np.clip(x, 0.0, 1.0).astype(np.float32, copy=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_radiometry_raw(raw: np.ndarray, role: str, p: AugParams, cam: CameraBin, rng_np: np.random.Generator, light_map_cache: Dict[Tuple[int, int], np.ndarray]) -> np.ndarray:
|
|
|
|
|
h, w = raw.shape[:2]
|
|
|
|
|
x = raw.astype(np.float32)
|
|
|
|
|
|
|
|
|
|
# Base global de exposição: muda todo mundo junto.
|
|
|
|
|
x *= p.exposure_mult
|
|
|
|
|
|
|
|
|
|
# Variação por papel espectral/canal.
|
|
|
|
|
# OBS: RGB não deve passar por aqui, porque RGB Bayer precisa ser demosaicado
|
|
|
|
|
# antes de qualquer warp/radiometria por canal.
|
|
|
|
|
if role == "re":
|
|
|
|
|
x *= p.re_gain
|
|
|
|
|
elif role == "nir":
|
|
|
|
|
x *= p.nir_gain
|
|
|
|
|
|
|
|
|
|
# Mesmo mapa espacial de luz por tamanho, para manter coerência física.
|
|
|
|
|
key = (h, w)
|
|
|
|
|
if key not in light_map_cache:
|
|
|
|
|
light_map_cache[key] = make_spatial_light_map((h, w), p)
|
|
|
|
|
x *= light_map_cache[key]
|
|
|
|
|
|
|
|
|
|
# Gamma leve, opcional. Em RAW científico puro, gamma seria discutível.
|
|
|
|
|
# Mantemos bem pequeno para simular resposta/exposição não ideal.
|
|
|
|
|
x = apply_gamma_raw(x, p.gamma)
|
|
|
|
|
|
|
|
|
|
if p.blur_enabled and p.blur_kernel >= 3:
|
|
|
|
|
x = cv2.GaussianBlur(x, (p.blur_kernel, p.blur_kernel), 0)
|
|
|
|
|
|
|
|
|
|
if p.noise_enabled:
|
|
|
|
|
noise = rng_np.normal(0.0, p.noise_sigma_dn, size=x.shape).astype(np.float32)
|
|
|
|
|
x += noise
|
|
|
|
|
|
|
|
|
|
return np.clip(x, 0, 1023).astype(np.uint16)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# Processamento de amostra/grupo
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
def validate_mask_colors(mask_before: np.ndarray, mask_after: np.ndarray, name: str) -> None:
|
|
|
|
|
if mask_before.ndim == 2 or mask_after.ndim == 2:
|
|
|
|
|
before_colors = int(np.unique(mask_before.reshape(-1)).size)
|
|
|
|
|
after_colors = int(np.unique(mask_after.reshape(-1)).size)
|
|
|
|
|
else:
|
|
|
|
|
before_colors = len(set(map(tuple, mask_before.reshape(-1, mask_before.shape[2]))))
|
|
|
|
|
after_colors = len(set(map(tuple, mask_after.reshape(-1, mask_after.shape[2]))))
|
|
|
|
|
|
|
|
|
|
if after_colors > max(before_colors * 3, 64):
|
|
|
|
|
print(
|
|
|
|
|
f"[WARN] {name}: máscara ganhou muitas cores. "
|
|
|
|
|
f"antes={before_colors}, depois={after_colors}. Confira interpolação NEAREST."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_aug_meta(
|
|
|
|
|
meta: Dict[str, Any],
|
|
|
|
|
out_path: Path,
|
|
|
|
|
base: str,
|
|
|
|
|
new_base: str,
|
|
|
|
|
params: AugParams,
|
|
|
|
|
cameras: Dict[str, CameraBin],
|
|
|
|
|
preview_method: Optional[str] = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
out = copy.deepcopy(meta)
|
|
|
|
|
out["synthetic"] = True
|
|
|
|
|
out["augmentation"] = {
|
|
|
|
|
"script": "_5_augmentation_raw_oak.py",
|
|
|
|
|
"parent_base": base,
|
|
|
|
|
"new_base": new_base,
|
|
|
|
|
"params": asdict(params),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Deixa o JSON compatível com o check_saved_files / validador.
|
|
|
|
|
out["saved_payload_type"] = "raw_native_multi"
|
|
|
|
|
out["saved_preview_method"] = preview_method or "unknown"
|
|
|
|
|
out["saved_payload_paths"] = {}
|
|
|
|
|
out["saved_payload_shapes"] = {}
|
|
|
|
|
out["saved_payload_dtypes"] = {}
|
|
|
|
|
|
|
|
|
|
stream_meta = copy.deepcopy(out.get("stream_meta") if isinstance(out.get("stream_meta"), dict) else {})
|
|
|
|
|
camera_info = stream_meta.get("camera_info") if isinstance(stream_meta.get("camera_info"), dict) else {}
|
|
|
|
|
camera_frames = stream_meta.get("camera_frames") if isinstance(stream_meta.get("camera_frames"), dict) else {}
|
|
|
|
|
|
|
|
|
|
out["augmented_bins"] = {}
|
|
|
|
|
|
|
|
|
|
for role, cam in cameras.items():
|
|
|
|
|
packed_w = int(math.ceil(int(cam.width) * 10 / 8))
|
|
|
|
|
fname = f"{new_base}_{cam.cam_key}.bin"
|
|
|
|
|
|
|
|
|
|
out["saved_payload_paths"][cam.cam_key] = fname
|
|
|
|
|
out["saved_payload_shapes"][cam.cam_key] = [int(cam.height), int(packed_w)]
|
|
|
|
|
out["saved_payload_dtypes"][cam.cam_key] = "uint8"
|
|
|
|
|
|
|
|
|
|
cam_info = dict(camera_info.get(cam.cam_key, {}) or {})
|
|
|
|
|
cam_info.update({
|
|
|
|
|
"role": role,
|
|
|
|
|
"width": int(cam.width),
|
|
|
|
|
"height": int(cam.height),
|
|
|
|
|
"bit_depth": int(cam.bit_depth),
|
|
|
|
|
"raw_format": cam.raw_format or DEFAULT_RAW_FORMAT,
|
|
|
|
|
"packed": True,
|
|
|
|
|
"channels": 1,
|
|
|
|
|
"bayer_pattern": cam.bayer_pattern or DEFAULT_BAYER_PATTERN,
|
|
|
|
|
})
|
|
|
|
|
camera_info[cam.cam_key] = cam_info
|
|
|
|
|
camera_frames[cam.cam_key] = dict(cam_info)
|
|
|
|
|
|
|
|
|
|
out["augmented_bins"][role] = {
|
|
|
|
|
"cam_key": cam.cam_key,
|
|
|
|
|
"filename": fname,
|
|
|
|
|
"role": role,
|
|
|
|
|
"width": int(cam.width),
|
|
|
|
|
"height": int(cam.height),
|
|
|
|
|
"bit_depth": int(cam.bit_depth),
|
|
|
|
|
"raw_format": cam.raw_format,
|
|
|
|
|
"bayer_pattern": cam.bayer_pattern,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stream_meta["frame_type"] = "RAW_BRUTO"
|
|
|
|
|
stream_meta["camera_info"] = camera_info
|
|
|
|
|
stream_meta["camera_frames"] = camera_frames
|
|
|
|
|
stream_meta["payload_sources"] = [cameras[r].cam_key for r in ("rgb", "re", "nir") if r in cameras]
|
|
|
|
|
out["stream_meta"] = stream_meta
|
|
|
|
|
|
|
|
|
|
save_json(out_path, out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_one_sample(
|
|
|
|
|
base: str,
|
|
|
|
|
group_name: str,
|
|
|
|
|
src_group: Path,
|
|
|
|
|
dst_group: Path,
|
|
|
|
|
mask_path: Path,
|
|
|
|
|
meta_path: Path,
|
|
|
|
|
copies: int,
|
|
|
|
|
rng_global: random.Random,
|
|
|
|
|
dry_run: bool = False,
|
|
|
|
|
) -> Tuple[int, int]:
|
|
|
|
|
bins_dir = src_group / "bins"
|
|
|
|
|
meta = load_json(meta_path)
|
|
|
|
|
cameras = discover_sample_cameras(meta, bins_dir, base)
|
|
|
|
|
|
|
|
|
|
required = ["rgb", "re", "nir"]
|
|
|
|
|
missing = [r for r in required if r not in cameras]
|
|
|
|
|
if missing:
|
|
|
|
|
print(f"[WARN] [{group_name}] {base}: câmeras ausentes {missing}. Pulando.")
|
|
|
|
|
return 0, 1
|
|
|
|
|
|
|
|
|
|
mask = load_mask(mask_path)
|
|
|
|
|
|
|
|
|
|
# Carrega todos os RAWs uma vez.
|
|
|
|
|
for role, cam in cameras.items():
|
|
|
|
|
cam.data = read_raw_bin(cam.path, cam.width, cam.height, cam.raw_format)
|
|
|
|
|
|
|
|
|
|
generated = 0
|
|
|
|
|
errors = 0
|
|
|
|
|
|
|
|
|
|
out_bins = dst_group / "bins"
|
|
|
|
|
out_masks = dst_group / "masks"
|
|
|
|
|
out_metas = dst_group / "metas"
|
|
|
|
|
out_previews = dst_group / "previews"
|
|
|
|
|
|
|
|
|
|
for i in range(copies):
|
|
|
|
|
seed_value = rng_global.randint(0, 2**31 - 1)
|
|
|
|
|
rng = random.Random(seed_value)
|
|
|
|
|
rng_np = np.random.default_rng(seed_value)
|
|
|
|
|
params = sample_aug_params(rng, seed_value)
|
|
|
|
|
new_base = f"{base}_aug_{i:02d}"
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if dry_run:
|
|
|
|
|
generated += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Mesma geometria para mask e raws.
|
|
|
|
|
mask_aug = apply_geometry_mask(mask, params, random.Random(seed_value + 1000))
|
|
|
|
|
validate_mask_colors(mask, mask_aug, f"{new_base}_mask")
|
|
|
|
|
|
|
|
|
|
light_map_cache: Dict[Tuple[int, int], np.ndarray] = {}
|
|
|
|
|
cams_out: Dict[str, CameraBin] = {}
|
|
|
|
|
|
|
|
|
|
for role in required:
|
|
|
|
|
cam = cameras[role]
|
|
|
|
|
assert cam.data is not None
|
|
|
|
|
|
|
|
|
|
if role == "rgb":
|
|
|
|
|
# RGB RAW é mosaico Bayer. Não podemos aplicar warp direto nele,
|
|
|
|
|
# porque isso mistura pixels R/G/B antes do demosaic e cria ruído colorido.
|
|
|
|
|
# Fluxo correto offline:
|
|
|
|
|
# RAW Bayer -> RGB linear -> geometria/radiometria -> mosaico Bayer -> RAW10
|
|
|
|
|
rgb01 = demosaic_raw10_to_rgb01(
|
|
|
|
|
cam.data,
|
|
|
|
|
bayer_pattern=cam.bayer_pattern,
|
|
|
|
|
algorithm="ea",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
rgb_geom = rgb01
|
|
|
|
|
if params.flip_h:
|
|
|
|
|
rgb_geom = cv2.flip(rgb_geom, 1)
|
|
|
|
|
|
|
|
|
|
h_rgb, w_rgb = rgb_geom.shape[:2]
|
|
|
|
|
M_rgb = build_affine_matrix(w_rgb, h_rgb, params)
|
|
|
|
|
rgb_geom = warp_image(rgb_geom, M_rgb, (w_rgb, h_rgb), is_mask=False)
|
|
|
|
|
|
|
|
|
|
H_rgb = build_perspective_matrix(w_rgb, h_rgb, params, random.Random(seed_value + 1000))
|
|
|
|
|
if H_rgb is not None:
|
|
|
|
|
rgb_geom = warp_perspective(rgb_geom, H_rgb, (w_rgb, h_rgb), is_mask=False)
|
|
|
|
|
|
|
|
|
|
rgb_aug = apply_radiometry_rgb01(
|
|
|
|
|
rgb_geom,
|
|
|
|
|
params,
|
|
|
|
|
rng_np,
|
|
|
|
|
light_map_cache,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
raw_aug = remosaic_rgb01_to_bayer_raw10(
|
|
|
|
|
rgb_aug,
|
|
|
|
|
bayer_pattern=cam.bayer_pattern,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
raw_geom = apply_geometry_raw(cam.data, params, random.Random(seed_value + 1000))
|
|
|
|
|
raw_aug = apply_radiometry_raw(raw_geom, role, params, cam, rng_np, light_map_cache)
|
|
|
|
|
|
|
|
|
|
out_cam = copy.deepcopy(cam)
|
|
|
|
|
out_cam.path = out_bins / f"{new_base}_{cam.cam_key}.bin"
|
|
|
|
|
out_cam.data = raw_aug
|
|
|
|
|
cams_out[role] = out_cam
|
|
|
|
|
|
|
|
|
|
write_raw_bin(out_cam.path, raw_aug, cam.raw_format)
|
|
|
|
|
|
|
|
|
|
# Máscara com mesmo nome-base.
|
|
|
|
|
mask_ext = mask_path.suffix.lower() if mask_path.suffix.lower() in IMG_EXTS else ".png"
|
|
|
|
|
save_mask(out_masks / f"{new_base}{mask_ext}", mask_aug)
|
|
|
|
|
|
|
|
|
|
# Preview visual usando o mesmo caminho do validador/check_saved_files:
|
|
|
|
|
# bins augmentados -> RawProcessorCore -> MULTISPEC final -> RGB final.
|
|
|
|
|
# Se o core falhar, cai no fallback CAM_A e avisa no console.
|
|
|
|
|
preview_method = save_preview_from_augmented_raws(
|
|
|
|
|
out_previews / f"{new_base}.png",
|
|
|
|
|
cams_out,
|
|
|
|
|
meta=meta,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Meta novo, com mesmo stem do preview/mask para manter o layout dataset limpo.
|
|
|
|
|
save_aug_meta(
|
|
|
|
|
meta=meta,
|
|
|
|
|
out_path=out_metas / f"{new_base}.json",
|
|
|
|
|
base=base,
|
|
|
|
|
new_base=new_base,
|
|
|
|
|
params=params,
|
|
|
|
|
cameras=cams_out,
|
|
|
|
|
preview_method=preview_method,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
generated += 1
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
errors += 1
|
|
|
|
|
print(f"[ERRO] [{group_name}] {new_base}: {e}")
|
|
|
|
|
|
|
|
|
|
return generated, errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_group_copies(text: Optional[str]) -> Dict[str, int]:
|
|
|
|
|
"""
|
|
|
|
|
Ex: chao:1,chao_cana:3,chao_erva:3,chao_cana_erva:5
|
|
|
|
|
"""
|
|
|
|
|
out: Dict[str, int] = {}
|
|
|
|
|
if not text:
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
for part in text.split(","):
|
|
|
|
|
part = part.strip()
|
|
|
|
|
if not part:
|
|
|
|
|
continue
|
|
|
|
|
if ":" not in part:
|
|
|
|
|
raise ValueError(f"group-copies inválido: {part}. Use grupo:N")
|
|
|
|
|
g, n = part.split(":", 1)
|
|
|
|
|
out[g.strip()] = int(n.strip())
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_group(
|
|
|
|
|
src_root: Path,
|
|
|
|
|
dst_root: Path,
|
|
|
|
|
group_name: str,
|
|
|
|
|
copies: int,
|
|
|
|
|
limit: Optional[int],
|
|
|
|
|
seed: int,
|
|
|
|
|
dry_run: bool,
|
|
|
|
|
) -> Tuple[int, int, int]:
|
|
|
|
|
src_group = src_root / group_name
|
|
|
|
|
dst_group = dst_root / group_name
|
|
|
|
|
|
|
|
|
|
mask_map = map_files_by_base(src_group / "masks", IMG_EXTS)
|
|
|
|
|
meta_map = map_files_by_base(src_group / "metas", META_EXTS)
|
|
|
|
|
|
|
|
|
|
bases = sorted(set(mask_map.keys()) & set(meta_map.keys()))
|
|
|
|
|
|
|
|
|
|
if limit is not None and limit > 0 and limit < len(bases):
|
|
|
|
|
rng_select = random.Random(seed)
|
|
|
|
|
bases = sorted(rng_select.sample(bases, limit))
|
|
|
|
|
print(f"[INFO] [{group_name}] limit={limit}, amostras selecionadas={len(bases)}")
|
|
|
|
|
|
|
|
|
|
if not bases:
|
|
|
|
|
print(f"[WARN] [{group_name}] Nenhum par mask/meta encontrado.")
|
|
|
|
|
return 0, 0, 0
|
|
|
|
|
|
|
|
|
|
ensure_dir(dst_group / "bins")
|
|
|
|
|
ensure_dir(dst_group / "masks")
|
|
|
|
|
ensure_dir(dst_group / "metas")
|
|
|
|
|
ensure_dir(dst_group / "previews")
|
|
|
|
|
|
|
|
|
|
rng_global = random.Random(seed + abs(hash(group_name)) % 1000000)
|
|
|
|
|
|
|
|
|
|
total_generated = 0
|
|
|
|
|
total_errors = 0
|
|
|
|
|
|
|
|
|
|
for idx, base in enumerate(bases, start=1):
|
|
|
|
|
gen, err = process_one_sample(
|
|
|
|
|
base=base,
|
|
|
|
|
group_name=group_name,
|
|
|
|
|
src_group=src_group,
|
|
|
|
|
dst_group=dst_group,
|
|
|
|
|
mask_path=mask_map[base],
|
|
|
|
|
meta_path=meta_map[base],
|
2026-05-15 10:52:30 +00:00
|
|
|
copies=copies,
|
2026-05-22 22:32:11 +00:00
|
|
|
rng_global=rng_global,
|
|
|
|
|
dry_run=dry_run,
|
2026-05-15 10:52:30 +00:00
|
|
|
)
|
2026-05-22 22:32:11 +00:00
|
|
|
total_generated += gen
|
|
|
|
|
total_errors += err
|
|
|
|
|
|
|
|
|
|
if idx % 25 == 0 or idx == len(bases):
|
|
|
|
|
print(
|
|
|
|
|
f"[INFO] [{group_name}] {idx}/{len(bases)} amostras | "
|
|
|
|
|
f"gerados={total_generated} | erros={total_errors}"
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
print(
|
|
|
|
|
f"[OK] Grupo '{group_name}' concluído: "
|
|
|
|
|
f"originais={len(bases)} | copies={copies} | gerados={total_generated} | erros={total_errors}"
|
|
|
|
|
)
|
2026-05-15 10:52:30 +00:00
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
return len(bases), total_generated, total_errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
# CLI
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
dataset_base = Path("dataset")
|
|
|
|
|
|
|
|
|
|
default_src = dataset_base / "original" / "group"
|
|
|
|
|
if not default_src.exists():
|
|
|
|
|
# Compatibilidade com script antigo que usava "originals".
|
|
|
|
|
alt = dataset_base / "originals" / "group"
|
|
|
|
|
if alt.exists():
|
|
|
|
|
default_src = alt
|
|
|
|
|
|
|
|
|
|
default_dst = dataset_base / "augmented" / "group"
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
ap = argparse.ArgumentParser(
|
2026-05-22 22:32:11 +00:00
|
|
|
description="Augmentation multiespectral RAW10 packed para dataset OAK-FCC-3."
|
2026-05-15 10:52:30 +00:00
|
|
|
)
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
ap.add_argument("--copies", type=int, default=3, help="Cópias augmentadas por amostra, se --group-copies não sobrescrever.")
|
|
|
|
|
ap.add_argument("--group-copies", type=str, default=None, help="Cópias por grupo. Ex: chao:1,chao_cana:3,chao_erva:3,chao_cana_erva:5")
|
2026-05-15 10:52:30 +00:00
|
|
|
ap.add_argument("--groups", type=str, default=None, help="Lista de grupos separados por vírgula.")
|
2026-05-22 22:32:11 +00:00
|
|
|
ap.add_argument("--src-root", type=str, default=str(default_src), help="Raiz dos grupos originais.")
|
|
|
|
|
ap.add_argument("--dst-root", type=str, default=str(default_dst), help="Raiz dos grupos augmentados.")
|
|
|
|
|
ap.add_argument("--limit", type=int, default=None, help="Quantidade máxima de amostras originais por grupo.")
|
|
|
|
|
ap.add_argument("--seed", type=int, default=42, help="Seed geral para reproducibilidade.")
|
2026-05-15 10:52:30 +00:00
|
|
|
ap.add_argument("--clear-dst", action="store_true", help="Apaga dst-root antes de gerar.")
|
2026-05-22 22:32:11 +00:00
|
|
|
ap.add_argument("--dry-run", action="store_true", help="Simula sem salvar arquivos.")
|
2026-05-15 10:52:30 +00:00
|
|
|
|
|
|
|
|
args = ap.parse_args()
|
|
|
|
|
|
2026-05-22 22:32:11 +00:00
|
|
|
src_root = Path(args.src_root)
|
|
|
|
|
dst_root = Path(args.dst_root)
|
|
|
|
|
group_copies = parse_group_copies(args.group_copies)
|
|
|
|
|
|
|
|
|
|
print("==============================================")
|
|
|
|
|
print("Augmentation RAW OAK-FCC-3")
|
|
|
|
|
print(f"MODEL/DATASET : {dataset_base}")
|
|
|
|
|
print(f"SRC_ROOT : {src_root}")
|
|
|
|
|
print(f"DST_ROOT : {dst_root}")
|
|
|
|
|
print(f"copies : {args.copies}")
|
|
|
|
|
print(f"group_copies : {group_copies if group_copies else '{}'}")
|
|
|
|
|
print(f"limit : {args.limit}")
|
|
|
|
|
print(f"seed : {args.seed}")
|
|
|
|
|
print(f"dry_run : {args.dry_run}")
|
|
|
|
|
print("==============================================")
|
|
|
|
|
|
|
|
|
|
if not src_root.exists():
|
|
|
|
|
raise SystemExit(f"[ERRO] src-root não encontrado: {src_root}")
|
|
|
|
|
|
|
|
|
|
if args.clear_dst and not args.dry_run:
|
|
|
|
|
print(f"[INFO] Limpando destino: {dst_root}")
|
|
|
|
|
clear_dir(dst_root)
|
|
|
|
|
else:
|
|
|
|
|
ensure_dir(dst_root)
|
|
|
|
|
|
|
|
|
|
groups = list_groups(src_root)
|
|
|
|
|
|
|
|
|
|
if args.groups:
|
|
|
|
|
wanted = {g.strip() for g in args.groups.split(",") if g.strip()}
|
|
|
|
|
groups = [g for g in groups if g in wanted]
|
|
|
|
|
|
|
|
|
|
if not groups:
|
|
|
|
|
raise SystemExit("[WARN] Nenhum grupo válido encontrado.")
|
|
|
|
|
|
|
|
|
|
print(f"Grupos encontrados: {', '.join(groups)}")
|
|
|
|
|
|
|
|
|
|
total_originals = 0
|
|
|
|
|
total_generated = 0
|
|
|
|
|
total_errors = 0
|
|
|
|
|
|
|
|
|
|
for group_name in groups:
|
|
|
|
|
copies = group_copies.get(group_name, args.copies)
|
|
|
|
|
if copies <= 0:
|
|
|
|
|
print(f"[INFO] [{group_name}] copies={copies}. Pulando.")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
n_orig, n_gen, n_err = process_group(
|
|
|
|
|
src_root=src_root,
|
|
|
|
|
dst_root=dst_root,
|
|
|
|
|
group_name=group_name,
|
|
|
|
|
copies=copies,
|
|
|
|
|
limit=args.limit,
|
|
|
|
|
seed=args.seed,
|
|
|
|
|
dry_run=args.dry_run,
|
|
|
|
|
)
|
|
|
|
|
total_originals += n_orig
|
|
|
|
|
total_generated += n_gen
|
|
|
|
|
total_errors += n_err
|
|
|
|
|
|
|
|
|
|
print("\n==============================================")
|
|
|
|
|
print("Resumo final")
|
|
|
|
|
print(f"Originais processados : {total_originals}")
|
|
|
|
|
print(f"Amostras geradas : {total_generated}")
|
|
|
|
|
print(f"Erros/Pulos : {total_errors}")
|
|
|
|
|
print(f"Destino : {dst_root}")
|
|
|
|
|
print("==============================================")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|