ajustes na calibragem e visualizacao

This commit is contained in:
Diego Freitas 2026-05-06 13:27:38 -03:00
parent 1f871dc700
commit 6e166748cf
5 changed files with 490 additions and 93 deletions

View File

@ -175,19 +175,15 @@ class OakFcc3Client:
return self.svc.capture_frame(timeout=timeout)
def get_next_frame(self, timeout=2.0):
frame, meta, _ = self.get_next_decoded(
timeout=timeout,
update_radiometry=False,
)
frame, meta, _ = self.get_next_decoded(timeout=timeout)
return frame, meta
def get_next_decoded(self, timeout=2.0, update_radiometry=True):
def get_next_decoded(self, timeout=2.0):
raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout)
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
if update_radiometry:
self.update_radiometry(decoded, raw_meta)
self.update_radiometry(decoded, raw_meta)
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()

View File

@ -11,6 +11,7 @@ class RawProcessorCore:
self.sensor_width = sensor_width
self.sensor_height = sensor_height
self.bayer_pattern = bayer_pattern.upper()
self.fusion_config = {
"alignment_mode": "manual_affine",
"baseline_mm": 75.0,
@ -26,6 +27,7 @@ class RawProcessorCore:
"resize_after_crop": True,
"target_size": None,
}
self.rgb_calibration = {
"enabled": False,
"gains": {
@ -53,6 +55,14 @@ class RawProcessorCore:
self.flatfield_maps = {}
self.flatfield_loaded = False
self.radiometric_normalization_config = {
"enabled": False,
"method": "exposure_gain_reference",
"reference_controls": {},
"clip_output": False,
}
self.camera_settings = {}
if calibration_json_path:
self.load_fusion_config_json(calibration_json_path)
@ -469,10 +479,67 @@ class RawProcessorCore:
return decoded
def _decode_spectral_frame_to_float01(self, data, cam_meta):
arr = data
if arr.ndim == 3 and arr.shape[2] == 1:
arr = arr[:, :, 0]
bit_depth = int(cam_meta.get("bit_depth", 8))
raw_format = str(cam_meta.get("raw_format", "")).upper()
packed = bool(cam_meta.get("packed", False))
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
sensor_width = int(cam_meta.get("width", self.sensor_width))
sensor_height = int(cam_meta.get("height", arr.shape[0]))
packed_width = int(cam_meta.get("packed_width", 0) or 0)
looks_like_raw10_packed = (
arr.ndim == 2
and arr.dtype == np.uint8
and arr.shape[0] == sensor_height
and (
raw_format == "RAW10_PACKED"
or packed
or bit_depth == 10
or (packed_width > 0 and arr.shape[1] == packed_width and packed_width != sensor_width)
or arr.shape[1] == int(sensor_width * 10 / 8)
)
)
if looks_like_raw10_packed:
raw16 = self.unpack_raw10_packed(
arr,
sensor_width=sensor_width,
sensor_height=sensor_height,
)
max_val = float((1 << bit_depth) - 1)
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
# Caso preview/processado: mono já vem uint8 normal.
if arr.ndim == 2 and arr.dtype == np.uint8:
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
if arr.ndim == 2 and arr.dtype == np.uint16:
max_val = float((1 << bit_depth) - 1) if bit_depth > 0 and bit_depth <= 16 else 65535.0
return np.clip(arr.astype(np.float32) / max_val, 0.0, 1.0)
arr01 = arr.astype(np.float32)
if arr01.max() > 1.5:
arr01 /= 255.0
return np.clip(arr01, 0.0, 1.0)
def fuse_multispec_cameras(self, decoded, meta, channels_expected):
# Flat-field pertence ao espaço nativo de cada câmera.
# Por isso é aplicado antes de warp/homografia/crop comum.
decoded = self.apply_flatfield_to_decoded(decoded)
# 1) Coloca todos os frames na mesma escala de exposição/ganho de referência
decoded = self.normalize_decoded_by_capture_controls(decoded, meta)
# 2) Subtrai dark/offset no espaço individual de cada câmera
decoded = self.apply_dark_to_decoded(decoded)
# 3) Aplica o ganho espacial do flat field no espaço individual de cada câmera
decoded = self.apply_flat_gain_to_decoded(decoded)
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
if rgb_cam_id is None:
@ -801,7 +868,6 @@ class RawProcessorCore:
return arr.reshape(shape)
def save_rgb_u8_file(self, path: str, arr: np.ndarray):
arr.astype(np.uint8).tofile(path)
@ -862,23 +928,47 @@ class RawProcessorCore:
fusion = data.get("fusion_config")
if isinstance(fusion, dict):
self.fusion_config = self._merge_fusion_config(self.fusion_config, fusion)
self.fusion_config = self._merge_config(self.fusion_config, fusion)
else:
print("[WARN] JSON sem fusion_config. Mantendo config padrão.")
rgb_cal = data.get("rgb_calibration")
if isinstance(rgb_cal, dict):
self.rgb_calibration = self._merge_fusion_config(self.rgb_calibration, rgb_cal)
self.rgb_calibration = self._merge_config(self.rgb_calibration, rgb_cal)
flatfield = data.get("flatfield_config")
if isinstance(flatfield, dict):
self.flatfield_config = self._merge_fusion_config(self.flatfield_config, flatfield)
self.flatfield_config = self._merge_config(self.flatfield_config, flatfield)
self.load_flatfield_maps()
else:
self.flatfield_config["enabled"] = False
self.flatfield_maps = {}
self.flatfield_loaded = False
rad_norm_config = data.get("radiometric_normalization")
if isinstance(rad_norm_config, dict):
self.radiometric_normalization_config = self._merge_config(self.radiometric_normalization_config, rad_norm_config)
cam_set = data.get("camera_settings")
if isinstance(cam_set, dict):
self.camera_settings = self._merge_config(self.camera_settings, cam_set)
def _merge_config(self, default_cfg: dict, loaded_cfg: dict) -> dict:
cfg = json.loads(json.dumps(default_cfg))
def merge(dst: dict, src: dict):
for key, value in src.items():
if isinstance(value, dict) and isinstance(dst.get(key), dict):
merge(dst[key], value)
else:
dst[key] = value
if isinstance(loaded_cfg, dict):
merge(cfg, loaded_cfg)
return cfg
def _resolve_calibration_path(self, path: str) -> str:
if not path:
return ""
@ -966,12 +1056,16 @@ class RawProcessorCore:
return self.flatfield_loaded
def apply_flatfield_to_decoded(self, decoded: dict) -> dict:
def apply_dark_to_decoded(self, decoded: dict) -> dict:
cfg = self.flatfield_config or {}
if not cfg.get("enabled", False):
return decoded
subtract_dark = bool(cfg.get("subtract_dark", True))
if not subtract_dark:
return decoded
if not self.flatfield_loaded:
self.load_flatfield_maps()
@ -990,8 +1084,6 @@ class RawProcessorCore:
new_item = dict(item)
new_meta = dict(item.get("meta", {}) or {})
subtract_dark = bool(cfg.get("subtract_dark", False))
clip_output = bool(cfg.get("clip_output", True))
if role == "rgb":
if img.ndim != 3 or img.shape[2] < 3:
@ -999,11 +1091,70 @@ class RawProcessorCore:
continue
out = img.astype(np.float32).copy()
for idx, ch in enumerate(("R", "G", "B")):
out[:, :, idx] = self._apply_flatfield_single_channel(
out[:, :, idx] = self._subtract_dark_single_channel(
out[:, :, idx],
ch,
)
new_item["image"] = out
elif role in ("re", "nir"):
ch = "RE" if role == "re" else "NIR"
new_item["image"] = self._subtract_dark_single_channel(
img.astype(np.float32),
ch,
)
else:
corrected[cam_id] = item
continue
new_meta["dark_applied"] = True
new_item["meta"] = new_meta
corrected[cam_id] = new_item
return corrected
def apply_flat_gain_to_decoded(self, decoded: dict) -> dict:
cfg = self.flatfield_config or {}
if not cfg.get("enabled", False):
return decoded
if not self.flatfield_loaded:
self.load_flatfield_maps()
if not self.flatfield_loaded:
return decoded
clip_output = bool(cfg.get("clip_output", True))
corrected = {}
for cam_id, item in decoded.items():
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
img = item.get("image")
if img is None:
corrected[cam_id] = item
continue
new_item = dict(item)
new_meta = dict(item.get("meta", {}) or {})
if role == "rgb":
if img.ndim != 3 or img.shape[2] < 3:
corrected[cam_id] = item
continue
out = img.astype(np.float32).copy()
for idx, ch in enumerate(("R", "G", "B")):
out[:, :, idx] = self._apply_flat_gain_single_channel(
out[:, :, idx],
ch,
subtract_dark=subtract_dark,
clip_output=clip_output,
)
@ -1011,10 +1162,10 @@ class RawProcessorCore:
elif role in ("re", "nir"):
ch = "RE" if role == "re" else "NIR"
new_item["image"] = self._apply_flatfield_single_channel(
new_item["image"] = self._apply_flat_gain_single_channel(
img.astype(np.float32),
ch,
subtract_dark=subtract_dark,
clip_output=clip_output,
)
@ -1029,11 +1180,38 @@ class RawProcessorCore:
return corrected
def _apply_flatfield_single_channel(
def _subtract_dark_single_channel(
self,
img: np.ndarray,
channel_name: str,
) -> np.ndarray:
ch = str(channel_name).upper()
entry = self.flatfield_maps.get(ch)
if not entry:
return img.astype(np.float32, copy=False)
dark = entry.get("dark")
if dark is None:
return img.astype(np.float32, copy=False)
base = img.astype(np.float32)
dark = dark.astype(np.float32)
if dark.shape[:2] != base.shape[:2]:
dark = cv2.resize(
dark,
(base.shape[1], base.shape[0]),
interpolation=cv2.INTER_LINEAR,
)
out = np.maximum(base - dark, 0.0)
return out.astype(np.float32, copy=False)
def _apply_flat_gain_single_channel(
self,
img: np.ndarray,
channel_name: str,
subtract_dark: bool = False,
clip_output: bool = True,
) -> np.ndarray:
ch = str(channel_name).upper()
@ -1046,71 +1224,133 @@ class RawProcessorCore:
if gain is None:
return img.astype(np.float32, copy=False)
if gain.shape[:2] != img.shape[:2]:
base = img.astype(np.float32)
gain = gain.astype(np.float32)
if gain.shape[:2] != base.shape[:2]:
gain = cv2.resize(
gain.astype(np.float32),
(img.shape[1], img.shape[0]),
gain,
(base.shape[1], base.shape[0]),
interpolation=cv2.INTER_LINEAR,
)
base = img.astype(np.float32)
if subtract_dark and "dark" in entry:
dark = entry["dark"].astype(np.float32)
if dark.shape[:2] != img.shape[:2]:
dark = cv2.resize(dark, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_LINEAR)
base = np.maximum(base - dark, 0.0)
out = base * gain.astype(np.float32)
out = base * gain
if clip_output:
out = np.clip(out, 0.0, 1.0)
return out.astype(np.float32, copy=False)
def _merge_fusion_config(self, default_cfg: dict, loaded_cfg: dict) -> dict:
cfg = json.loads(json.dumps(default_cfg))
for key, value in loaded_cfg.items():
if isinstance(value, dict) and isinstance(cfg.get(key), dict):
cfg[key].update(value)
else:
cfg[key] = value
def normalize_decoded_by_capture_controls(self, decoded: dict, meta: dict | None = None) -> dict:
cfg = self.radiometric_normalization_config or {}
return cfg
if not cfg.get("enabled", False):
return decoded
def _decode_spectral_frame_to_float01(self, data, cam_meta):
arr = data
method = str(cfg.get("method", "exposure_gain_reference")).lower()
if method != "exposure_gain_reference":
return decoded
if arr.ndim == 3 and arr.shape[2] == 1:
arr = arr[:, :, 0]
controls = self._extract_actual_controls_from_meta(meta)
if not controls:
return decoded
bit_depth = int(cam_meta.get("bit_depth", 8))
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
reference_controls = cfg.get("reference_controls", {}) or {}
clip_output = bool(cfg.get("clip_output", False))
# Caso preview/processado: mono já vem uint8/uint16 normal.
if arr.ndim == 2 and arr.dtype == np.uint8:
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
normalized = {}
if arr.ndim == 2 and arr.dtype == np.uint16 and bit_depth != 10:
return np.clip(arr.astype(np.float32) / 65535.0, 0.0, 1.0)
for cam_id, item in decoded.items():
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
img = item.get("image")
if bit_depth == 10:
sensor_width = int(cam_meta.get("width", self.sensor_width))
sensor_height = int(cam_meta.get("height", arr.shape[0]))
if img is None or not role:
normalized[cam_id] = item
continue
raw16 = self.unpack_raw10_packed(
arr,
sensor_width=sensor_width,
sensor_height=sensor_height,
actual_ctrl = controls.get(role, {}) or {}
ref_ctrl = (
reference_controls.get(role)
or self.camera_settings.get(role)
or actual_ctrl
or {}
)
max_val = float((1 << bit_depth) - 1)
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
actual_factor = self._exposure_gain_factor(actual_ctrl)
ref_factor = self._exposure_gain_factor(ref_ctrl)
# fallback
arr01 = arr.astype(np.float32)
if arr01.max() > 1.5:
arr01 /= 255.0
if actual_factor <= 0 or ref_factor <= 0:
normalized[cam_id] = item
continue
return np.clip(arr01, 0.0, 1.0)
scale = ref_factor / actual_factor
new_item = dict(item)
new_meta = dict(item.get("meta", {}) or {})
out = img.astype(np.float32) * float(scale)
if clip_output:
out = np.clip(out, 0.0, 1.0)
new_meta["radiometric_normalization_applied"] = True
new_meta["radiometric_normalization_method"] = method
new_meta["radiometric_normalization_scale"] = float(scale)
new_meta["radiometric_actual_factor"] = float(actual_factor)
new_meta["radiometric_reference_factor"] = float(ref_factor)
new_item["image"] = out.astype(np.float32, copy=False)
new_item["meta"] = new_meta
normalized[cam_id] = new_item
return normalized
def _extract_actual_controls_from_meta(self, meta: dict | None) -> dict:
if not meta:
return {}
# Preferência: controles reais daquele frame.
controls = meta.get("actual_camera_controls")
if isinstance(controls, dict) and controls:
return controls
# Possíveis nomes alternativos.
controls = meta.get("camera_controls")
if isinstance(controls, dict) and controls:
return controls
controls = meta.get("startup_camera_controls")
if isinstance(controls, dict) and controls:
return controls
# Em alguns casos o JSON da captura pode ter stream_meta separado,
# mas se o meta recebido aqui for só stream_meta, talvez não tenha controles.
return {}
def _exposure_gain_factor(self, ctrl: dict) -> float:
if not isinstance(ctrl, dict):
return 0.0
exp = ctrl.get("exposure_time_us", None)
gain = ctrl.get("analogue_gain", None)
try:
exp = float(exp)
except Exception:
exp = 0.0
try:
gain = float(gain)
except Exception:
gain = 1.0
if exp <= 0:
return 0.0
if gain <= 0:
gain = 1.0
return float(exp * gain)

View File

@ -29,6 +29,112 @@ def chw_to_hwc(arr: np.ndarray) -> np.ndarray:
return np.transpose(arr, (1, 2, 0))
def tensor_to_preview_panels(tensor: np.ndarray):
"""
Recebe tensor CHW [R,G,B,RE,NIR] float32 e devolve painéis visuais.
"""
if tensor.ndim != 3 or tensor.shape[0] < 5:
raise RuntimeError(f"Tensor MULTISPEC inválido: shape={tensor.shape}")
rgb_hwc = np.transpose(tensor[:3].astype(np.float32), (1, 2, 0))
rgb_bgr = normalize_float01_to_bgr(rgb_hwc)
re01 = tensor[3].astype(np.float32)
nir01 = tensor[4].astype(np.float32)
re_bgr = cv2.cvtColor(
np.clip(re01 * 255.0, 0, 255).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
nir_bgr = cv2.cvtColor(
np.clip(nir01 * 255.0, 0, 255).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
return [
("MULTISPEC RGB final", rgb_bgr, f"tensor {list(tensor.shape)} | canais 0,1,2"),
("MULTISPEC RE final", re_bgr, "tensor canal 3"),
("MULTISPEC NIR final", nir_bgr, "tensor canal 4"),
]
def build_multispec_from_raw_native_multi(group: dict, meta: dict):
"""
Reconstrói o tensor MULTISPEC final a partir dos .bin RAW_BRUTO salvos.
Usa:
- saved_payload_paths
- saved_payload_shapes
- saved_payload_dtypes
- stream_meta.camera_info
- camera_params_json/module_params.json
"""
if meta.get("saved_payload_type") != "raw_native_multi":
return None, "captura não é raw_native_multi"
stream_meta = meta.get("stream_meta", {}) or {}
camera_info = stream_meta.get("camera_info", {}) or {}
saved_dtypes = meta.get("saved_payload_dtypes", {}) or {}
saved_shapes = meta.get("saved_payload_shapes", {}) or {}
frame = {}
for cam_id, path in group["cameras"].items():
saved_dtype = saved_dtypes.get(cam_id)
saved_shape = saved_shapes.get(cam_id)
if saved_dtype is None or saved_shape is None:
raise RuntimeError(f"Faltam dtype/shape para {cam_id}")
arr = np.fromfile(str(path), dtype=np.dtype(saved_dtype)).reshape(tuple(saved_shape))
frame[cam_id] = arr
if not frame:
raise RuntimeError("Nenhum payload de câmera encontrado para reconstruir MULTISPEC.")
sensor_width = int(meta.get("sensor_width", 1280))
sensor_height = int(meta.get("sensor_height", 800))
bayer = meta.get("bayer_pattern", "RGGB")
# Tenta usar o mesmo module_params que foi usado na captura.
calib_path = meta.get("camera_params_json") or "calibration/module_params.json"
# Se vier relativo, tenta resolver relativo ao diretório atual.
# Normalmente seu script roda da raiz do projeto, então calibration/module_params.json funciona.
if calib_path and not os.path.isfile(calib_path):
# fallback: tenta relativo à pasta do JSON
json_dir = Path(group["json"]).parent
alt = json_dir / calib_path
if alt.exists():
calib_path = str(alt)
else:
print(f"[WARN] module_params não encontrado: {calib_path}. Tentando sem calibração.")
calib_path = None
core = RawProcessorCore(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
calibration_json_path=calib_path,
)
# O decode precisa do stream_meta com camera_info.
processing_meta = dict(stream_meta)
# A normalização radiométrica precisa dos controles reais salvos no JSON da captura.
if meta.get("actual_camera_controls") is not None:
processing_meta["actual_camera_controls"] = meta.get("actual_camera_controls")
if meta.get("startup_camera_controls") is not None:
processing_meta["startup_camera_controls"] = meta.get("startup_camera_controls")
tensor = core.build_infer_tensor_from_stream(frame, processing_meta, 5)
return tensor, f"MULTISPEC gerado offline do RAW_BRUTO | shape={list(tensor.shape)}"
def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str | None = None) -> tuple[np.ndarray, str]:
"""
Retorna:
@ -236,12 +342,51 @@ def build_panels_from_group(group):
panels = []
meta = load_json(group["json"])
saved_type = meta.get("saved_payload_type")
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
if preview_saved is None:
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}"))
# =========================================================
# Para RAW_BRUTO multi, o primeiro painel vira o tensor final
# gerado offline a partir dos .bin salvos.
# =========================================================
if saved_type == "raw_native_multi":
try:
tensor, desc = build_multispec_from_raw_native_multi(group, meta)
tensor_panels = tensor_to_preview_panels(tensor)
# Aqui colocamos só o RGB final como painel principal,
# para substituir o antigo PNG salvo.
title, img, subtitle = tensor_panels[0]
panels.append((title, img, desc))
# Opcional: se quiser também ver RE/NIR finais do tensor,
# descomente estas duas linhas:
# panels.append(tensor_panels[1])
# panels.append(tensor_panels[2])
except Exception as e:
# Fallback para o PNG salvo caso a reconstrução falhe.
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
if preview_saved is None:
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
panels.append((
"Preview salvo fallback",
preview_saved,
f"Falha ao gerar MULTISPEC offline: {e}"
))
else:
# Para RGB/MULTISPEC salvos direto, mantém comportamento antigo.
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
if preview_saved is None:
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}"))
# =========================================================
# Se houver payload final único, reconstrói normalmente.
# Ex: saved_payload_type == multispec
# =========================================================
if group["final_raw"] is not None:
result, desc = build_visual_from_saved_payload(group["final_raw"], meta)
@ -251,6 +396,9 @@ def build_panels_from_group(group):
else:
panels.append(("Reconstruido (final)", result, desc))
# =========================================================
# Continua mostrando CAM_A/CAM_B/CAM_C reconstruídas individualmente.
# =========================================================
for cam_id, path in group["cameras"].items():
img, desc = build_visual_from_saved_payload(path, meta, cam_id=cam_id)
panels.append((f"{cam_id} reconstruido", img, desc))

View File

@ -96,7 +96,7 @@ def get_image_by_role(decoded: dict, role: str):
def validate_module_ready(status: dict, raw_policy: str):
if not status.get("ok", True):
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
raise RuntimeError(f"Status invalido retornado pelo modulo: {status}")
active_roles = status.get("active_roles", {}) or {}
active_count = int(status.get("camera_count_active", 0))
@ -254,6 +254,19 @@ def extract_channels_from_decoded(decoded: dict) -> dict:
_, re01 = get_image_by_role(decoded, "re")
_, nir01 = get_image_by_role(decoded, "nir")
def assert_not_raw10_packed_image(role, img, expected_w=1280):
if img is None:
return
if img.ndim == 2 and img.shape[1] == int(expected_w * 10 / 8):
raise RuntimeError(
f"{role.upper()} parece RAW10_PACKED interpretado como imagem: "
f"shape={img.shape}. Esperado decodificado com largura {expected_w}."
)
assert_not_raw10_packed_image("re", re01, expected_w=1280)
assert_not_raw10_packed_image("nir", nir01, expected_w=1280)
out = {}
if rgb01 is not None:
@ -354,7 +367,7 @@ def build_board(decoded, controls, state_lines, progress_lines, preview_scale=1.
"S = pular etapa dark/preto",
"Q / ESC = sair sem salvar",
"",
"Dica: branco/preto devem preencher todo o campo de visão.",
"Dica: branco/preto devem preencher todo o campo de visao.",
"Para dark-frame perfeito, tampe as lentes em vez de usar fundo preto.",
])
@ -420,12 +433,12 @@ def capture_stage(
progress_lines = [
f"Etapa: {stage_name}",
f"Descartando frames iniciais: {len(seen_frame_ids)}/{discard_frames}",
"Aguardando estabilização de exposição/stream...",
"Aguardando estabilizacao de exposicao/stream...",
]
board = build_board(
decoded=last_decoded,
controls=last_controls,
state_lines=[f"CALIBRAÇÃO FLAT-FIELD - {stage_name.upper()}"],
state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"],
progress_lines=progress_lines,
preview_scale=preview_scale,
)
@ -458,7 +471,7 @@ def capture_stage(
board = build_board(
decoded=last_decoded,
controls=last_controls,
state_lines=[f"CALIBRAÇÃO FLAT-FIELD - {stage_name.upper()}"],
state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"],
progress_lines=progress_lines,
preview_scale=preview_scale,
)
@ -473,7 +486,7 @@ def capture_stage(
k = cv2.waitKey(1) & 0xFF
if k in (ord("q"), ord("Q"), 27):
raise KeyboardInterrupt("Captura cancelada pelo usuário.")
raise KeyboardInterrupt("Captura cancelada pelo usuario.")
return channel_stack, controls_log, meta_log
@ -579,10 +592,10 @@ def show_final_preview(window_name: str, gain_maps: dict, preview_scale: float):
"Flat-field salvo com sucesso.",
"ENTER/qualquer tecla = fechar",
"",
"Use estes mapas antes da fusão geométrica.",
"Use estes mapas antes da fusao geometrica.",
"",
"Obs: RGB e mono podem ter shapes diferentes;",
"isso é normal se o decode gerar resoluções distintas.",
"isso e normal se o decode gerar resolucoes distintas.",
], x=18, y=36)
top = np.hstack([panels[0], panels[1], panels[2]])
@ -635,7 +648,7 @@ def wait_for_enter_or_skip(
if allow_skip and k in (ord("s"), ord("S")):
return "skip"
if k in (ord("q"), ord("Q"), 27):
raise KeyboardInterrupt("Cancelado pelo usuário.")
raise KeyboardInterrupt("Cancelado pelo usuario.")
# ============================================================
@ -644,7 +657,7 @@ def wait_for_enter_or_skip(
def main():
parser = argparse.ArgumentParser(
description="Calibrador automático de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.",
description="Calibrador automatico de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
@ -721,7 +734,7 @@ def main():
output_dtype="uint8",
capture_mode=args.capture_mode,
raw_policy=args.raw_policy,
module_calibration_json=args.module_calibration_json
module_calibration_json=args.module_calibration_json or None
) as cam:
validate_module_ready(cam.get_status(), args.raw_policy)
@ -731,11 +744,11 @@ def main():
window_name=window_name,
title="ETAPA 1/2 - WHITE / FLAT FIELD",
instruction_lines=[
"Posicione o módulo na altura real de operação.",
"Aponte para uma superfície branca/cinza fosca, uniforme e sem textura.",
"Evite reflexos, sombras laterais e saturação.",
"A superfície deve preencher todo o campo de visão.",
"Pressione ENTER para começar a captura WHITE.",
"Posicione o modulo na altura real de operacao.",
"Aponte para uma superficie branca/cinza fosca, uniforme e sem textura.",
"Evite reflexos, sombras laterais e saturacao.",
"A superficie deve preencher todo o campo de visao.",
"Pressione ENTER para comecar a captura WHITE.",
],
preview_scale=args.preview_scale,
allow_skip=False,
@ -767,10 +780,10 @@ def main():
window_name=window_name,
title="ETAPA 2/2 - DARK / PRETO",
instruction_lines=[
"Agora faça a captura dark/preto.",
"Melhor opção: tampe as lentes completamente.",
"Agora faca a captura dark/preto.",
"Melhor opcao: tampe as lentes completamente.",
"Alternativa: use fundo preto fosco preenchendo todo o frame.",
"Mantenha exposição/ganho iguais aos da etapa anterior, se possível.",
"Mantenha exposicao/ganho iguais aos da etapa anterior, se possivel.",
"Pressione ENTER para capturar DARK/PRETO.",
],
preview_scale=args.preview_scale,
@ -801,7 +814,7 @@ def main():
# Processamento robusto.
processing_panel = np.zeros((720, 1280, 3), dtype=np.uint8)
overlay_hud(processing_panel, [
"Processando calibração flat-field...",
"Processando calibracao flat-field...",
"Calculando medianas robustas por canal.",
"Gerando mapas de ganho e previews.",
], x=40, y=80, font_scale=0.8, line_step=34)

View File

@ -703,7 +703,7 @@ def main():
validate_module_ready(cam.get_status(), args.raw_policy)
while True:
raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0, update_radiometry=False)
raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0)
if raw_meta is not None and raw_meta.get("frame_id") != last_frame_id:
last_frame_id = raw_meta.get("frame_id")